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:
parent
e74605de39
commit
be32f1607b
|
|
@ -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.
|
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 |
|
| 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-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. |
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue