{ "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=v=>String(v??'').replace(/&/g,'&').replace(//g,'>').replace(/\\\"/g,'"').replace(/'/g,''');\nconst now=new Date(),end=new Date(Date.UTC(now.getUTCFullYear(),now.getUTCMonth(),now.getUTCDate()));\nend.setUTCDate(end.getUTCDate()-((end.getUTCDay()+6)%7)); const start=new Date(end-7*86400000);\nconst groups=new Map();\nfor(const item of items){const p=item.json.payload||{},n=p.NinjaOne||p.ninjaOne||{},id=String(n.OrganizationId||n.organizationId||'unknown'),name=String(n.OrganizationName||n.organizationName||`Organisation ${id}`);if(!groups.has(id))groups.set(id,{id,name,devices:[]});groups.get(id).devices.push({machine:item.json.machine_name||p.MachineName||'Unbekannt',state:String(item.json.alert_state||p.AlertState||'unknown').toLowerCase(),p});}\nconst result=[];\nfor(const group of groups.values()){let warnings=0,criticals=0,total=0,cveTotal=0,cveCritical=0;const ips=new Set(),alerts=[],clean=[];for(const d of group.devices){if(d.state==='warning')warnings++;if(d.state==='critical')criticals++;const vc=d.p.VulnerabilityCorrelation||{};cveTotal+=Number(vc.TotalCount||0);cveCritical+=Number(vc.CriticalCount||0);const events=(d.p.Events||[]).filter(e=>{const t=new Date(e.Timestamp);return !Number.isNaN(t)&&t>=start&&t${esc(d.machine)}Keine--0-Log sauber / Keine Angriffe`);continue;}const rows=new Map();for(const e of events){const ip=e.SourceIp||'-',account=e.Username||'-',type=e.Target||'Sicherheitsereignis',key=[type,account,ip].join('|'),row=rows.get(key)||{ip,account,type,count:0,last:e.Timestamp};row.count++;if(new Date(e.Timestamp)>new Date(row.last))row.last=e.Timestamp;rows.set(key,row);if(ip!=='-')ips.add(ip);}for(const r of rows.values()){alerts.push(`${esc(d.machine)}${esc(r.type)}${esc(r.account)}${esc(new Date(r.last).toLocaleString('de-DE',{timeZone:'Europe/Berlin'}))}${r.count}${esc(r.ip)}${d.state==='critical'?'Problem entdeckt (kritisch)':'Problem entdeckt'}`);}}const summary={deviceCount:group.devices.length,warningCount:warnings,criticalCount:criticals,totalEvents:total,uniqueIps:ips.size,cveTotal,cveCritical};const html=`

OCSentinel - Konsolidierter Sicherheitsbericht

${esc(group.name)}
Berichtszeitraum: ${start.toLocaleDateString('de-DE')} bis ${end.toLocaleDateString('de-DE')}
Erstellt am: ${now.toLocaleString('de-DE',{timeZone:'Europe/Berlin'})}
${summary.deviceCount} Geräte | ${summary.criticalCount} kritisch | ${summary.warningCount} Warnungen | ${summary.totalEvents} Ereignisse | ${summary.uniqueIps} IPs | ${summary.cveCritical} kritische CVEs
${alerts.join('')}${clean.join('')}
ServerVorfall-TypBetroffenes KontoLetzter ZeitpunktAnzahlAngreifer-IPStatus / Bemerkung
Automatisch durch OCSentinel und n8n erzeugt.
`;result.push({json:{organizationId:group.id,organizationName:group.name,periodStartUtc:start.toISOString(),periodEndUtc:end.toISOString(),...summary,reportHtml:html,summaryJson:JSON.stringify(summary)}});}return result;" }, "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": [] }