Assessor_2.0/tests/domain/scoped-risk-acceptance.test.ts

159 lines
11 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import type { FindingOccurrence, JsonValue } from '../../src/domain/canonical-model';
import {
appendScopedRiskAcceptance,
defenderRiskCoverageDecisions,
defenderRiskTargetsForOccurrence,
defenderRiskTargetsForOccurrences,
scopedAcceptanceCovers,
scopedRiskAcceptanceState,
scopedRiskTargetFromOccurrences
} 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<typeof observation>[], 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('carries coverage only to the exact device key and captured canonical CVE definition', () => {
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];
expect(scopedAcceptanceCovers(acceptance, laterSame, '2026-09-21')).toBe(true);
expect(scopedAcceptanceCovers(acceptance, laterNewCve, '2026-09-21')).toBe(false);
expect(scopedAcceptanceCovers(acceptance, changedDevice, '2026-09-21')).toBe(false);
});
});