import { describe, expect, it } from 'vitest'; import type { CanonicalOutcome, CanonicalTechnicalExecutionState } from '../../src/domain/canonical-model'; import { RESULT_CONTRACT_MAPPING_VERSION, deriveExposureState, deriveIsIoe, deriveResultType, projectResultState } from '../../src/domain/result-contract-mapping'; describe('RESULT_CONTRACT result_type derivation', () => { it('maps exposure-scanner tools to exposure_finding', () => { expect(deriveResultType('pingcastle')).toBe('exposure_finding'); expect(deriveResultType('purple-knight')).toBe('exposure_finding'); expect(deriveResultType('maester')).toBe('exposure_finding'); }); it('maps CIS-CAT to compliance_check', () => { expect(deriveResultType('cis-cat')).toBe('compliance_check'); }); it('falls back to unknown for unknown or generic tools, never inventing exposure/compliance', () => { expect(deriveResultType('generic')).toBe('unknown'); expect(deriveResultType('some-future-tool')).toBe('unknown'); }); it('is case- and whitespace-insensitive on the tool id, not on unrelated text', () => { expect(deriveResultType(' PingCastle ')).toBe('exposure_finding'); }); }); // Tabelle "Quellen-Mapping" aus docs/RESULT_CONTRACT.md, Zeile für Zeile. describe('RESULT_CONTRACT source mapping table (docs/RESULT_CONTRACT.md)', () => { const cases: Array<{ label: string; toolId: string; outcomeNormalized: CanonicalOutcome; expectedResultType: ReturnType; expectedExposureState: ReturnType; expectedIsIoe: boolean; }> = [ { label: 'Purple Knight: IOEs found', toolId: 'purple-knight', outcomeNormalized: 'fail', expectedResultType: 'exposure_finding', expectedExposureState: 'detected', expectedIsIoe: true }, { label: 'Purple Knight: No IOE found', toolId: 'purple-knight', outcomeNormalized: 'pass', expectedResultType: 'exposure_finding', expectedExposureState: 'not_detected', expectedIsIoe: false }, { label: 'PingCastle: RiskRule Points > 0', toolId: 'pingcastle', outcomeNormalized: 'fail', expectedResultType: 'exposure_finding', expectedExposureState: 'detected', expectedIsIoe: true }, { label: 'PingCastle: RiskRule Points = 0', toolId: 'pingcastle', outcomeNormalized: 'pass', expectedResultType: 'exposure_finding', expectedExposureState: 'not_detected', expectedIsIoe: false }, { label: 'PingCastle: nicht auswertbar', toolId: 'pingcastle', outcomeNormalized: 'not_evaluated', expectedResultType: 'exposure_finding', expectedExposureState: 'not_evaluated', expectedIsIoe: false }, { label: 'PingCastle: Ausfuehrungsfehler', toolId: 'pingcastle', outcomeNormalized: 'execution_error', expectedResultType: 'exposure_finding', expectedExposureState: 'execution_error', expectedIsIoe: false }, { label: 'Maester: Security Test Failed', toolId: 'maester', outcomeNormalized: 'fail', expectedResultType: 'exposure_finding', expectedExposureState: 'detected', expectedIsIoe: true }, { label: 'Maester: Passed', toolId: 'maester', outcomeNormalized: 'pass', expectedResultType: 'exposure_finding', expectedExposureState: 'not_detected', expectedIsIoe: false } ]; it.each(cases)('$label -> result_type=$expectedResultType exposure_state=$expectedExposureState is_ioe=$expectedIsIoe', ({ toolId, outcomeNormalized, expectedResultType, expectedExposureState, expectedIsIoe }) => { const resultType = deriveResultType(toolId); expect(resultType).toBe(expectedResultType); const exposureState = deriveExposureState(resultType, outcomeNormalized); expect(exposureState).toBe(expectedExposureState); expect(deriveIsIoe(resultType, exposureState).isIoe).toBe(expectedIsIoe); }); }); describe('RESULT_CONTRACT is_ioe invariant (never default true outside exposure_finding+detected)', () => { it('is false for compliance_check regardless of exposure-like input', () => { const derivation = deriveIsIoe('compliance_check', 'detected'); expect(derivation.isIoe).toBe(false); }); it('is false for policy_deviation', () => { expect(deriveIsIoe('policy_deviation', undefined).isIoe).toBe(false); }); it('is false for unknown result_type', () => { expect(deriveIsIoe('unknown', undefined).isIoe).toBe(false); }); it('is false for exposure_finding without a detected exposure_state', () => { expect(deriveIsIoe('exposure_finding', 'not_detected').isIoe).toBe(false); expect(deriveIsIoe('exposure_finding', 'not_evaluated').isIoe).toBe(false); expect(deriveIsIoe('exposure_finding', 'execution_error').isIoe).toBe(false); expect(deriveIsIoe('exposure_finding', undefined).isIoe).toBe(false); }); it('always carries the mapping rule id and the current mapping version', () => { const derivation = deriveIsIoe('exposure_finding', 'detected'); expect(derivation.isIoe).toBe(true); expect(derivation.ioeRuleId).toBeTruthy(); expect(derivation.ioeRuleVersion).toBe(RESULT_CONTRACT_MAPPING_VERSION); expect(derivation.ioeDerivationMethod).toBeTruthy(); }); }); describe('exposure_state only exists for exposure_finding', () => { it('is undefined for compliance_check, policy_deviation, informational and unknown', () => { (['compliance_check', 'policy_deviation', 'informational', 'unknown'] as const).forEach(resultType => { expect(deriveExposureState(resultType, 'fail')).toBeUndefined(); }); }); }); describe('projectResultState — Ableitung von result_state aus den getrennten Bestandsachsen', () => { const completed: CanonicalTechnicalExecutionState = 'completed'; const failed: CanonicalTechnicalExecutionState = 'failed'; it('a technical execution failure always projects to error, regardless of outcome', () => { expect(projectResultState('fail', failed)).toBe('error'); expect(projectResultState('pass', failed)).toBe('error'); expect(projectResultState('unknown', failed)).toBe('error'); }); it('projects the fachliche outcome when execution completed', () => { expect(projectResultState('fail', completed)).toBe('fail'); expect(projectResultState('pass', completed)).toBe('pass'); expect(projectResultState('warning', completed)).toBe('warning'); expect(projectResultState('execution_error', completed)).toBe('error'); expect(projectResultState('not_applicable', completed)).toBe('not_applicable'); expect(projectResultState('not_evaluated', completed)).toBe('not_evaluated'); expect(projectResultState('informational', completed)).toBe('informational'); expect(projectResultState('unknown', completed)).toBe('unknown'); }); it('projects not_run (a value the contract does not define) to not_evaluated rather than inventing a value', () => { expect(projectResultState('not_run', completed)).toBe('not_evaluated'); }); });