134 lines
7.0 KiB
TypeScript
134 lines
7.0 KiB
TypeScript
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('maps Defender CVEs to vulnerability_finding', () => {
|
|
expect(deriveResultType('defender-for-iot-vulnerabilities')).toBe('vulnerability_finding');
|
|
});
|
|
|
|
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<typeof deriveResultType>;
|
|
expectedExposureState: ReturnType<typeof deriveExposureState>;
|
|
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 vulnerability_finding', () => {
|
|
expect(deriveIsIoe('vulnerability_finding', 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 non-exposure result types', () => {
|
|
(['vulnerability_finding', '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');
|
|
});
|
|
});
|