import { describe, expect, it } from 'vitest'; import type { AssessmentProject } from '../../src/domain/canonical-model'; import type { EnvironmentObservation } from '../../src/domain/environment-context'; import type { UnifiedResultProjection } from '../../src/domain/unified-result'; import { buildResultDbIdentityGuards, collectResultDbIdentityLiterals, ResultDbIdentityGuard } from '../../src/domain/result-db-identity-guard'; import { discloseResultDbProjection, discloseResultDbProjectionWithTrace } from '../../src/domain/result-db-disclosure'; const project: AssessmentProject = { id: 'synthetic-project', name: 'Westeros Holding', createdAt: '2026-09-28T00:00:00Z', updatedAt: '2026-09-28T00:00:00Z', attributes: { shortName: 'WSTR' } }; function observation(property: string, value: string, identity?: string): EnvironmentObservation { return { version: 1, id: `obs-${property}`, scanRunId: 'run', importFileId: 'file', tool: { id: 'pingcastle', name: 'PingCastle' }, subject: { type: 'domain-information', identity }, property, label: property, originalValue: value, normalizedValue: value, valueType: 'string', state: 'observed', source: { objectPath: 'synthetic' }, mappingVersion: 'synthetic', attributes: {} }; } function scanner(overrides: Partial = {}): UnifiedResultProjection { return { resultProjectionId: 'scanner-finding:occ-1', sourceKind: 'scanner-finding', sourceResultId: 'occ-1', projectId: project.id, assessmentId: 'timeslot-7f3a', collectionRunId: 'scan-run-7f3a', scanRunId: 'scan-run-7f3a', sourceTool: 'pingcastle', sourceRuleId: 'A-DC-Spooler', title: 'Spooler is running on DC01', description: 'Host DC01 in sevenkingdoms.local exposes it.', recommendation: 'Disable the spooler on WINTERFELL and ask Jon Snow.', affectedAsset: 'DC01', severityOriginal: 'high', severityNormalized: 'high', outcomeOriginal: 'fail', outcomeNormalized: 'fail', technicalExecutionState: 'completed', resultType: 'exposure_finding', isIoe: true, isDeviation: true, lifecycleState: 'unknown', mappingConfidence: 1, reviewOwner: 'Jon Snow', importedAt: '2026-09-28T00:00:00Z', createdAt: '2026-09-28T00:00:00Z', frameworkReferences: [], categories: [], ...overrides }; } describe('Result DB client identity guard', () => { it('collects the identities a client names in its own data, without generic or public words', () => { const literals = collectResultDbIdentityLiterals({ projects: [project], environmentObservations: [observation('domain.fqdn', 'sevenkingdoms.local'), observation('host.name', 'WINTERFELL', 'WINTERFELL'), observation('domain.functional_level', 'Windows Server 2016')], affectedAssets: ['DC01', 'Domain Admins', 'krbtgt'], reviewOwners: ['Jon Snow'], principals: ['SEVENKINGDOMS\\arya.stark', 'BUILTIN\\Administrators', 'ned@sevenkingdoms.local'] }); // Case-insensitive: the NetBIOS name SEVENKINGDOMS and the domain label sevenkingdoms are one literal. expect(literals.map(value => value.toLowerCase())).toEqual(expect.arrayContaining(['westeros holding', 'wstr', 'sevenkingdoms.local', 'sevenkingdoms', 'winterfell', 'dc01', 'jon snow', 'arya.stark', 'ned'])); expect(literals.map(value => value.toLowerCase())).not.toEqual(expect.arrayContaining(['domain admins', 'krbtgt', 'builtin', 'administrators', 'windows server 2016'])); expect(literals.indexOf('sevenkingdoms.local')).toBeLessThan(literals.indexOf('sevenkingdoms')); }); it('removes names that no pattern recognizes, such as DC01 or a person, from every disclosed field', () => { const guards = buildResultDbIdentityGuards({ projects: [project], environmentObservations: [{ projectId: project.id, observation: observation('host.name', 'WINTERFELL') }], internalResults: [scanner()] }); const disclosed = discloseResultDbProjection(scanner(), guards.get(project.id)); const serialized = JSON.stringify(disclosed); for (const identity of ['DC01', 'WINTERFELL', 'Jon Snow', 'sevenkingdoms']) expect(serialized).not.toContain(identity); expect(disclosed.title).toBe('Spooler is running on [redacted]'); }); it('takes only account forms from GPO values, never registry keys or paths', () => { const guards = buildResultDbIdentityGuards({ projects: [project], environmentObservations: [], internalResults: [scanner({ policyValues: [{ key: 'Software\\Policies\\Microsoft\\Windows NT\\Printers', valueName: 'Machine\\Test', principals: ['SEVENKINGDOMS\\arya.stark', 'HKLM\\Software', 'BUILTIN\\Administrators'] }] })] }); const guard = guards.get(project.id)!; expect(guard.text('Software\\Policies\\Microsoft\\Windows NT\\Printers · RestrictDriverInstallationToAdministrators')) .toBe('Software\\Policies\\Microsoft\\Windows NT\\Printers · RestrictDriverInstallationToAdministrators'); expect(guard.text('Granted to arya.stark in SEVENKINGDOMS')).toBe('Granted to [redacted] in [redacted]'); }); it('withholds a whole field when an identity survives in another spelling', () => { const guard = new ResultDbIdentityGuard(['sevenkingdoms.local']); expect(guard.text('Forest seven-kingdoms . local is affected')).toBe('[withheld by identity guard]'); expect(guard.text('Forest sevenkingdoms.local is affected')).toBe('Forest [redacted] is affected'); expect(guard.text('Nothing identifying here')).toBe('Nothing identifying here'); }); it('records a content-free trace for every stored row', () => { const guards = buildResultDbIdentityGuards({ projects: [project], environmentObservations: [{ projectId: project.id, observation: observation('host.name', 'WINTERFELL') }], internalResults: [scanner()] }); const row = discloseResultDbProjectionWithTrace(scanner({ description: 'Host DC01 has 15 accounts, see https://tenant.sharepoint.com/x.', reviewStatus: 'open' }), guards.get(project.id)); expect(row.disclosureTrace).toMatchObject({ version: 1, guardWithheldFields: [] }); // DC01 is this finding's own affected asset (private-value); Jon Snow is caught by the client guard. expect(row.disclosureTrace?.redactions).toEqual(expect.arrayContaining([ { field: 'title', rule: 'private-value', count: 1 }, { field: 'description', rule: 'count', count: 1 }, { field: 'description', rule: 'url', count: 1 }, { field: 'description', rule: 'private-value', count: 1 }, { field: 'recommendation', rule: 'private-value', count: 1 }, { field: 'recommendation', rule: 'identity-guard', count: 1 } ])); expect(row.disclosureTrace?.withheldFields).toContain('affectedAsset'); // Client-owned review state is withheld without a trace entry, so its presence is not revealed. expect(JSON.stringify(row.disclosureTrace)).not.toMatch(/review/i); expect(JSON.stringify(row.disclosureTrace)).not.toMatch(/DC01|WINTERFELL|Jon Snow|15|sharepoint/); expect(row.recommendation).toBe('Disable the spooler on [redacted] and ask [redacted].'); }); it('is idempotent: disclosing a stored row again changes nothing, so UI, API and export show the same content', () => { const guards = buildResultDbIdentityGuards({ projects: [project], environmentObservations: [], internalResults: [scanner()] }); const fixtures = [ scanner(), scanner({ sourceTool: 'maester', sourceRuleId: 'MT.1074', title: 'No more than 100 mails via the .onmicrosoft.com domain', description: '1. Open [Entra](https://entra.microsoft.com). 2. Select Users. 3. Save. Found 4 accounts.', recommendation: 'Set at least 8 characters; see https://learn.microsoft.com/en-us/entra/ and Directory.ReadWrite.All.' }), scanner({ sourceTool: 'cis-cat', sourceRuleId: '18.9.20.1.1', title: "18.9.20.1.1 (L2) Ensure 'Turn off downloading of print drivers over HTTP' is set to 'Enabled'", recommendation: 'Computer Configuration\\Policies\\Administrative Templates\\System\\Internet Communication Management' }) ]; for (const fixture of fixtures) { const stored = discloseResultDbProjectionWithTrace(fixture, guards.get(project.id)); expect(discloseResultDbProjection(stored)).toEqual(stored); } }); });