import type { ReportDomainPriority, ReportDomainSummary, ReportFindingRef, ReportGpoBlock, ReportPriorityBasis, ReportProjection, ReportScoreSource } from '../domain/report-projection'; import { escapeHtml } from '../shared/html'; /** * 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: every style is inline, nothing is * fetched when it is opened. */ export const MANAGEMENT_OVERVIEW_RENDERER_VERSION = '1.0.0'; const DASH = '—'; /** 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 integer(input: number | undefined): string { return typeof input === 'number' && Number.isFinite(input) ? escapeHtml(input.toLocaleString('de-DE')) : DASH; } 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.'; } const SEVERITY_LABEL: Record = { critical: 'Critical', high: 'High', medium: 'Medium', low: 'Low', info: 'Info', unknown: 'Unknown' }; /** * Shared donut geometry: full 360 degrees, continuous neutral track, value arc * clockwise from twelve o'clock, flat caps. The number sits on the exact centre * and its unit follows to the right without displacing it. */ function donut(score: number | undefined, caption: string): string { const radius = 54; const circumference = 2 * Math.PI * radius; const ratio = typeof score === 'number' && Number.isFinite(score) ? Math.min(Math.max(score, 0), 100) / 100 : 0; const dash = `${(circumference * ratio).toFixed(3)} ${(circumference * (1 - ratio)).toFixed(3)}`; const display = typeof score === 'number' && Number.isFinite(score) ? Math.round(score).toLocaleString('de-DE') : DASH; const hasScore = display !== DASH; return `
${hasScore ? `` : ''}
${escapeHtml(display)}${hasScore ? '/100' : ''} ${escapeHtml(caption)}
`; } /** 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} ${integer(domain.deviations)} Deviations von ${integer(domain.assessed)} bewertet
  2. `; }).join('')}
`; } function findingRow(finding: ReportFindingRef, ranked: boolean): string { const severity = finding.severity; const showSeverity = severity !== 'unknown'; return `
  • ${escapeHtml(finding.title)} ${showSeverity ? `${escapeHtml(SEVERITY_LABEL[severity])}` : ''} ${ranked && finding.rankValue !== undefined && finding.rankBasis.kind === 'numeric' ? `${escapeHtml(finding.rankBasis.property)} ${integer(finding.rankValue)}` : ''} ${finding.affectedAsset ? `${escapeHtml(finding.affectedAsset)}` : ''}
  • `; } function prioritySection(priorities: ReportDomainPriority[]): string { if (!priorities.length) return '

    Es wurde keine Deviation festgestellt.

    '; return priorities.map(domain => `

    ${escapeHtml(domain.domainLabel)}

    ${integer(domain.deviations)} Deviations
    ${domain.sources.map(source => `
    ${escapeHtml(source.toolId)} ${basisLabel(source.basis)}
      ${source.shown.map(item => findingRow(item, source.ranked)).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]> = [ ['Fail', gpo.counts.Fail], ['Review required', gpo.counts['Review required']], ['Pass', 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

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

    ${rows.map(([label, count]) => `
    ${escapeHtml(label)}
    ${integer(count)}
    `).join('')}

    Betroffene Policy Settings: ${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.' : ''}

    `; } 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 head = `

    ASSESSOR · Management Overview

    ${escapeHtml(projection.project.name)}

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

    Overall Posture

    ${donut(projection.overallScore, 'Overall Score')}

    Security Domains

    ${domainSpine(projection.domains)}

    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.

    `; const priorities = `

    Priority Findings

    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.

    ${prioritySection(projection.priorities)} ${projection.gpo ? gpoSection(projection.gpo) : ''}
    `; 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.

    ${integer(remediation.items)}Maßnahmen in Arbeit
    ${integer(remediation.withoutOwner)}ohne Owner
    ${integer(remediation.withoutDue)}ohne Termin
    ${integer(remediation.riskAccepted)}Accepted Risk
    ${Object.keys(remediation.byStatus).length ? `
    ${Object.entries(remediation.byStatus) .map(([status, count]) => `
    ${escapeHtml(status)}
    ${integer(count)}
    `) .join('')}
    ` : ''}
    ` : ''; const coverageSection = `

    Abdeckung und Lücken

    ${integer(coverage.assessed)}bewertet
    ${integer(coverage.deviations)}Deviations
    ${integer(coverage.notRun)}not run
    ${integer(coverage.notApplicable)}not applicable
    ${integer(coverage.executionErrors)}execution errors

    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.

    ${coverage.perSource.map(source => ``).join('')}
    Abdeckung je Quelle
    QuelleErgebnissebewertetnot run
    ${escapeHtml(source.toolId)} ${integer(source.total)} ${integer(source.assessed)} ${integer(source.notRun)}
    `; const sources = `

    Source Overview

    ${projection.scoreSources.length ? projection.scoreSources.map(source => ``).join('') : ``}
    Score-Beitrag je Quelle
    QuelleStandBeitrag MethodePassDeviations
    ${escapeHtml(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 ` Management Overview · ${escapeHtml(projection.project.name)} ${head} ${posture} ${priorities} ${remediationSection} ${coverageSection} ${sources} `; }