diff --git a/alert.sh b/alert.sh index 12969c6..7642fc2 100755 --- a/alert.sh +++ b/alert.sh @@ -23,16 +23,19 @@ if [ -z "$MESSAGE" ] && [ -z "$HTML_FILE" ] && [ -z "$IMAGE_FILE" ]; then exit 1 fi -# Build jq args -JQARGS=(--arg msg "$MESSAGE") +TMPDIR_PAYLOAD=$(mktemp -d) +trap 'rm -rf "$TMPDIR_PAYLOAD"' EXIT -HTML="" +# HTML +HTML_TMP="$TMPDIR_PAYLOAD/html.txt" if [ -n "$HTML_FILE" ] && [ -f "$HTML_FILE" ]; then - HTML=$(cat "$HTML_FILE") + cp "$HTML_FILE" "$HTML_TMP" +else + printf '' > "$HTML_TMP" fi -JQARGS+=(--arg html "$HTML") -IMAGE="" +# Image : encodée en base64 dans un fichier temporaire (évite "Argument list too long") +IMAGE_TMP="$TMPDIR_PAYLOAD/image.txt" if [ -n "$IMAGE_FILE" ] && [ -f "$IMAGE_FILE" ]; then case "${IMAGE_FILE##*.}" in jpg|jpeg) MIME="image/jpeg" ;; @@ -41,14 +44,21 @@ if [ -n "$IMAGE_FILE" ] && [ -f "$IMAGE_FILE" ]; then webp) MIME="image/webp" ;; *) MIME="image/png" ;; esac - IMAGE="data:${MIME};base64,$(base64 "$IMAGE_FILE" | tr -d '\n')" + printf 'data:%s;base64,' "$MIME" > "$IMAGE_TMP" + base64 -i "$IMAGE_FILE" | tr -d '\n' >> "$IMAGE_TMP" +else + printf '' > "$IMAGE_TMP" fi -JQARGS+=(--arg image "$IMAGE") -PAYLOAD=$(jq -n "${JQARGS[@]}" '{"message":$msg,"html":$html,"image":$image}') +PAYLOAD_TMP="$TMPDIR_PAYLOAD/payload.json" +jq -n \ + --arg msg "$MESSAGE" \ + --rawfile html "$HTML_TMP" \ + --rawfile image "$IMAGE_TMP" \ + '{"message":$msg,"html":$html,"image":$image}' > "$PAYLOAD_TMP" curl -sf -X POST "http://localhost:${PORT}/api/alert" \ -H "Content-Type: application/json" \ - -d "$PAYLOAD" \ + -d "@$PAYLOAD_TMP" \ && echo "✓ Alerte déclenchée : $MESSAGE" \ || echo "✗ Serveur non disponible sur le port ${PORT}" diff --git a/news.mp3 b/news.mp3 new file mode 100644 index 0000000..68ef293 Binary files /dev/null and b/news.mp3 differ diff --git a/public/app.js b/public/app.js index 3984800..b941e96 100644 --- a/public/app.js +++ b/public/app.js @@ -56,6 +56,38 @@ const alertIconEl = document.getElementById('alert-icon'); const alertImageEl = document.getElementById('alert-image'); const infoAlert = document.getElementById('info-alert'); +// ── Notification (ANSSI / geo) ──────────────────────────────────────────────── + +const notifOverlay = document.getElementById('notif-overlay'); +const notifMessage = document.getElementById('notif-message'); +const notifBarInner = document.getElementById('notif-bar-inner'); +const softAlarmAudio = new Audio('/soft_alarm.mp3'); +let notifTimer = null; + +function showNotif(message) { + notifMessage.textContent = message; + + // Re-déclencher l'animation de la barre + notifBarInner.style.animation = 'none'; + void notifBarInner.offsetWidth; + notifBarInner.style.animation = ''; + + notifOverlay.style.animation = 'none'; + void notifOverlay.offsetWidth; + notifOverlay.style.animation = ''; + + notifOverlay.classList.remove('hidden'); + + softAlarmAudio.currentTime = 0; + softAlarmAudio.play().catch(err => console.error('soft_alarm:', err)); + + if (notifTimer) clearTimeout(notifTimer); + notifTimer = setTimeout(() => { + notifOverlay.classList.add('hidden'); + notifTimer = null; + }, 10_000); +} + // ── Alarm sound ─────────────────────────────────────────────────────────────── const alarmAudio = new Audio('/alert.mp3'); @@ -141,6 +173,7 @@ function updateClock() { const anssiList = document.getElementById('anssi-list'); const anssiStatus = document.getElementById('anssi-status'); +let seenAnssiLinks = null; async function loadAnssi() { anssiStatus.textContent = '...'; @@ -156,6 +189,17 @@ async function loadAnssi() { return; } + const currentAnssiLinks = new Set(items.map(i => i.link)); + if (seenAnssiLinks === null) { + seenAnssiLinks = currentAnssiLinks; + } else { + const newItems = items.filter(i => !seenAnssiLinks.has(i.link)); + if (newItems.length) { + showNotif(`Nouveau bulletin ANSSI : ${newItems[0].title}`); + seenAnssiLinks = currentAnssiLinks; + } + } + items.forEach(item => { const li = document.createElement('li'); li.className = 'anssi-item'; @@ -180,66 +224,58 @@ async function loadAnssi() { } } -// ── CVE feed ────────────────────────────────────────────────────────────────── +// ── Géopolitique feed ───────────────────────────────────────────────────────── -const cveList = document.getElementById('cve-list'); -const cveStatus = document.getElementById('cve-status'); +const geoList = document.getElementById('geo-list'); +const geoStatus = document.getElementById('geo-status'); +let seenGeoLinks = null; -function severityClass(cvss) { - if (cvss === undefined || cvss === null) return 'unknown'; - const score = parseFloat(cvss); - if (isNaN(score)) return 'unknown'; - if (score >= 9.0) return 'critical'; - if (score >= 7.0) return 'high'; - if (score >= 4.0) return 'medium'; - return 'low'; -} - -function severityLabel(cvss) { - const cls = severityClass(cvss); - const map = { critical: 'CRITIQUE', high: 'ÉLEVÉ', medium: 'MOYEN', low: 'FAIBLE', unknown: '???' }; - return map[cls]; -} - -async function loadCve() { - cveStatus.textContent = '...'; - cveStatus.className = 'widget-status'; +async function loadGeo() { + geoStatus.textContent = '...'; + geoStatus.className = 'widget-status'; try { - const resp = await fetch('/api/feeds/cve'); + const resp = await fetch('/api/feeds/geo'); if (!resp.ok) throw new Error(`HTTP ${resp.status}`); const items = await resp.json(); - cveList.innerHTML = ''; + geoList.innerHTML = ''; if (!items.length) { - cveList.innerHTML = '
  • Aucune CVE
  • '; + geoList.innerHTML = '
  • Aucune actualité
  • '; return; } - items.forEach(item => { - const cvss = item.cvss ?? item['cvss-score'] ?? null; - const cls = severityClass(cvss); - const label = severityLabel(cvss); - const desc = item.summary || item.description || ''; - const cveId = item.id || item['cve-id'] || 'CVE-????-????'; + const currentLinks = new Set(items.map(i => i.link)); + if (seenGeoLinks === null) { + // Premier chargement : on mémorise sans jouer + seenGeoLinks = currentLinks; + } else { + const hasNew = items.some(i => !seenGeoLinks.has(i.link)); + if (hasNew) { + showNotif('Nouvelle actualité géopolitique'); + seenGeoLinks = currentLinks; + } + } + + items.forEach(item => { const li = document.createElement('li'); - li.className = 'cve-item'; + li.className = 'anssi-item'; + const date = item.pubDate + ? new Date(item.pubDate).toLocaleDateString('fr-FR', { day: '2-digit', month: 'short', hour: '2-digit', minute: '2-digit' }) + : ''; li.innerHTML = ` - ${escapeHtml(label)} -
    -
    ${escapeHtml(cveId)}
    -
    ${escapeHtml(desc)}
    -
    + ${escapeHtml(item.title)} + ${date ? `${escapeHtml(item.source ? item.source + ' · ' : '')}${escapeHtml(date)}` : ''} `; - cveList.appendChild(li); + geoList.appendChild(li); }); - cveStatus.textContent = `${items.length} CVEs`; - cveStatus.className = 'widget-status ok'; + geoStatus.textContent = `${items.length} articles`; + geoStatus.className = 'widget-status ok'; } catch (err) { - cveStatus.textContent = 'ERREUR'; - cveStatus.className = 'widget-status err'; - cveList.innerHTML = `
  • Erreur : ${escapeHtml(err.message)}
  • `; + geoStatus.textContent = 'ERREUR'; + geoStatus.className = 'widget-status err'; + geoList.innerHTML = `
  • Erreur : ${escapeHtml(err.message)}
  • `; } } @@ -253,9 +289,9 @@ function init() { setInterval(updateClock, 1000); loadAnssi(); - loadCve(); + loadGeo(); setInterval(loadAnssi, 5 * 60 * 1000); - setInterval(loadCve, 5 * 60 * 1000); + setInterval(loadGeo, 5 * 60 * 1000); } // ── Helpers ─────────────────────────────────────────────────────────────────── diff --git a/public/index.html b/public/index.html index a8e958e..9f40921 100644 --- a/public/index.html +++ b/public/index.html @@ -8,6 +8,15 @@ + + +