diff --git a/infra/n8n/weekly-organization-report-code-compact.js b/infra/n8n/weekly-organization-report-code-compact.js
new file mode 100644
index 0000000..12d1835
--- /dev/null
+++ b/infra/n8n/weekly-organization-report-code-compact.js
@@ -0,0 +1,40 @@
+const esc = (value) => String(value ?? '')
+ .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(`
| ${esc(device.name)} | Keine sicherheitsrelevanten Ereignisse im Berichtszeitraum. | Sauber |
`); 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(`| ${esc(device.name)} | ${esc(event.type)} | ${esc(event.account)} | ${esc(fmt(event.latest))} | ${event.count} | ${esc(event.ip)} | ${device.state === 'critical' ? 'Kritisch' : 'Pruefen'} |
`);
+ }
+ const risk = criticals ? ['Kritisch', 'critical'] : warnings ? ['Beobachten', 'warning'] : ['Unauffaellig', 'ok'];
+ const html = `${risk[0]}OfficeCom Sentinel Sicherheitsbericht
${esc(group.name)} | ${esc(start.toLocaleDateString('de-DE'))} bis ${esc(end.toLocaleDateString('de-DE'))}
| ${group.devices.length}Geräte | ${criticals}Kritisch | ${warnings}Warnungen | ${totalEvents}Ereignisse | ${ips.size}Quell-IP-Adressen | ${cveCritical}Kritische CVEs |
| System | Vorfall | Konto | Letzter Zeitpunkt | Anzahl | Quell-IP | Bewertung |
${rows.join('')}
`;
+ 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;
diff --git a/infra/n8n/weekly-organization-report-code.js b/infra/n8n/weekly-organization-report-code.js
new file mode 100644
index 0000000..6604849
--- /dev/null
+++ b/infra/n8n/weekly-organization-report-code.js
@@ -0,0 +1,129 @@
+const esc = (value) => String(value ?? '')
+ .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(`| ${esc(device.machineName)} | Keine sicherheitsrelevanten Ereignisse im Berichtszeitraum. | Sauber |
`);
+ 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(`| ${esc(device.machineName)} | ${esc(event.type)} | ${esc(event.account)} | ${esc(formatDate(event.latest))} | ${event.count} | ${esc(event.sourceIp)} | ${severity} |
`);
+ }
+ }
+
+ 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 = `
+
+
+
| ${summary.deviceCount}Geräte | ${summary.criticalCount}Kritisch | ${summary.warningCount}Warnungen | ${summary.totalEvents}Ereignisse | ${summary.uniqueIps}Quell-IP-Adressen | ${summary.cveCritical}Kritische CVEs |
+
| System | Vorfall | Konto | Letzter Zeitpunkt | Anzahl | Quell-IP | Bewertung |
${rows}
`;
+
+ reportItems.push({
+ json: {
+ organizationId: group.organizationId,
+ organizationName: group.organizationName,
+ periodStartUtc: periodStart.toISOString(),
+ periodEndUtc: periodEnd.toISOString(),
+ ...summary,
+ reportHtml,
+ summaryJson: JSON.stringify(summary)
+ }
+ });
+}
+
+return reportItems;