Files
oc-sentinel/infra/n8n/weekly-organization-report-code.js
OfficeCom Codex 585b4f91b1
All checks were successful
OfficeCom Sentinel Client / validate-client (push) Successful in 24s
OfficeCom Sentinel Client / build-client-windows (push) Successful in 49s
Add robust weekly report templates
2026-07-27 12:43:09 +02:00

130 lines
7.8 KiB
JavaScript

const esc = (value) => String(value ?? '')
.replace(/&/g, '&')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;')
// 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(`<tr class="clean"><td>${esc(device.machineName)}</td><td colspan="5">Keine sicherheitsrelevanten Ereignisse im Berichtszeitraum.</td><td><span class="badge badge-ok">Sauber</span></td></tr>`);
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(`<tr class="alert"><td>${esc(device.machineName)}</td><td>${esc(event.type)}</td><td>${esc(event.account)}</td><td>${esc(formatDate(event.latest))}</td><td>${event.count}</td><td>${esc(event.sourceIp)}</td><td><span class="badge ${badge}">${severity}</span></td></tr>`);
}
}
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 = `<div class="ocsentinel-report">
<style>
.ocsentinel-report{max-width:1180px;margin:0 auto;font-family:Segoe UI,Tahoma,sans-serif;font-size:13px;line-height:1.35;color:#172033;background:#fff}
.ocsentinel-report .header{padding:18px 20px;background:#102a43;color:#fff;border-radius:8px 8px 0 0}
.ocsentinel-report h1{margin:0;font-size:20px;line-height:1.2}.ocsentinel-report .subtitle{margin-top:5px;color:#cbd5e1;font-size:12px}
.ocsentinel-report .risk{float:right;padding:5px 9px;border-radius:999px;font-size:11px;font-weight:700}.ocsentinel-report .risk-ok{background:#d1fae5;color:#065f46}.ocsentinel-report .risk-warning{background:#fef3c7;color:#92400e}.ocsentinel-report .risk-critical{background:#fee2e2;color:#991b1b}
.ocsentinel-report .body{padding:16px 20px 20px;border:1px solid #dbe5ef;border-top:0}.ocsentinel-report .metrics{width:100%;border-collapse:separate;border-spacing:8px 0;margin:0 -8px 15px}.ocsentinel-report .metrics td{width:16.66%;padding:10px;background:#f6f9fc;border:1px solid #dbe5ef;border-radius:5px}.ocsentinel-report .metric-value{display:block;font-size:20px;font-weight:700;color:#102a43}.ocsentinel-report .metric-label{display:block;font-size:10px;color:#526577;text-transform:uppercase;letter-spacing:.04em}
.ocsentinel-report table{width:100%;border-collapse:collapse}.ocsentinel-report th{padding:8px;background:#1d4e89;color:#fff;text-align:left;font-size:11px}.ocsentinel-report td{padding:8px;border-bottom:1px solid #dbe5ef;vertical-align:top}.ocsentinel-report tr.alert{background:#fff8f3}.ocsentinel-report tr.clean{background:#f3fbf6;color:#275b3b}.ocsentinel-report .badge{display:inline-block;padding:3px 6px;border-radius:4px;font-size:10px;font-weight:700}.ocsentinel-report .badge-ok{background:#d1fae5;color:#065f46}.ocsentinel-report .badge-warning{background:#fef3c7;color:#92400e}.ocsentinel-report .badge-critical{background:#fee2e2;color:#991b1b}.ocsentinel-report .footer{margin-top:14px;color:#64748b;font-size:10px;text-align:right}
</style>
<div class="header"><span class="risk ${riskClass}">${riskLabel}</span><h1>OfficeCom Sentinel Sicherheitsbericht</h1><div class="subtitle">${esc(group.organizationName)} | ${esc(periodStart.toLocaleDateString('de-DE'))} bis ${esc(periodEnd.toLocaleDateString('de-DE'))}</div></div>
<div class="body"><table class="metrics"><tr><td><span class="metric-value">${summary.deviceCount}</span><span class="metric-label">Ger&auml;te</span></td><td><span class="metric-value">${summary.criticalCount}</span><span class="metric-label">Kritisch</span></td><td><span class="metric-value">${summary.warningCount}</span><span class="metric-label">Warnungen</span></td><td><span class="metric-value">${summary.totalEvents}</span><span class="metric-label">Ereignisse</span></td><td><span class="metric-value">${summary.uniqueIps}</span><span class="metric-label">Quell-IP-Adressen</span></td><td><span class="metric-value">${summary.cveCritical}</span><span class="metric-label">Kritische CVEs</span></td></tr></table>
<table><thead><tr><th>System</th><th>Vorfall</th><th>Konto</th><th>Letzter Zeitpunkt</th><th>Anzahl</th><th>Quell-IP</th><th>Bewertung</th></tr></thead><tbody>${rows}</tbody></table><div class="footer">Automatisch erstellt am ${esc(formatDate(now))} durch OfficeCom Sentinel.</div></div></div>`;
reportItems.push({
json: {
organizationId: group.organizationId,
organizationName: group.organizationName,
periodStartUtc: periodStart.toISOString(),
periodEndUtc: periodEnd.toISOString(),
...summary,
reportHtml,
summaryJson: JSON.stringify(summary)
}
});
}
return reportItems;