import { describe, expect, it } from 'vitest'; import type { FindingOccurrence, JsonValue } from '../../src/domain/canonical-model'; import { appendScopedRiskAcceptance, defenderRiskCoverageDecisions, defenderRiskTargetsForOccurrence, defenderRiskTargetsForOccurrences, scopedAcceptanceCovers, scopedRiskAcceptanceState, scopedRiskAcceptanceStateForTargets, scopedRiskTargetFromOccurrences, withCapturedScopeStatus } from '../../src/domain/scoped-risk-acceptance'; import { appendRiskAcceptanceReview } from '../../src/domain/risk-acceptance'; import { riskCommand } from './risk-acceptance-fixture'; const now = '2026-09-21T08:00:00.000Z'; const confidence = { level: 'high' as const, score: 1, reasons: ['synthetic'] }; const provenance = { decisionSource: 'source-native' as const, confidence, mappingVersion: '1.0.0' }; function observation(deviceId: string, cve: string, name = deviceId, ip = '192.0.2.10') { return { name, ip, site: 'Synthetic site', sensor: 'sensor-a', cve, severity: 'High', description: 'Synthetic finding', additionalSourceFields: deviceId ? { 'Device ID': deviceId } : {}, source: { tableIndex: 1, rowIndex: 2 } }; } function occurrence(id: string, definitionId: string, cve: string, observations: ReturnType[], scanRunId = 'run-1'): FindingOccurrence { return { id, definitionId, scanRunId, importFileId: `file-${scanRunId}`, sourceTableId: `table-${scanRunId}`, sourceIdentity: { tool: { id: 'defender-for-iot-vulnerabilities', name: 'Microsoft Defender for IoT', version: '1.2.0' }, originalRuleId: cve, canonicalRuleId: cve, originalTitle: cve }, sourcePosition: { rowIndex: 2 }, originalFields: [{ key: 'source-observations', label: 'Source Observations', value: observations as JsonValue, columnIndex: 0 }], fieldMappings: [], severity: { originalValue: 'High', normalizedValue: 'high', provenance }, status: { originalValue: 'Open', normalizedValue: 'open', provenance } }; } describe('device-scoped Defender risk acceptance', () => { it('projects stable targets and isolates one CVE across multiple devices', () => { const source = occurrence('occurrence-1', 'definition-cve-1', 'CVE-2026-1001', [ observation('DEV-A', 'CVE-2026-1001', 'Device A', '192.0.2.10'), observation('DEV-B', 'CVE-2026-1001', 'Device B', '192.0.2.20') ]); const projected = defenderRiskTargetsForOccurrence(source); expect(projected).toEqual([ expect.objectContaining({ definitionId: 'definition-cve-1', cve: 'CVE-2026-1001', device: expect.objectContaining({ key: 'device-id:dev-a', identityMethod: 'device-id', identityConfidence: 'high' }), eligible: true }), expect.objectContaining({ definitionId: 'definition-cve-1', cve: 'CVE-2026-1001', device: expect.objectContaining({ key: 'device-id:dev-b' }), eligible: true }) ]); const target = scopedRiskTargetFromOccurrences('device-finding', 'device-id:dev-a', [source], now); expect(target.members).toEqual([expect.objectContaining({ findingDefinitionId: 'definition-cve-1', cve: 'CVE-2026-1001', capturedOccurrences: [expect.objectContaining({ occurrenceId: 'occurrence-1', scanRunId: 'run-1', importFileId: 'file-run-1', sourceTableId: 'table-run-1', sourcePosition: { rowIndex: 2 } })] })]); const pair = appendScopedRiskAcceptance(undefined, 'project-1', 'pair-1', target, riskCommand('accept', '2026-09-21'), now); expect(defenderRiskCoverageDecisions(projected[0], undefined, [pair], '2026-09-21')).toHaveLength(1); expect(defenderRiskCoverageDecisions(projected[1], undefined, [pair], '2026-09-21')).toHaveLength(0); const whole = appendRiskAcceptanceReview(undefined, source.id, riskCommand('accept', '2026-09-21'), now).riskAcceptance; expect(defenderRiskCoverageDecisions(projected[0], whole, [], '2026-09-21')).toHaveLength(1); expect(defenderRiskCoverageDecisions(projected[1], whole, [], '2026-09-21')).toHaveLength(1); }); it('captures every current CVE once and rejects unresolved or ambiguous device identities', () => { const cve1 = occurrence('occurrence-1', 'definition-cve-1', 'CVE-2026-1001', [observation('DEV-A', 'CVE-2026-1001', 'Device A')]); const cve2 = occurrence('occurrence-2', 'definition-cve-2', 'CVE-2026-1002', [observation('DEV-A', 'CVE-2026-1002', 'Device A')]); const duplicate = occurrence('occurrence-3', 'definition-cve-1', 'CVE-2026-1001', [observation('DEV-A', 'CVE-2026-1001', 'Device A')], 'run-2'); const target = scopedRiskTargetFromOccurrences('device-snapshot', 'device-id:dev-a', [cve1, cve2, duplicate], now); expect(target.members.map(item => item.cve)).toEqual(['CVE-2026-1001', 'CVE-2026-1002']); expect(target.members[0].capturedOccurrences.map(item => item.occurrenceId)).toEqual(['occurrence-1', 'occurrence-3']); expect(target.capturedScanRunIds).toEqual(['run-1', 'run-2']); const unresolvedObservation = { ...observation('', 'CVE-2026-2001', '', ''), site: '' }; const unresolved = occurrence('unresolved', 'definition-unresolved', 'CVE-2026-2001', [unresolvedObservation]); const unresolvedTarget = defenderRiskTargetsForOccurrence(unresolved)[0]; expect(unresolvedTarget).toMatchObject({ eligible: false, unavailableReason: expect.stringContaining('stable device identity') }); expect(() => scopedRiskTargetFromOccurrences('device-finding', unresolvedTarget.device.key, [unresolved], now)).toThrow('stable device identity'); const ambiguous = occurrence('ambiguous', 'definition-ambiguous', 'CVE-2026-2002', [ observation('DEV-X', 'CVE-2026-2002', 'Device X', '192.0.2.30'), observation('DEV-X', 'CVE-2026-2002', 'Different device', '192.0.2.31') ]); expect(defenderRiskTargetsForOccurrence(ambiguous)[0]).toMatchObject({ eligible: false, unavailableReason: expect.stringContaining('conflicting identity') }); expect(() => scopedRiskTargetFromOccurrences('device-finding', 'device-id:dev-x', [ambiguous], now)).toThrow('conflicting identity'); const crossCveConflict = [ occurrence('cross-1', 'cross-definition-1', 'CVE-2026-3001', [observation('DEV-Y', 'CVE-2026-3001', 'Device Y', '192.0.2.40')]), occurrence('cross-2', 'cross-definition-2', 'CVE-2026-3002', [observation('DEV-Y', 'CVE-2026-3002', 'Different Y', '192.0.2.41')]) ]; expect(defenderRiskTargetsForOccurrences(crossCveConflict).every(item => !item.eligible)).toBe(true); expect(() => scopedRiskTargetFromOccurrences('device-snapshot', 'device-id:dev-y', crossCveConflict, now)).toThrow('conflicting identity'); }); it('keeps same-CVE devices attached to their own source occurrence', () => { const first = occurrence('occurrence-a', 'definition-cve-1', 'CVE-2026-1001', [ observation('DEV-A', 'CVE-2026-1001', 'Device A') ]); const second = occurrence('occurrence-b', 'definition-cve-1', 'CVE-2026-1001', [ observation('DEV-B', 'CVE-2026-1001', 'Device B') ]); expect(defenderRiskTargetsForOccurrences([first, second]).map(item => [item.occurrenceId, item.device.key])).toEqual([ ['occurrence-a', 'device-id:dev-a'], ['occurrence-b', 'device-id:dev-b'] ]); expect(() => scopedRiskTargetFromOccurrences('device-finding', 'device-id:dev-b', [first, second], now, new Set([first.id]))) .toThrow('not an actionable member'); }); it('derives decision state, preserves overlaps, and prevents target mutation', () => { const cve1 = occurrence('occurrence-1', 'definition-cve-1', 'CVE-2026-1001', [observation('DEV-A', 'CVE-2026-1001')]); const cve2 = occurrence('occurrence-2', 'definition-cve-2', 'CVE-2026-1002', [observation('DEV-A', 'CVE-2026-1002')]); const pairTarget = scopedRiskTargetFromOccurrences('device-finding', 'device-id:dev-a', [cve1], now); const snapshotTarget = scopedRiskTargetFromOccurrences('device-snapshot', 'device-id:dev-a', [cve1, cve2], now); const pair = appendScopedRiskAcceptance(undefined, 'project-1', 'pair-1', pairTarget, riskCommand('accept', '2026-09-21'), now); const snapshot = appendScopedRiskAcceptance(undefined, 'project-1', 'snapshot-1', snapshotTarget, riskCommand('accept', '2026-09-21'), now); const target1 = defenderRiskTargetsForOccurrence(cve1)[0]; const target2 = defenderRiskTargetsForOccurrence(cve2)[0]; expect(scopedRiskAcceptanceState(pair, '2026-09-21')).toBe('Accepted'); const draft = appendScopedRiskAcceptance(undefined, 'project-1', 'draft-1', pairTarget, riskCommand('draft', '2026-09-21'), now); expect(scopedRiskAcceptanceState(draft, '2026-09-21')).toBe('Draft'); const scheduledCommand = riskCommand('accept', '2026-09-21'); scheduledCommand.details.validFrom = '2026-09-22'; const scheduled = appendScopedRiskAcceptance(undefined, 'project-1', 'scheduled-1', pairTarget, scheduledCommand, now); expect(scopedRiskAcceptanceState(scheduled, '2026-09-21')).toBe('Scheduled'); const reviewDue = structuredClone(pair); reviewDue.revisions[0].details.reviewOn = '2026-09-21'; expect(scopedRiskAcceptanceState(reviewDue, '2026-09-21')).toBe('Review due'); const expired = structuredClone(pair); expired.revisions[0].details.expiresOn = '2026-09-20'; expect(scopedRiskAcceptanceState(expired, '2026-09-21')).toBe('Expired'); expect(scopedAcceptanceCovers(pair, target1, '2026-09-21')).toBe(true); expect(scopedAcceptanceCovers(pair, target2, '2026-09-21')).toBe(false); expect(scopedAcceptanceCovers(snapshot, target1, '2026-09-21')).toBe(true); expect(scopedAcceptanceCovers(snapshot, target2, '2026-09-21')).toBe(true); const revokedPair = appendScopedRiskAcceptance(pair, 'project-1', pair.id, pair.target, riskCommand('revoke', '2026-09-21'), '2026-09-21T09:00:00.000Z'); expect(scopedAcceptanceCovers(revokedPair, target1, '2026-09-21')).toBe(false); expect(scopedAcceptanceCovers(snapshot, target1, '2026-09-21')).toBe(true); expect(() => appendScopedRiskAcceptance(pair, 'project-1', pair.id, snapshotTarget, riskCommand('draft', '2026-09-21'), now)).toThrow('cannot be retargeted'); }); it('covers only the captured occurrence and needs review on a later import, like a whole-finding acceptance', () => { const original = occurrence('occurrence-old', 'definition-cve-1', 'CVE-2026-1001', [observation('DEV-A', 'CVE-2026-1001')]); const target = scopedRiskTargetFromOccurrences('device-snapshot', 'device-id:dev-a', [original], now); const acceptance = appendScopedRiskAcceptance(undefined, 'project-1', 'snapshot-1', target, riskCommand('accept', '2026-09-21'), now); const laterSame = defenderRiskTargetsForOccurrence(occurrence('occurrence-new', 'definition-cve-1', 'CVE-2026-1001', [observation('DEV-A', 'CVE-2026-1001')], 'run-2'))[0]; const laterNewCve = defenderRiskTargetsForOccurrence(occurrence('occurrence-new-cve', 'definition-cve-2', 'CVE-2026-1002', [observation('DEV-A', 'CVE-2026-1002')], 'run-2'))[0]; const changedDevice = defenderRiskTargetsForOccurrence(occurrence('occurrence-changed-device', 'definition-cve-1', 'CVE-2026-1001', [observation('DEV-B', 'CVE-2026-1001')], 'run-2'))[0]; const captured = defenderRiskTargetsForOccurrence(original)[0]; expect(scopedAcceptanceCovers(acceptance, captured, '2026-09-21')).toBe(true); expect(scopedRiskAcceptanceStateForTargets(acceptance, [captured], '2026-09-21')).toBe('Accepted'); // Same device and CVE through a replacement occurrence: kept, but never silently re-attached. expect(scopedAcceptanceCovers(acceptance, laterSame, '2026-09-21')).toBe(false); expect(scopedRiskAcceptanceStateForTargets(acceptance, [laterSame], '2026-09-21')).toBe('Review required'); expect(scopedAcceptanceCovers(acceptance, laterNewCve, '2026-09-21')).toBe(false); expect(scopedAcceptanceCovers(acceptance, changedDevice, '2026-09-21')).toBe(false); }); it('marks a decision whose captured scan run was replaced as Review required without coverage', () => { const original = occurrence('occurrence-old', 'definition-cve-1', 'CVE-2026-1001', [observation('DEV-A', 'CVE-2026-1001')]); const target = scopedRiskTargetFromOccurrences('device-finding', 'device-id:dev-a', [original], now); const acceptance = appendScopedRiskAcceptance(undefined, 'project-1', 'pair-1', target, riskCommand('accept', '2026-09-21'), now); const captured = defenderRiskTargetsForOccurrence(original)[0]; const current = withCapturedScopeStatus(acceptance, new Set(['run-1'])); expect(current.capturedScopeReplaced).toBe(false); expect(scopedRiskAcceptanceState(current, '2026-09-21')).toBe('Accepted'); const replaced = withCapturedScopeStatus(acceptance, new Set(['run-2'])); expect(replaced.capturedScopeReplaced).toBe(true); expect(scopedRiskAcceptanceState(replaced, '2026-09-21')).toBe('Review required'); expect(scopedAcceptanceCovers(replaced, captured, '2026-09-21')).toBe(false); expect(defenderRiskCoverageDecisions(captured, undefined, [replaced], '2026-09-21')).toEqual([]); // The history is kept and a revoked decision stays revoked. expect(replaced.revisions).toEqual(acceptance.revisions); const revoked = withCapturedScopeStatus( appendScopedRiskAcceptance(acceptance, 'project-1', 'pair-1', target, riskCommand('revoke', '2026-09-21'), now), new Set(['run-2'])); expect(scopedRiskAcceptanceState(revoked, '2026-09-21')).toBe('Revoked'); // The marker is derived on read and never becomes part of the stored history. expect(appendScopedRiskAcceptance(replaced, 'project-1', 'pair-1', target, riskCommand('draft', '2026-09-21'), now)) .not.toHaveProperty('capturedScopeReplaced'); }); });