Add robust weekly report templates
This commit is contained in:
129
infra/n8n/weekly-organization-report-code.js
Normal file
129
infra/n8n/weekly-organization-report-code.js
Normal file
@@ -0,0 +1,129 @@
|
||||
const esc = (value) => String(value ?? '')
|
||||
.replace(/&/g, '&')
|
||||
.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(`<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ä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;
|
||||
Reference in New Issue
Block a user