223 lines
14 KiB
JSON
223 lines
14 KiB
JSON
{
|
|
"id": "OCwRpt7eK3mQ2xL9",
|
|
"name": "OCSentinel - Weekly Organization Reports",
|
|
"nodes": [
|
|
{
|
|
"parameters": {
|
|
"rule": {
|
|
"interval": [
|
|
{
|
|
"field": "weeks",
|
|
"weeksInterval": 1,
|
|
"triggerAtDay": [
|
|
1
|
|
],
|
|
"triggerAtHour": 7,
|
|
"triggerAtMinute": 20
|
|
}
|
|
]
|
|
}
|
|
},
|
|
"id": "schedule-weekly-reports",
|
|
"name": "Every Monday 07:20",
|
|
"type": "n8n-nodes-base.scheduleTrigger",
|
|
"typeVersion": 1.3,
|
|
"position": [
|
|
300,
|
|
300
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"operation": "executeQuery",
|
|
"query": "WITH latest AS (\n SELECT DISTINCT ON (d.id) d.machine_name, r.alert_state, r.payload\n FROM ocsentinel.scan_report AS r\n JOIN ocsentinel.device AS d ON d.id = r.device_id\n WHERE r.received_at >= now() - interval '8 days'\n ORDER BY d.id, r.generated_at_utc DESC, r.received_at DESC\n)\nSELECT machine_name, alert_state, payload\nFROM latest\nORDER BY payload #>> '{NinjaOne,OrganizationName}', machine_name;"
|
|
},
|
|
"id": "load-weekly-data",
|
|
"name": "Load Latest Device Reports",
|
|
"type": "n8n-nodes-base.postgres",
|
|
"typeVersion": 2.5,
|
|
"position": [
|
|
560,
|
|
300
|
|
],
|
|
"credentials": {
|
|
"postgres": {
|
|
"id": "WkjY0kIF3kHvREys",
|
|
"name": "OCSentinel PostgreSQL"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "const esc = (value) => String(value ?? '')\n .replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')\n .replace(/\"/g, '"').replace(/'/g, ''')\n .replace(/[^\\x20-\\x7e]/g, (character) => `&#${character.codePointAt(0)};`);\nconst now = new Date();\nconst end = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()));\nend.setUTCDate(end.getUTCDate() - ((end.getUTCDay() + 6) % 7));\nconst start = new Date(end.getTime() - 7 * 86400000);\nconst formatDate = (value) => new Date(value).toLocaleString('de-DE', { timeZone: 'Europe/Berlin', dateStyle: 'medium', timeStyle: 'short' });\nconst groups = new Map();\nfor (const item of items) {\n const payload = item.json.payload || {}, ninja = payload.NinjaOne || payload.ninjaOne || {};\n const id = String(ninja.OrganizationId || ninja.organizationId || 'unknown');\n if (!groups.has(id)) groups.set(id, { id, name: String(ninja.OrganizationName || ninja.organizationName || `Organisation ${id}`), devices: [] });\n groups.get(id).devices.push({ name: item.json.machine_name || payload.MachineName || 'Unbekannt', state: String(item.json.alert_state || payload.AlertState || 'unknown').toLowerCase(), payload });\n}\nconst output = [];\nfor (const group of groups.values()) {\n let warnings = 0, criticals = 0, totalEvents = 0, cveCritical = 0;\n const ips = new Set(), rows = [];\n for (const device of group.devices.sort((a, b) => a.name.localeCompare(b.name))) {\n if (device.state === 'warning') warnings++;\n if (device.state === 'critical') criticals++;\n cveCritical += Number((device.payload.VulnerabilityCorrelation || {}).CriticalCount || 0);\n const events = (device.payload.Events || []).filter((event) => { const timestamp = new Date(event.Timestamp); return !Number.isNaN(timestamp) && timestamp >= start && timestamp < end; });\n totalEvents += events.length;\n if (!events.length) { rows.push(`<tr bgcolor=\"#f0fdf4\"><td style=\"padding:8px;border-bottom:1px solid #dbe5ef;font-family:Arial,sans-serif;font-size:12px\">${esc(device.name)}</td><td colspan=\"5\" style=\"padding:8px;border-bottom:1px solid #dbe5ef;font-family:Arial,sans-serif;font-size:12px;color:#166534\">Keine sicherheitsrelevanten Ereignisse im Berichtszeitraum.</td><td style=\"padding:8px;border-bottom:1px solid #dbe5ef;font-family:Arial,sans-serif;font-size:12px;color:#166534\"><b>Sauber</b></td></tr>`); continue; }\n const grouped = new Map();\n for (const event of events) {\n const ip = event.SourceIp || '-', account = event.Username || '-', type = event.Target || 'Sicherheitsereignis', key = [type, account, ip].join('|');\n const entry = grouped.get(key) || { ip, account, type, count: 0, latest: event.Timestamp };\n entry.count++; if (new Date(event.Timestamp) > new Date(entry.latest)) entry.latest = event.Timestamp; grouped.set(key, entry); if (ip !== '-') ips.add(ip);\n }\n for (const event of grouped.values()) { const critical = device.state === 'critical'; rows.push(`<tr bgcolor=\"#fff7ed\"><td style=\"padding:8px;border-bottom:1px solid #fed7aa;font-family:Arial,sans-serif;font-size:12px\">${esc(device.name)}</td><td style=\"padding:8px;border-bottom:1px solid #fed7aa;font-family:Arial,sans-serif;font-size:12px\">${esc(event.type)}</td><td style=\"padding:8px;border-bottom:1px solid #fed7aa;font-family:Arial,sans-serif;font-size:12px\">${esc(event.account)}</td><td style=\"padding:8px;border-bottom:1px solid #fed7aa;font-family:Arial,sans-serif;font-size:12px\">${esc(formatDate(event.latest))}</td><td style=\"padding:8px;border-bottom:1px solid #fed7aa;font-family:Arial,sans-serif;font-size:12px\">${event.count}</td><td style=\"padding:8px;border-bottom:1px solid #fed7aa;font-family:Arial,sans-serif;font-size:12px\">${esc(event.ip)}</td><td style=\"padding:8px;border-bottom:1px solid #fed7aa;font-family:Arial,sans-serif;font-size:12px;color:${critical ? '#991b1b' : '#92400e'}\"><b>${critical ? 'Kritisch' : 'Pruefen'}</b></td></tr>`); }\n }\n const risk = criticals ? ['Kritisch', '#991b1b', '#fee2e2'] : warnings ? ['Beobachten', '#92400e', '#fef3c7'] : ['Unauffaellig', '#166534', '#dcfce7'];\n const metric = (value, label) => `<td width=\"16.66%\" style=\"padding:10px;background:#f8fafc;border:1px solid #dbe5ef;font-family:Arial,sans-serif\"><b style=\"display:block;font-size:20px;color:#102a43\">${value}</b><span style=\"font-size:10px;color:#526577\">${label}</span></td>`;\n const html = `<table role=\"presentation\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\" border=\"0\" style=\"max-width:1100px;border-collapse:collapse\"><tr><td style=\"padding:18px 20px;background:#102a43;font-family:Arial,sans-serif;color:#ffffff\"><span style=\"float:right;padding:5px 9px;background:${risk[2]};color:${risk[1]};font-size:11px\"><b>${risk[0]}</b></span><h2 style=\"margin:0;font-size:20px;color:#ffffff\">OfficeCom Sentinel Sicherheitsbericht</h2><p style=\"margin:6px 0 0;color:#d5e2ee;font-size:12px\">${esc(group.name)} | ${esc(start.toLocaleDateString('de-DE'))} bis ${esc(end.toLocaleDateString('de-DE'))}</p></td></tr><tr><td style=\"padding:16px 20px;border:1px solid #dbe5ef\"><table role=\"presentation\" width=\"100%\" cellspacing=\"6\" cellpadding=\"0\" border=\"0\"><tr>${metric(group.devices.length, 'Geräte')}${metric(criticals, 'Kritisch')}${metric(warnings, 'Warnungen')}${metric(totalEvents, 'Ereignisse')}${metric(ips.size, 'Quell-IP-Adressen')}${metric(cveCritical, 'Kritische CVEs')}</tr></table><table width=\"100%\" cellspacing=\"0\" cellpadding=\"0\" border=\"0\" style=\"border-collapse:collapse\"><thead><tr bgcolor=\"#1d4e89\"><th align=\"left\" style=\"padding:8px;color:#fff;font-family:Arial,sans-serif;font-size:11px\">System</th><th align=\"left\" style=\"padding:8px;color:#fff;font-family:Arial,sans-serif;font-size:11px\">Vorfall</th><th align=\"left\" style=\"padding:8px;color:#fff;font-family:Arial,sans-serif;font-size:11px\">Konto</th><th align=\"left\" style=\"padding:8px;color:#fff;font-family:Arial,sans-serif;font-size:11px\">Letzter Zeitpunkt</th><th align=\"left\" style=\"padding:8px;color:#fff;font-family:Arial,sans-serif;font-size:11px\">Anzahl</th><th align=\"left\" style=\"padding:8px;color:#fff;font-family:Arial,sans-serif;font-size:11px\">Quell-IP</th><th align=\"left\" style=\"padding:8px;color:#fff;font-family:Arial,sans-serif;font-size:11px\">Bewertung</th></tr></thead><tbody>${rows.join('')}</tbody></table><p style=\"margin:14px 0 0;text-align:right;color:#64748b;font-family:Arial,sans-serif;font-size:10px\">Automatisch erstellt am ${esc(formatDate(now))} durch OfficeCom Sentinel.</p></td></tr></table>`;\n 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 }) } });\n}\nreturn output;\n"
|
|
},
|
|
"id": "build-weekly-reports",
|
|
"name": "Build Organization HTML Reports",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
820,
|
|
300
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"operation": "executeQuery",
|
|
"query": "INSERT INTO ocsentinel.weekly_organization_report (organization_id, organization_name, period_start_utc, period_end_utc, device_count, warning_count, critical_count, total_events, unique_ips, cve_total, cve_critical, report_html, summary)\nVALUES ($1, $2, $3::timestamptz, $4::timestamptz, $5, $6, $7, $8, $9, $10, $11, $12, $13::jsonb)\nON CONFLICT (organization_id, period_start_utc) DO UPDATE SET organization_name=EXCLUDED.organization_name, period_end_utc=EXCLUDED.period_end_utc, generated_at=now(), device_count=EXCLUDED.device_count, warning_count=EXCLUDED.warning_count, critical_count=EXCLUDED.critical_count, total_events=EXCLUDED.total_events, unique_ips=EXCLUDED.unique_ips, cve_total=EXCLUDED.cve_total, cve_critical=EXCLUDED.cve_critical, report_html=EXCLUDED.report_html, summary=EXCLUDED.summary\nRETURNING id;",
|
|
"options": {
|
|
"queryReplacement": "={{ [$json.organizationId, $json.organizationName, $json.periodStartUtc, $json.periodEndUtc, $json.deviceCount, $json.warningCount, $json.criticalCount, $json.totalEvents, $json.uniqueIps, $json.cveTotal, $json.cveCritical, $json.reportHtml, $json.summaryJson] }}"
|
|
}
|
|
},
|
|
"id": "store-weekly-reports",
|
|
"name": "Store Weekly Organization Reports",
|
|
"type": "n8n-nodes-base.postgres",
|
|
"typeVersion": 2.5,
|
|
"position": [
|
|
1080,
|
|
300
|
|
],
|
|
"credentials": {
|
|
"postgres": {
|
|
"id": "WkjY0kIF3kHvREys",
|
|
"name": "OCSentinel PostgreSQL"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"operation": "executeQuery",
|
|
"query": "SELECT coalesce(array_agg(recipient_email ORDER BY recipient_email), ARRAY[]::text[]) AS recipients\nFROM ocsentinel.organization_report_recipient\nWHERE enabled = TRUE AND (organization_id = '*' OR organization_id = $1);",
|
|
"options": {
|
|
"queryReplacement": "={{ [$json.organizationId] }}"
|
|
}
|
|
},
|
|
"id": "load-report-recipients",
|
|
"name": "Empfaenger aus zentraler Zuordnung laden",
|
|
"type": "n8n-nodes-base.postgres",
|
|
"typeVersion": 2.5,
|
|
"position": [
|
|
1560,
|
|
300
|
|
],
|
|
"credentials": {
|
|
"postgres": {
|
|
"id": "WkjY0kIF3kHvREys",
|
|
"name": "OCSentinel PostgreSQL"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"mode": "runOnceForEachItem",
|
|
"jsCode": "const report = $('Build Organization HTML Reports').item.json;\nconst recipients = Array.from(new Set($json.recipients || []));\nif (recipients.length === 0) return [];\nreturn { json: { ...report, recipients } };"
|
|
},
|
|
"id": "prepare-report-email",
|
|
"name": "E-Mail vorbereiten",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
1800,
|
|
300
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"fromEmail": "donotreply@officecom.biz",
|
|
"toEmail": "={{ $json.recipients.join(', ') }}",
|
|
"subject": "=OCSentinel Wochenbericht - {{ $json.organizationName }}",
|
|
"html": "={{ $json.reportHtml }}",
|
|
"options": {
|
|
"appendAttribution": false
|
|
}
|
|
},
|
|
"id": "send-weekly-report-email",
|
|
"name": "Send Weekly Organization Report",
|
|
"type": "n8n-nodes-base.emailSend",
|
|
"typeVersion": 2.1,
|
|
"position": [
|
|
2040,
|
|
300
|
|
],
|
|
"credentials": {
|
|
"smtp": {
|
|
"id": "vafGYgYzM9SbxQbW",
|
|
"name": "SMTP account"
|
|
}
|
|
}
|
|
}
|
|
],
|
|
"connections": {
|
|
"Every Monday 07:20": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Load Latest Device Reports",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Load Latest Device Reports": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Build Organization HTML Reports",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Build Organization HTML Reports": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Store Weekly Organization Reports",
|
|
"type": "main",
|
|
"index": 0
|
|
},
|
|
{
|
|
"node": "Empfaenger aus zentraler Zuordnung laden",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Empfaenger aus zentraler Zuordnung laden": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "E-Mail vorbereiten",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"E-Mail vorbereiten": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Send Weekly Organization Report",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
}
|
|
},
|
|
"settings": {
|
|
"executionOrder": "v1",
|
|
"timezone": "Europe/Berlin"
|
|
},
|
|
"active": true,
|
|
"pinData": {},
|
|
"versionId": "af98f45b-192e-49c8-9a1e-e7b1fc7e00b2",
|
|
"meta": {
|
|
"templateCredsSetupCompleted": true
|
|
},
|
|
"tags": []
|
|
}
|