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.
This commit is contained in:
Binh 2026-10-02 09:22:48 +02:00
parent d9da5c39c0
commit af4f14845a
3 changed files with 62 additions and 2 deletions

View File

@ -4133,6 +4133,7 @@ Verifiziert 2026-09-08 (Phase 7/13B): GPO-Quellenkorrektur fuer Work und Client
| 2026-10-06 | Phase 7: Data Hub Deviation/Passed now use the same assessed-percentage helper as Client Info; all six donuts state their population basis. 29 focused tests and synthetic Chrome Dark/Light checks passed; absolute counts, other population shares and assessment semantics retained. Client build, design and UI/API version checks passed. Nine out-of-scope report test failures, a report Local-only failure and report server type errors prevent full release verification; attempted 2.16.9 reverted to 2.16.8 pending successful production builds. | | 2026-10-06 | Phase 7: Data Hub Deviation/Passed now use the same assessed-percentage helper as Client Info; all six donuts state their population basis. 29 focused tests and synthetic Chrome Dark/Light checks passed; absolute counts, other population shares and assessment semantics retained. Client build, design and UI/API version checks passed. Nine out-of-scope report test failures, a report Local-only failure and report server type errors prevent full release verification; attempted 2.16.9 reverted to 2.16.8 pending successful production builds. |
| 2026-10-06 | Phase 7: Client Info Deviations percentage now divides by Passed + Deviations and labels Of assessed checks; zero assessed shows a dash while preserving absolute zero. Permanent UI/click invariance tests in both themes and synthetic Chrome presentation verified; unchanged counts, verdicts, scores and other donuts. Patch 2.16.8: both builds and version contracts passed; full run 1,936 passed/15 skipped with one stale API-version assertion corrected and passed in the 43-test focused rerun; Local-only/design checks passed. | | 2026-10-06 | Phase 7: Client Info Deviations percentage now divides by Passed + Deviations and labels Of assessed checks; zero assessed shows a dash while preserving absolute zero. Permanent UI/click invariance tests in both themes and synthetic Chrome presentation verified; unchanged counts, verdicts, scores and other donuts. Patch 2.16.8: both builds and version contracts passed; full run 1,936 passed/15 skipped with one stale API-version assertion corrected and passed in the 43-test focused rerun; Local-only/design checks passed. |
| 2026-10-06 | Phase 13B: GPO folder read diagnostics retain only safe error names, stages and counts, distinguish empty/denied/unreadable selections, preserve cancellation and wrap complete timeslot messages. 27 focused GPO UI tests and synthetic Chrome Dark/Light checks passed; both builds and version contracts verified for patch 2.16.7. Full run: 1,932 passed, 15 skipped, one general import timeout passed on separate rerun; Local-only/design checks passed. Customer-specific read failure remains unconfirmed; no customer path/data used, Phase 13B remains in progress. | | 2026-10-06 | Phase 13B: GPO folder read diagnostics retain only safe error names, stages and counts, distinguish empty/denied/unreadable selections, preserve cancellation and wrap complete timeslot messages. 27 focused GPO UI tests and synthetic Chrome Dark/Light checks passed; both builds and version contracts verified for patch 2.16.7. Full run: 1,932 passed, 15 skipped, one general import timeout passed on separate rerun; Local-only/design checks passed. Customer-specific read failure remains unconfirmed; no customer path/data used, Phase 13B remains in progress. |
| 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-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-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. | | 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. |

View File

@ -1,4 +1,6 @@
import type { UnifiedResultProjection } from '../domain/unified-result'; 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([ const SAFE_ENVIRONMENT_PROPERTIES = new Set([
'domain.controller_count', 'domain.controller_count',
@ -16,7 +18,10 @@ export function privacySafeResultJsonFileName(generatedAt: string): string {
} }
export function resultsToPrivacySafeExportJson(items: UnifiedResultProjection[]): 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 environment = items.filter(item => item.sourceKind === 'environment-observation');
const actionable = findings.filter(item => const actionable = findings.filter(item =>
item.isDeviation || item.isIoe || ['fail', 'warning', 'execution_error'].includes(item.outcomeNormalized)); item.isDeviation || item.isIoe || ['fail', 'warning', 'execution_error'].includes(item.outcomeNormalized));
@ -28,11 +33,13 @@ export function resultsToPrivacySafeExportJson(items: UnifiedResultProjection[])
totalResults: items.length, totalResults: items.length,
findingResults: findings.length, findingResults: findings.length,
environmentResults: environment.length, environmentResults: environment.length,
policyObservationResults: policyObservations.length,
actionableFindings: actionable.length, actionableFindings: actionable.length,
notRunChecks: notRun.length, notRunChecks: notRun.length,
findingOutcomes: countBy(findings, item => item.outcomeNormalized), findingOutcomes: countBy(findings, item => item.outcomeNormalized),
findingSeverities: countBy(findings, item => item.severityNormalized), findingSeverities: countBy(findings, item => item.severityNormalized),
findingTools: countBy(findings, item => item.sourceTool ?? 'unknown'), findingTools: countBy(findings, item => item.sourceTool ?? 'unknown'),
policyStatuses: countBy(policyFindings, policyStatus),
environmentStates: countBy(environment, item => item.environmentState ?? 'unknown') environmentStates: countBy(environment, item => item.environmentState ?? 'unknown')
}, },
findings: actionable.map(item => { findings: actionable.map(item => {
@ -48,7 +55,8 @@ export function resultsToPrivacySafeExportJson(items: UnifiedResultProjection[])
resultType: item.resultType, resultType: item.resultType,
isIoe: item.isIoe, isIoe: item.isIoe,
deviation: item.isDeviation, deviation: item.isDeviation,
tool: item.sourceTool ?? 'unknown' tool: item.sourceTool ?? 'unknown',
...privacySafePolicy(item)
}; };
}), }),
notRunChecks: notRun.map(item => ({ notRunChecks: notRun.map(item => ({
@ -62,6 +70,27 @@ export function resultsToPrivacySafeExportJson(items: UnifiedResultProjection[])
}, null, 2); }, 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<string, unknown> } {
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[]) { function groupedEnvironmentFacts(items: UnifiedResultProjection[]) {
const grouped = new Map<string, { const grouped = new Map<string, {
title: string; title: string;

View File

@ -58,6 +58,36 @@ describe('Result DB compact privacy-safe JSON export', () => {
expect(copied.findings[0].id).toBeNull(); 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', () => { it('groups approved environment facts and excludes identities and arbitrary values', () => {
const os = result({ const os = result({
sourceKind: 'environment-observation', title: 'Operating System', environmentState: 'observed', sourceKind: 'environment-observation', title: 'Operating System', environmentState: 'observed',