import type { ReportDomainPriority, ReportDomainSummary, ReportFindingRef, ReportGpoBlock, ReportPriorityBasis, ReportProjection, ReportScoreSource } from '../domain/report-projection'; import { escapeHtml } from '../shared/html'; import { DASH, SEVERITY_LABEL, SEVERITY_ORDER, barList, filterToolbar, integer, reportDocument, scoreDonut, severityText, toolLabel, type SeverityKey } from './report-theme'; /** * Artifact A of docs/REPORT_STRATEGY.md: the Management Overview. * * A frozen snapshot for the customer's decision makers. It answers how the * environment stands, what is most urgent, what the statement rests on and what * is not known — and deliberately carries no registry keys, SIDs, evidence paths * or parser versions, so the file can be forwarded as it is. * * German carries the prose; established English technical terminology stays * English. The file is self-contained: styles, font and the small filter script * are inline, nothing is fetched when it is opened. */ export const MANAGEMENT_OVERVIEW_RENDERER_VERSION = '2.1.0'; /** Unknown stays unknown. An absent value never becomes a zero. */ function value(input: string | number | undefined | null): string { if (input === undefined || input === null) return DASH; if (typeof input === 'number' && !Number.isFinite(input)) return DASH; const text = String(input).trim(); return text === '' ? DASH : escapeHtml(text); } function percent(ratio: number | undefined, digits = 0): string { return typeof ratio === 'number' && Number.isFinite(ratio) ? escapeHtml((ratio * 100).toLocaleString('de-DE', { minimumFractionDigits: digits, maximumFractionDigits: digits })) : DASH; } function germanDateTime(iso: string): string { const parsed = Date.parse(iso); if (!Number.isFinite(parsed)) return value(iso); return escapeHtml(new Date(parsed).toLocaleString('de-DE', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' })); } function germanDate(iso: string | undefined): string { if (!iso) return DASH; const parsed = Date.parse(iso); if (!Number.isFinite(parsed)) return value(iso); return escapeHtml(new Date(parsed).toLocaleDateString('de-DE', { year: 'numeric', month: '2-digit', day: '2-digit' })); } /** * Binding wording from docs/SCORING_MODEL.md. A method is named, never implied, * and the GPO contribution is never called Baseline-Compliance. */ const SCORE_METHOD_LABEL: Record = { native: 'Nativer Score des Werkzeugs', 'inverted-native': 'Nativer Risk, invertiert als 100 − Risk', 'pass-rate': 'Pass Rate der bewerteten Checks', unavailable: 'Kein verwertbarer Score' }; /** Names why a source is ordered the way it is, or why it is not ordered. */ function basisLabel(basis: ReportPriorityBasis): string { if (basis.kind === 'severity') return 'Sortiert nach nativer Severity'; if (basis.kind === 'numeric') { const direction = basis.direction === 'higher-is-worse' ? 'höher ist kritischer' : 'niedriger ist kritischer'; return `Sortiert nach ${escapeHtml(basis.property)} (${escapeHtml(basis.scale)}, ${direction})`; } if (basis.kind === 'equal-weight') return `Ohne Rangfolge: ${escapeHtml(basis.reason)}`; return 'Ohne Rangfolge: Die Quelle deklariert keinen belastbaren Maßstab.'; } function severityCounts(findings: ReportFindingRef[]): Record { const counts = Object.fromEntries(SEVERITY_ORDER.map(key => [key, 0])) as Record; for (const finding of findings) counts[finding.severity] += 1; return counts; } /** Domain pass rates as a connected spine, not a conventional table. */ function domainSpine(domains: ReportDomainSummary[]): string { if (!domains.length) return '

Keine Security Domain wurde bewertet.

'; return `
    ${domains.map(domain => { const known = typeof domain.passRate === 'number'; const width = known ? Math.round(domain.passRate! * 100) : 0; return `
  1. ${escapeHtml(domain.domainLabel)} ${known ? `${percent(domain.passRate)} %` : DASH} ${domain.assessed ? `${integer(domain.deviations)} Deviations von ${integer(domain.assessed)} bewertet` : 'nicht bewertet'}
  2. `; }).join('')}
`; } function findingRow(finding: ReportFindingRef, ranked: boolean, member: string): string { const severity = finding.severity; return `
  • ${escapeHtml(finding.title)} ${ranked && finding.rankValue !== undefined && finding.rankBasis.kind === 'numeric' ? `${escapeHtml(finding.rankBasis.property)} ${integer(finding.rankValue)}` : ''} ${severityText(severity)}
  • `; } function prioritySection(priorities: ReportDomainPriority[]): string { if (!priorities.length) return '

    Es wurde keine Deviation festgestellt.

    '; return priorities.map((domain, domainIndex) => { const member = `domain-${domainIndex}`; return `
    ${escapeHtml(domain.domainLabel)} ${integer(domain.deviations)} Deviations ${domain.sources.map(source => `
    ${escapeHtml(toolLabel(source.toolId))} ${basisLabel(source.basis)}
      ${source.shown.map(item => findingRow(item, source.ranked, member)).join('')}
    ${source.shown.length < source.total ? `

    ${integer(source.total - source.shown.length)} weitere Deviations dieser Quelle stehen im Technical Report.

    ` : ''}
    `).join('')}
    `; }).join(''); } /** * GPO deviations carry no native criticality, so they form their own * equal-weight block instead of being ranked against severity-bearing sources. */ function gpoSection(gpo: ReportGpoBlock): string { const rows: Array<[string, number]> = [ ['Deviation', gpo.counts.Fail], ['Review required', gpo.counts['Review required']], ['Passed', gpo.counts.Pass], ['Not configured', gpo.counts['Not configured']], ['No baseline rule', gpo.counts['No baseline rule']], ['Not comparable', gpo.counts['Not comparable']] ]; return `

    GPO & Baseline

    ${integer(gpo.counts.Fail)} Deviations
    ${barList(rows.map(([label, count]) => ({ label, value: count })), 'Keine GPO Policy Settings bewertet.')}

    ${escapeHtml(gpo.basis.reason)} Die Einträge stehen deshalb gleichgewichtig nebeneinander.

    Betroffene GPO-Objekte

    Policy Settings mit Deviation${integer(gpo.affectedSettings)}
    betroffene GPO Backups${integer(gpo.affectedBackups)}
    Conflicts${integer(gpo.conflicts)}
    Duplicates${integer(gpo.duplicates)}

    Not configured, No baseline rule und Not comparable sind score-neutral und begründen keine Maßnahme. ${gpo.scopeLimited ? 'Die Auswertung bezieht sich ausschließlich auf den importierten GPO-Backup-Satz und trifft keine Aussage über die gesamte Domäne.' : ''}

    `; } function kpiTile(label: string, figure: string, caption: string): string { return `
    ${escapeHtml(label)} ${figure} ${caption}
    `; } const MANAGEMENT_CSS = ` .spine{list-style:none;margin:0;padding:0 0 0 16px;border-left:1px solid var(--border-default)} .spine__item{position:relative;display:grid;grid-template-columns:minmax(150px,1fr) minmax(80px,160px) 56px; gap:4px 14px;align-items:center;padding:8px 0} .spine__node{position:absolute;left:-20px;top:17px;width:7px;height:7px;border-radius:50%;background:var(--donut-all)} .spine__label{font-size:16px;font-weight:600} .spine__meter{height:10px;background:var(--bar-track)} .spine__fill{display:block;height:100%;background:var(--donut-all)} .spine__figure{font-size:16px;font-weight:600;font-variant-numeric:tabular-nums;text-align:right} .spine__counts{grid-column:1 / -1;font-size:14px;color:var(--text-secondary)} .kpis{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:20px} .kpis .kpi__figure{font-size:32px} .domain__count{margin-left:auto} .domain__count{font-size:14px;font-weight:600;color:var(--text-secondary);font-variant-numeric:tabular-nums} .source-block{padding:12px 20px 16px} .source-block[hidden]{display:none} .source-block__head{display:flex;flex-wrap:wrap;align-items:baseline;gap:4px 12px} .source-block__tool{font-size:16px;font-weight:600} .source-block__basis{font-size:14px;color:var(--text-secondary)} .source-block__more{margin-top:8px;font-size:14px;color:var(--text-muted)} .findings{list-style:none;margin:8px 0 0;padding:0} .finding{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:16px;align-items:center; padding:10px 0;border-bottom:1px solid var(--border-subtle)} .finding:last-child{border-bottom:0} .finding[hidden]{display:none} .finding__title{font-size:16px;font-weight:400;overflow-wrap:anywhere} .finding__meta{display:flex;flex-wrap:wrap;justify-content:flex-end;align-items:center;gap:10px} .gpo{margin-top:16px} @media (max-width:720px){ .spine__item{grid-template-columns:1fr 60px} .spine__meter{grid-column:1 / -1} .finding{grid-template-columns:1fr} .finding__meta{justify-content:flex-start} }`; export function buildManagementOverviewHtml(projection: ReportProjection): string { const facts = projection.environment.length ? projection.environment : [{ key: 'Umgebung', value: undefined }]; const remediation = projection.remediation; const coverage = projection.coverage; const isGpo = (toolId: string | undefined) => toolLabel(toolId) === 'Group Policy'; const scanFindings = projection.findings.filter(finding => !isGpo(finding.toolId)); const gpoSettings = projection.gpo?.counts.Fail ?? 0; const severities = severityCounts(scanFindings); const criticalHigh = severities.critical + severities.high; const deviationLine = [ `${integer(scanFindings.length)} aus Scans`, projection.gpo ? `${integer(gpoSettings)} GPO Policy Settings` : '' ].filter(Boolean).join(' · '); const dated = projection.scoreSources.filter(source => germanDate(source.observedAt) !== DASH); const sourceLine = dated.length ? dated.map(source => `${escapeHtml(toolLabel(source.toolId))}: ${germanDate(source.observedAt)}`).join(' • ') : projection.scoreSources.map(source => escapeHtml(toolLabel(source.toolId))).join(' • '); const head = `

    ASSESSOR · Management Overview

    ${escapeHtml(projection.project.name)}

    ${sourceLine ? `

    ${sourceLine}

    ` : ''}
    ${facts.map(fact => `
    ${escapeHtml(fact.key)}
    ${value(fact.value)}
    `).join('')}
    Assessment
    ${value(projection.assessmentId)}
    Berichtsdatum
    ${germanDateTime(projection.generatedAt)}
    `; const posture = `

    Overall Posture

    ${kpiTile('Deviations', integer(scanFindings.length + gpoSettings), deviationLine)} ${kpiTile('Critical / High', integer(criticalHigh), 'Scan-Deviations mit hoher nativer Kritikalität')}
    Overall Score ${scoreDonut(projection.overallScore, 'Overall Score')}

    Security Domains

    Pass Rate je Domain
    ${domainSpine(projection.domains)}

    Severity-Verteilung

    ${integer(scanFindings.length)} Scan-Deviations
    ${barList(SEVERITY_ORDER.filter(key => key !== 'unknown' || severities.unknown > 0) .map(key => ({ label: SEVERITY_LABEL[key], value: severities[key], tone: key })), 'Keine Deviations.')}

    Deviations je Quelle

    ${barList(projection.scoreSources.map(source => ({ label: toolLabel(source.toolId), value: source.deviations })), 'Keine Quelle bewertet.')}

    Der Overall Score ist ein Mixed-Method-Index über die unten genannten Quellen. Der GPO-Anteil bezeichnet die Konformität der konfigurierten, vergleichbaren Einstellungen im Scope — er ist keine Aussage über eine vollständig ausgerollte Microsoft Baseline. Security-Domain-Werte sind Pass Rates und keine Zerlegung des Index. Quellen ohne native Severity erscheinen als „ohne Severity“.

    `; // With a GPO block, GPO deviations are shown there once per policy setting, not per baseline comparison. const priorityDomains = projection.gpo ? projection.priorities.map(domain => { const sources = domain.sources.filter(source => !isGpo(source.toolId)); return { ...domain, sources, deviations: sources.reduce((sum, source) => sum + source.total, 0) }; }).filter(domain => domain.sources.length) : projection.priorities; const shownFindings = priorityDomains.reduce((sum, domain) => sum + domain.sources.reduce((inner, source) => inner + source.shown.length, 0), 0); const priorities = `

    Priority Findings

    ↑ Nach oben

    Gruppiert nach Security Domain. Innerhalb einer Domain wird je Quelle nach deren eigenem, deklariertem Maßstab geordnet; eine quellenübergreifende Rangfolge gibt es nicht, weil die Maßstäbe nicht vergleichbar sind.

    ${shownFindings ? filterToolbar({ placeholder: 'Findings durchsuchen', reset: 'Alle', groups: [{ key: 'severity', label: 'Severity', values: (['critical', 'high'] as SeverityKey[]).filter(key => severities[key] > 0) .map(key => ({ value: key, label: SEVERITY_LABEL[key] })) }] }) : ''}
    Findings ›Nach Security Domain ${integer(shownFindings)}
    ${prioritySection(priorityDomains)}
    ${projection.gpo ? '

    GPO-Deviations tragen keine native Kritikalität und stehen gleichgewichtig im Block GPO & Baseline.

    ' : ''} ${projection.gpo ? gpoSection(projection.gpo) : ''}
    `; const convergence = `

    Konvergenz der Quellen

    Mehrere Quellen in einer Security Domain belegen nicht automatisch denselben Finding.

    ${projection.convergence.filter(item => item.sourceCount > 0).map(item => `

    ${value(projection.domains.find(domain => domain.domainId === item.domainId)?.domainLabel ?? item.domainId)}

    ${integer(item.sourceCount)} Quellen

    ${integer(item.sourceCount)} unabhängige Quellen mit Deviations: ${item.sources.filter(source => source.deviations > 0) .map(source => escapeHtml(toolLabel(source.toolId))).join(', ')}.

    ${item.evidence === 'shared-framework-reference' ? 'Belegte Konvergenz: Mindestens zwei Quellen nennen dieselbe Framework-Referenz.' : 'Nur thematische Nähe innerhalb der Domain; ein gemeinsamer Finding ist nicht belegt und bedarf Validierung.'}

    `).join('') || '

    Keine quellenübergreifende Konvergenz belegt.

    '}
    `; const bundles = `

    Maßnahmenbündel

    Gleiche Framework-Referenzen gruppieren Findings ohne Aussage zu Aufwand, Nutzen oder Priorität.

    ${projection.bundles.length ? `
    ${projection.bundles.map(bundle => ``).join('')}
    Framework-ReferenzFindings
    ${escapeHtml(bundle.key)}${integer(bundle.members.length)}
    ` : '

    Keine mehrfach belegte Framework-Referenz.

    '}

    Einzelquellen-GPO-Maßnahmen ohne Conflict oder Duplicate: ${integer(projection.singleSourcePolicyActions.length)}.

    `; const remediationSection = remediation ? `

    Remediation-Status

    Selbstauskunft des Kunden aus der Arbeitsgrundlage. Diese Angaben sind kein technischer Nachweis: Eine Maßnahme gilt erst dann als behoben, wenn ein Rescan das belegt.

    ${kpiTile('Maßnahmen in Arbeit', integer(remediation.items), 'Selbstauskunft')} ${kpiTile('ohne Owner', integer(remediation.withoutOwner), 'Maßnahmen')} ${kpiTile('ohne Termin', integer(remediation.withoutDue), 'Maßnahmen')} ${kpiTile('Accepted Risk', integer(remediation.riskAccepted), 'Maßnahmen')}
    ${Object.keys(remediation.byStatus).length ? `

    Status

    ${barList(Object.entries(remediation.byStatus).map(([status, count]) => ({ label: status, value: count })), '')}
    ` : ''}
    ` : ''; const coverageSection = `

    Abdeckung und Lücken

    ${kpiTile('bewertet', integer(coverage.assessed), `von ${integer(coverage.total)} Ergebnissen`)} ${kpiTile('Deviations', integer(coverage.deviations), 'bewertete Abweichungen')} ${kpiTile('not run', integer(coverage.notRun), 'nicht ausgeführt')} ${kpiTile('not applicable', integer(coverage.notApplicable), 'nicht anwendbar')} ${kpiTile('execution errors', integer(coverage.executionErrors), 'fehlerhaft ausgeführt')}

    Von insgesamt ${integer(coverage.total)} Ergebnissen konnten ${integer(coverage.assessed)} bewertet werden. Nicht ausgeführte, nicht anwendbare und fehlerhaft ausgeführte Checks zählen weder als bestanden noch als Deviation.

    Abdeckung je Quelle

    ${coverage.perSource.map(source => ``).join('') || ``}
    QuelleErgebnissebewertetnot runnot run Anteil
    ${escapeHtml(toolLabel(source.toolId))} ${integer(source.total)} ${integer(source.assessed)} ${integer(source.notRun)} ${source.total ? `${percent(source.notRun / source.total)} %` : DASH}
    ${DASH}
    `; const sources = `

    Source Overview

    Score-Beitrag je Quelle

    ${projection.scoreSources.length ? projection.scoreSources.map(source => ``).join('') : ``}
    QuelleStandBeitrag MethodePassDeviations
    ${escapeHtml(toolLabel(source.toolId))} ${germanDate(source.observedAt)} ${typeof source.score === 'number' ? integer(Math.round(source.score)) : DASH} ${escapeHtml(SCORE_METHOD_LABEL[source.method])} ${integer(source.passed)} ${integer(source.deviations)}
    ${DASH}
    ${projection.scoreSources.some(source => source.method === 'inverted-native') ? '

    Eine invertierte Quelle liefert nativ einen Risk-Wert, bei dem ein höherer Wert schlechter ist. Er geht als 100 − Risk in den Index ein; der rohe Risk-Wert bleibt in der Quelle erhalten.

    ' : ''}
    `; return reportDocument( `Management Overview · ${projection.project.name}`, `${head} ${posture} ${priorities} ${remediationSection} ${coverageSection} ${convergence} ${bundles} ${sources}`, MANAGEMENT_CSS ); }