From 585b4f91b1d756def9d292339e2520c6a9590bbc Mon Sep 17 00:00:00 2001 From: OfficeCom Codex Date: Mon, 27 Jul 2026 12:43:09 +0200 Subject: [PATCH] Add robust weekly report templates --- ...weekly-organization-report-code-compact.js | 40 ++++++ infra/n8n/weekly-organization-report-code.js | 129 ++++++++++++++++++ 2 files changed, 169 insertions(+) create mode 100644 infra/n8n/weekly-organization-report-code-compact.js create mode 100644 infra/n8n/weekly-organization-report-code.js diff --git a/infra/n8n/weekly-organization-report-code-compact.js b/infra/n8n/weekly-organization-report-code-compact.js new file mode 100644 index 0000000..12d1835 --- /dev/null +++ b/infra/n8n/weekly-organization-report-code-compact.js @@ -0,0 +1,40 @@ +const esc = (value) => String(value ?? '') + .replace(/&/g, '&').replace(//g, '>') + .replace(/"/g, '"').replace(/'/g, ''') + .replace(/[^\x20-\x7e]/g, (character) => `&#${character.codePointAt(0)};`); +const fmt = (value) => new Date(value).toLocaleString('de-DE', { timeZone: 'Europe/Berlin', dateStyle: 'medium', timeStyle: 'short' }); +const now = new Date(); +const end = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate())); +end.setUTCDate(end.getUTCDate() - ((end.getUTCDay() + 6) % 7)); +const start = new Date(end.getTime() - 7 * 86400000); +const groups = new Map(); +for (const item of items) { + const payload = item.json.payload || {}, ninja = payload.NinjaOne || payload.ninjaOne || {}; + const id = String(ninja.OrganizationId || ninja.organizationId || 'unknown'); + if (!groups.has(id)) groups.set(id, { id, name: String(ninja.OrganizationName || ninja.organizationName || `Organisation ${id}`), devices: [] }); + groups.get(id).devices.push({ name: item.json.machine_name || payload.MachineName || 'Unbekannt', state: String(item.json.alert_state || payload.AlertState || 'unknown').toLowerCase(), payload }); +} +const output = []; +for (const group of groups.values()) { + let warnings = 0, criticals = 0, totalEvents = 0, cveCritical = 0; + const ips = new Set(), rows = []; + for (const device of group.devices.sort((a, b) => a.name.localeCompare(b.name))) { + if (device.state === 'warning') warnings++; + if (device.state === 'critical') criticals++; + cveCritical += Number((device.payload.VulnerabilityCorrelation || {}).CriticalCount || 0); + const events = (device.payload.Events || []).filter((event) => { const date = new Date(event.Timestamp); return !Number.isNaN(date) && date >= start && date < end; }); + totalEvents += events.length; + if (!events.length) { rows.push(`${esc(device.name)}Keine sicherheitsrelevanten Ereignisse im Berichtszeitraum.Sauber`); continue; } + const grouped = new Map(); + for (const event of events) { + const ip = event.SourceIp || '-', account = event.Username || '-', type = event.Target || 'Sicherheitsereignis', key = [type, account, ip].join('|'); + const entry = grouped.get(key) || { ip, account, type, count: 0, latest: event.Timestamp }; + entry.count++; if (new Date(event.Timestamp) > new Date(entry.latest)) entry.latest = event.Timestamp; grouped.set(key, entry); if (ip !== '-') ips.add(ip); + } + for (const event of grouped.values()) rows.push(`${esc(device.name)}${esc(event.type)}${esc(event.account)}${esc(fmt(event.latest))}${event.count}${esc(event.ip)}${device.state === 'critical' ? 'Kritisch' : 'Pruefen'}`); + } + const risk = criticals ? ['Kritisch', 'critical'] : warnings ? ['Beobachten', 'warning'] : ['Unauffaellig', 'ok']; + const html = `
${risk[0]}

OfficeCom Sentinel Sicherheitsbericht

${esc(group.name)} | ${esc(start.toLocaleDateString('de-DE'))} bis ${esc(end.toLocaleDateString('de-DE'))}
${group.devices.length}Geräte${criticals}Kritisch${warnings}Warnungen${totalEvents}Ereignisse${ips.size}Quell-IP-Adressen${cveCritical}Kritische CVEs
${rows.join('')}
SystemVorfallKontoLetzter ZeitpunktAnzahlQuell-IPBewertung
Automatisch erstellt am ${esc(fmt(now))} durch OfficeCom Sentinel.
`; + output.push({ json: { organizationId: group.id, organizationName: group.name, periodStartUtc: start.toISOString(), periodEndUtc: end.toISOString(), deviceCount: group.devices.length, warningCount: warnings, criticalCount: criticals, totalEvents, uniqueIps: ips.size, cveTotal: 0, cveCritical, reportHtml: html, summaryJson: JSON.stringify({ deviceCount: group.devices.length, warningCount: warnings, criticalCount: criticals, totalEvents, uniqueIps: ips.size, cveCritical }) } }); +} +return output; diff --git a/infra/n8n/weekly-organization-report-code.js b/infra/n8n/weekly-organization-report-code.js new file mode 100644 index 0000000..6604849 --- /dev/null +++ b/infra/n8n/weekly-organization-report-code.js @@ -0,0 +1,129 @@ +const esc = (value) => String(value ?? '') + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, ''') + // Numeric entities keep German text intact in every supported mail client. + .replace(/[^\x20-\x7e]/g, (character) => `&#${character.codePointAt(0)};`); + +const formatDate = (value) => new Date(value).toLocaleString('de-DE', { + timeZone: 'Europe/Berlin', + dateStyle: 'medium', + timeStyle: 'short' +}); + +const now = new Date(); +const periodEnd = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate())); +periodEnd.setUTCDate(periodEnd.getUTCDate() - ((periodEnd.getUTCDay() + 6) % 7)); +const periodStart = new Date(periodEnd.getTime() - 7 * 86400000); +const groups = new Map(); + +for (const item of items) { + const payload = item.json.payload || {}; + const ninja = payload.NinjaOne || payload.ninjaOne || {}; + const organizationId = String(ninja.OrganizationId || ninja.organizationId || 'unknown'); + const organizationName = String(ninja.OrganizationName || ninja.organizationName || `Organisation ${organizationId}`); + + if (!groups.has(organizationId)) { + groups.set(organizationId, { organizationId, organizationName, devices: [] }); + } + + groups.get(organizationId).devices.push({ + machineName: item.json.machine_name || payload.MachineName || 'Unbekannt', + state: String(item.json.alert_state || payload.AlertState || 'unknown').toLowerCase(), + payload + }); +} + +const reportItems = []; + +for (const group of groups.values()) { + let warningCount = 0; + let criticalCount = 0; + let totalEvents = 0; + let cveTotal = 0; + let cveCritical = 0; + const uniqueIps = new Set(); + const alertRows = []; + const cleanRows = []; + + for (const device of group.devices.sort((left, right) => left.machineName.localeCompare(right.machineName))) { + if (device.state === 'warning') warningCount++; + if (device.state === 'critical') criticalCount++; + + const vulnerabilities = device.payload.VulnerabilityCorrelation || {}; + cveTotal += Number(vulnerabilities.TotalCount || 0); + cveCritical += Number(vulnerabilities.CriticalCount || 0); + + const events = (device.payload.Events || []).filter((event) => { + const timestamp = new Date(event.Timestamp); + return !Number.isNaN(timestamp) && timestamp >= periodStart && timestamp < periodEnd; + }); + totalEvents += events.length; + + if (events.length === 0) { + cleanRows.push(`${esc(device.machineName)}Keine sicherheitsrelevanten Ereignisse im Berichtszeitraum.Sauber`); + continue; + } + + const groupedEvents = new Map(); + for (const event of events) { + const sourceIp = event.SourceIp || '-'; + const account = event.Username || '-'; + const type = event.Target || 'Sicherheitsereignis'; + const key = [type, account, sourceIp].join('|'); + const row = groupedEvents.get(key) || { type, account, sourceIp, count: 0, latest: event.Timestamp }; + row.count++; + if (new Date(event.Timestamp) > new Date(row.latest)) row.latest = event.Timestamp; + groupedEvents.set(key, row); + if (sourceIp !== '-') uniqueIps.add(sourceIp); + } + + for (const event of groupedEvents.values()) { + const severity = device.state === 'critical' ? 'Kritisch' : 'Pruefen'; + const badge = device.state === 'critical' ? 'badge-critical' : 'badge-warning'; + alertRows.push(`${esc(device.machineName)}${esc(event.type)}${esc(event.account)}${esc(formatDate(event.latest))}${event.count}${esc(event.sourceIp)}${severity}`); + } + } + + const summary = { + deviceCount: group.devices.length, + warningCount, + criticalCount, + totalEvents, + uniqueIps: uniqueIps.size, + cveTotal, + cveCritical + }; + const riskLabel = criticalCount > 0 ? 'Kritisch' : warningCount > 0 ? 'Beobachten' : 'Unauffaellig'; + const riskClass = criticalCount > 0 ? 'risk-critical' : warningCount > 0 ? 'risk-warning' : 'risk-ok'; + const rows = alertRows.length > 0 ? `${alertRows.join('')}${cleanRows.join('')}` : cleanRows.join(''); + + const reportHtml = `
+ +
${riskLabel}

OfficeCom Sentinel Sicherheitsbericht

${esc(group.organizationName)} | ${esc(periodStart.toLocaleDateString('de-DE'))} bis ${esc(periodEnd.toLocaleDateString('de-DE'))}
+
${summary.deviceCount}Geräte${summary.criticalCount}Kritisch${summary.warningCount}Warnungen${summary.totalEvents}Ereignisse${summary.uniqueIps}Quell-IP-Adressen${summary.cveCritical}Kritische CVEs
+${rows}
SystemVorfallKontoLetzter ZeitpunktAnzahlQuell-IPBewertung
`; + + reportItems.push({ + json: { + organizationId: group.organizationId, + organizationName: group.organizationName, + periodStartUtc: periodStart.toISOString(), + periodEndUtc: periodEnd.toISOString(), + ...summary, + reportHtml, + summaryJson: JSON.stringify(summary) + } + }); +} + +return reportItems;