From be32f1607bf83acc0fe0f69eb999bc7ebea7de10 Mon Sep 17 00:00:00 2001 From: Binh Date: Fri, 2 Oct 2026 09:22:48 +0200 Subject: [PATCH] feat(exports): add GPO comparison to privacy-safe JSON export Actionable policy-evaluation findings carry a policy block built from the Result DB disclosure projection, so the customer value stays withheld. Raw policy observations are counted separately instead of as findings. --- docs/IMPLEMENTATION_PLAN.md | 1 + src/exports/result-db-copy.ts | 33 ++++++++++++++++++++++++++-- tests/exports/result-db-copy.test.ts | 30 +++++++++++++++++++++++++ 3 files changed, 62 insertions(+), 2 deletions(-) diff --git a/docs/IMPLEMENTATION_PLAN.md b/docs/IMPLEMENTATION_PLAN.md index cd8f882..f93dbcd 100644 --- a/docs/IMPLEMENTATION_PLAN.md +++ b/docs/IMPLEMENTATION_PLAN.md @@ -3554,6 +3554,7 @@ Verifiziert 2026-09-09 (Phase 7): Bereichswechsel beschleunigt und Performance-V Verifiziert 2026-09-08 (Phase 7/13B): GPO-Quellenkorrektur fuer Work und Client Info. Aktuelle Evaluationen aus dem vorhandenen autoritativen Readmodell statt ergebnisabhaengiger Backupgruppierung; Import- und Auswertungszeit getrennt, Source-Scan-Provenienz auch auf GPO-only-Blaettern. 1.014 Tests bestanden, acht optionale PostgreSQL-Tests uebersprungen; Local-only-/Designpruefung und beide Builds erfolgreich. Dummy lesend bestaetigt: eine statt drei GPO-Quellenoptionen, 3.805 aktuelle Zeilen, elf historische Evaluationen unveraendert. Keine Persistenzmutation oder Neuanalyse. | Datum | Änderung | +| 2026-10-02 | Phase 7: Privacy-Safe-`Export JSON` um GPO-Vergleiche ergaenzt. Handlungsrelevante `policy-evaluation`-Findings tragen einen `policy`-Block (Status, Baseline-Familie, Sollwert, Vergleich, Kundenzustand, Scope, Delivery, Diagnosecodes) ausschliesslich aus der bestehenden Result-DB-Disclosure-Projektion; der Kundenwert bleibt zurueckgehalten. Rohe `policy-observation`-Zeilen zaehlen nicht mehr als Findings, sondern separat als `policyObservationResults`; `policyStatuses` zaehlt die GPO-Urteile. Synthetischer Test in `tests/exports/result-db-copy.test.ts`. | | 2026-10-02 | Phase 7: AI-Integration auf den aktuellen Michael-Stand uebernommen (Branch `merge/michael-binh`). Der Privacy-Safe-`Export JSON` aus `result-db-copy` ergaenzt die Result DB als vierte Exportaktion; `Export JSONL`, `Export Excel` und `Export CSV` bleiben unveraendert. Die Einzelaktions-Festlegung vom 2026-09-14 gilt auf diesem Stand deshalb nicht als Ersatz des JSONL-Exports. Die Beispieldateien `test.json`, `test-output.json` und `artifacts/test-output-*` wurden nicht uebernommen. | | 2026-09-26 | Phase 7: MITRE-Onlineaktualisierung aus offizieller Enterprise-STIX-Quelle, separater Download der originalen MITRE-Datei und weiterhin manueller JSON-Import. Gemeinsam gespeicherter privater Katalog, sofortige Graphaktualisierung, Same-Origin-API; keine Assessment-/DB-Migration. Minorversion 2.7.0; synthetische Service-/HTTP-/UI-Tests, Live-Update und Originaldownload, Design-/Local-only-Pruefung sowie beide Builds verifiziert. Sechs vorbestehende fachfremde Tests bleiben offen. | | 2026-09-26 | Phase 7: MITRE-Standardkatalog v19.2 aus offiziellem STIX, sichtbarer STIX-/JSON-Upload im Data Hub und sofortige Pathfinder-Aktualisierung bei Upload/Reset. Reproduzierbare Quelle und Browser-Override-Grenze in `docs/MITRE_CATALOG.md`; Patchversion 2.6.47. 21 fokussierte Tests, Data-Hub-Layouttest, Design-/Local-only-Pruefung und beide Builds bestanden; Gesamtsuite mit sechs fachfremden bestehenden Fehlern. | diff --git a/src/exports/result-db-copy.ts b/src/exports/result-db-copy.ts index ba1490e..f6bc840 100644 --- a/src/exports/result-db-copy.ts +++ b/src/exports/result-db-copy.ts @@ -1,4 +1,6 @@ import type { UnifiedResultProjection } from '../domain/unified-result'; +import { POLICY_CLASSIFICATION_LABELS } from '../domain/policy-status-labels'; +import { discloseResultDbProjection } from '../domain/result-db-disclosure'; const SAFE_ENVIRONMENT_PROPERTIES = new Set([ 'domain.controller_count', @@ -16,7 +18,10 @@ export function privacySafeResultJsonFileName(generatedAt: string): string { } export function resultsToPrivacySafeExportJson(items: UnifiedResultProjection[]): string { - const findings = items.filter(item => item.sourceKind !== 'environment-observation'); + // A raw GPO observation is context for its rolled-up comparison, not a finding of its own. + const findings = items.filter(item => item.sourceKind !== 'environment-observation' && item.sourceKind !== 'policy-observation'); + const policyObservations = items.filter(item => item.sourceKind === 'policy-observation'); + const policyFindings = findings.filter(item => item.sourceKind === 'policy-evaluation'); const environment = items.filter(item => item.sourceKind === 'environment-observation'); const actionable = findings.filter(item => item.isDeviation || item.isIoe || ['fail', 'warning', 'execution_error'].includes(item.outcomeNormalized)); @@ -28,11 +33,13 @@ export function resultsToPrivacySafeExportJson(items: UnifiedResultProjection[]) totalResults: items.length, findingResults: findings.length, environmentResults: environment.length, + policyObservationResults: policyObservations.length, actionableFindings: actionable.length, notRunChecks: notRun.length, findingOutcomes: countBy(findings, item => item.outcomeNormalized), findingSeverities: countBy(findings, item => item.severityNormalized), findingTools: countBy(findings, item => item.sourceTool ?? 'unknown'), + policyStatuses: countBy(policyFindings, policyStatus), environmentStates: countBy(environment, item => item.environmentState ?? 'unknown') }, findings: actionable.map(item => { @@ -48,7 +55,8 @@ export function resultsToPrivacySafeExportJson(items: UnifiedResultProjection[]) resultType: item.resultType, isIoe: item.isIoe, deviation: item.isDeviation, - tool: item.sourceTool ?? 'unknown' + tool: item.sourceTool ?? 'unknown', + ...privacySafePolicy(item) }; }), notRunChecks: notRun.map(item => ({ @@ -62,6 +70,27 @@ export function resultsToPrivacySafeExportJson(items: UnifiedResultProjection[]) }, null, 2); } +function policyStatus(item: UnifiedResultProjection): string { + return item.policyClassification ? POLICY_CLASSIFICATION_LABELS[item.policyClassification] : 'unknown'; +} + +/** GPO comparison facts under the Result DB disclosure policy: the customer value stays withheld. */ +function privacySafePolicy(item: UnifiedResultProjection): { policy?: Record } { + if (item.sourceKind !== 'policy-evaluation') return {}; + const disclosed = discloseResultDbProjection(item); + const policy = { + status: policyStatus(disclosed), + baseline: disclosed.policyBaselineLabel, + expectedValue: disclosed.policyBaselineValue, + comparison: disclosed.policyComparisonRelation, + customerState: disclosed.policyCustomerState, + scope: disclosed.policyScope, + delivery: disclosed.policyDeliveryMechanism, + diagnostics: disclosed.policyDiagnosticCodes?.length ? disclosed.policyDiagnosticCodes : undefined + }; + return { policy: Object.fromEntries(Object.entries(policy).filter(([, value]) => value !== undefined)) }; +} + function groupedEnvironmentFacts(items: UnifiedResultProjection[]) { const grouped = new Map { expect(copied.findings[0].id).toBeNull(); }); + it('adds the GPO comparison to a policy finding without the customer value', () => { + const copied = JSON.parse(resultsToPrivacySafeExportJson([ + result({ + sourceKind: 'policy-evaluation', sourceTool: 'GPOs', title: 'Synthetic policy mode', + sourceRuleId: 'registry:machine:software\\policies\\synthetic:mode', + outcomeNormalized: 'fail', isDeviation: true, policyClassification: 'misconfigured', + policyBaselineLabel: 'DUMMY Microsoft Security Baseline Windows Server 2022 v1', + policyBaselineValue: { operation: 'set-value', data: { kind: 'dword', value: 1 } }, + policyActualValue: { operation: 'set-value', data: { kind: 'dword', value: 0 }, principals: ['DUMMY\\named.user'] }, + policyComparisonRelation: 'different', policyCustomerState: 'configured', + policyScope: 'computer', policyDeliveryMechanism: 'registry-policy', + policyGpoId: '{A1B2C3D4-1111-2222-3333-444455556666}', policyIdentityKey: 'private-policy-hash' + }), + result({ sourceKind: 'policy-evaluation', sourceTool: 'GPOs', title: 'Passed policy', policyClassification: 'match' }), + result({ sourceKind: 'policy-observation', sourceTool: 'GPOs', title: 'Raw observation', policyValues: ['PRIVATE'] }) + ])); + + expect(copied.summary).toMatchObject({ + totalResults: 3, findingResults: 2, policyObservationResults: 1, actionableFindings: 1, + policyStatuses: { Deviation: 1, Passed: 1 } + }); + expect(copied.findings).toHaveLength(1); + expect(copied.findings[0].policy).toEqual({ + status: 'Deviation', baseline: 'Microsoft Security Baseline - Windows Server', + expectedValue: { operation: 'set-value', data: { kind: 'dword', value: 1 } }, + comparison: 'different', customerState: 'configured', scope: 'computer', delivery: 'registry-policy' + }); + expect(JSON.stringify(copied)).not.toMatch(/named\.user|A1B2C3D4|private-policy-hash|PRIVATE|"value":0/); + }); + it('groups approved environment facts and excludes identities and arbitrary values', () => { const os = result({ sourceKind: 'environment-observation', title: 'Operating System', environmentState: 'observed',