Add robust weekly report templates
This commit is contained in:
40
infra/n8n/weekly-organization-report-code-compact.js
Normal file
40
infra/n8n/weekly-organization-report-code-compact.js
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
const esc = (value) => String(value ?? '')
|
||||||
|
.replace(/&/g, '&').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(`<tr class="clean"><td>${esc(device.name)}</td><td colspan="5">Keine sicherheitsrelevanten Ereignisse im Berichtszeitraum.</td><td><b class="ok">Sauber</b></td></tr>`); 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(`<tr class="alert"><td>${esc(device.name)}</td><td>${esc(event.type)}</td><td>${esc(event.account)}</td><td>${esc(fmt(event.latest))}</td><td>${event.count}</td><td>${esc(event.ip)}</td><td><b class="${device.state === 'critical' ? 'critical' : 'warning'}">${device.state === 'critical' ? 'Kritisch' : 'Pruefen'}</b></td></tr>`);
|
||||||
|
}
|
||||||
|
const risk = criticals ? ['Kritisch', 'critical'] : warnings ? ['Beobachten', 'warning'] : ['Unauffaellig', 'ok'];
|
||||||
|
const html = `<div class="ocs"><style>.ocs{font:13px Segoe UI,Tahoma,sans-serif;color:#172033;max-width:1160px}.ocs .head{background:#102a43;color:#fff;padding:18px 20px;border-radius:8px 8px 0 0}.ocs h1{margin:0;font-size:20px}.ocs .meta{color:#d5e2ee;margin-top:5px}.ocs .risk{float:right;padding:4px 8px;border-radius:12px}.ocs .stats{width:100%;border-collapse:separate;border-spacing:8px;margin:8px -8px}.ocs .stats td{width:16%;padding:9px;background:#f4f8fc;border:1px solid #dbe5ef}.ocs .stats b{display:block;font-size:20px;color:#102a43}.ocs table{width:100%;border-collapse:collapse}.ocs th{background:#1d4e89;color:#fff;text-align:left;padding:8px;font-size:11px}.ocs td{padding:8px;border-bottom:1px solid #dbe5ef;vertical-align:top}.ocs .alert{background:#fff8f3}.ocs .clean{background:#f3fbf6}.ocs .ok,.ocs .warning,.ocs .critical{padding:3px 6px;border-radius:4px}.ocs .ok{background:#d1fae5;color:#065f46}.ocs .warning{background:#fef3c7;color:#92400e}.ocs .critical{background:#fee2e2;color:#991b1b}.ocs .foot{margin-top:12px;text-align:right;color:#64748b;font-size:10px}</style><div class="head"><b class="risk ${risk[1]}">${risk[0]}</b><h1>OfficeCom Sentinel Sicherheitsbericht</h1><div class="meta">${esc(group.name)} | ${esc(start.toLocaleDateString('de-DE'))} bis ${esc(end.toLocaleDateString('de-DE'))}</div></div><table class="stats"><tr><td><b>${group.devices.length}</b>Geräte</td><td><b>${criticals}</b>Kritisch</td><td><b>${warnings}</b>Warnungen</td><td><b>${totalEvents}</b>Ereignisse</td><td><b>${ips.size}</b>Quell-IP-Adressen</td><td><b>${cveCritical}</b>Kritische CVEs</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.join('')}</tbody></table><div class="foot">Automatisch erstellt am ${esc(fmt(now))} durch OfficeCom Sentinel.</div></div>`;
|
||||||
|
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;
|
||||||
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