301 lines
13 KiB
TypeScript
301 lines
13 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import type { CanonicalOutcome, CanonicalSeverity } from '../../src/domain/canonical-model';
|
|
import type { UnifiedResultProjection } from '../../src/domain/unified-result';
|
|
import {
|
|
REPORT_PROJECTION_VERSION,
|
|
buildReportProjection,
|
|
reportPriorityBases,
|
|
type ReportDomain,
|
|
type ReportPolicyEntry,
|
|
type ReportProjectionInput
|
|
} from '../../src/reporting/report-projection';
|
|
import { toolProfileDefinitions } from '../../src/profiles/profile-definition-catalog';
|
|
|
|
const domains: ReportDomain[] = [
|
|
{ id: 'identity', label: 'Identity & Account Hygiene' },
|
|
{ id: 'hardening', label: 'System Hardening' },
|
|
{ id: 'gpo', label: 'GPO & Baseline' }
|
|
];
|
|
|
|
let sequence = 0;
|
|
|
|
function result(overrides: Partial<UnifiedResultProjection> & {
|
|
severity?: CanonicalSeverity;
|
|
outcome?: CanonicalOutcome;
|
|
} = {}): UnifiedResultProjection {
|
|
sequence += 1;
|
|
const severity = overrides.severity ?? 'unknown';
|
|
const outcome = overrides.outcome ?? 'fail';
|
|
return {
|
|
resultProjectionId: overrides.resultProjectionId ?? `synthetic-result-${sequence}`,
|
|
sourceKind: 'scanner-finding',
|
|
sourceResultId: `synthetic-source-${sequence}`,
|
|
projectId: 'synthetic-project',
|
|
collectionRunId: 'synthetic-collection-run',
|
|
sourceTool: overrides.sourceTool ?? 'synthetic-tool',
|
|
title: overrides.title ?? `Synthetic finding ${sequence}`,
|
|
severityOriginal: severity,
|
|
severityNormalized: severity,
|
|
outcomeOriginal: outcome,
|
|
outcomeNormalized: outcome,
|
|
technicalExecutionState: outcome === 'not_run' ? 'not-run' : 'completed',
|
|
resultType: 'control_result',
|
|
isIoe: false,
|
|
isDeviation: outcome === 'fail' || outcome === 'warning',
|
|
lifecycleState: 'unknown',
|
|
mappingConfidence: 1,
|
|
reviewStatus: 'unreviewed',
|
|
importedAt: '2026-09-18T08:00:00.000Z',
|
|
createdAt: '2026-09-18T08:00:00.000Z',
|
|
categories: [],
|
|
...overrides
|
|
} as UnifiedResultProjection;
|
|
}
|
|
|
|
function input(overrides: Partial<ReportProjectionInput> = {}): ReportProjectionInput {
|
|
return {
|
|
generatedAt: '2026-09-18T10:00:00.000Z',
|
|
project: { id: 'synthetic-project', name: 'Synthetic' },
|
|
results: [],
|
|
domains,
|
|
domainOf: () => 'identity',
|
|
...overrides
|
|
};
|
|
}
|
|
|
|
describe('report projection', () => {
|
|
it('carries its version and the caller supplied generation time', () => {
|
|
const projection = buildReportProjection(input());
|
|
expect(projection.version).toBe(REPORT_PROJECTION_VERSION);
|
|
expect(projection.generatedAt).toBe('2026-09-18T10:00:00.000Z');
|
|
});
|
|
|
|
it('orders within one source and never merges sources into one ranking', () => {
|
|
const results = [
|
|
result({ sourceTool: 'alpha', severity: 'low', title: 'alpha-low' }),
|
|
result({ sourceTool: 'alpha', severity: 'critical', title: 'alpha-critical' }),
|
|
result({ sourceTool: 'beta', severity: 'medium', title: 'beta-medium' })
|
|
];
|
|
const projection = buildReportProjection(input({
|
|
results,
|
|
priorityBases: { alpha: { kind: 'severity' }, beta: { kind: 'severity' } }
|
|
}));
|
|
const [domain] = projection.priorities;
|
|
expect(domain.domainId).toBe('identity');
|
|
expect(domain.sources.map(source => source.toolId)).toEqual(['alpha', 'beta']);
|
|
expect(domain.sources[0].shown.map(ref => ref.title)).toEqual(['alpha-critical', 'alpha-low']);
|
|
expect(domain.sources[1].shown.map(ref => ref.title)).toEqual(['beta-medium']);
|
|
});
|
|
|
|
it('refuses a declared severity basis when the source delivers no severity', () => {
|
|
const results = [
|
|
result({ sourceTool: 'cis-cat', severity: 'unknown' }),
|
|
result({ sourceTool: 'cis-cat', severity: 'unknown' })
|
|
];
|
|
const projection = buildReportProjection(input({
|
|
results,
|
|
priorityBases: { 'cis-cat': { kind: 'severity' } }
|
|
}));
|
|
const [source] = projection.sourcePriorities;
|
|
expect(source.declared.kind).toBe('severity');
|
|
expect(source.applied.kind).toBe('undeclared');
|
|
expect(source.severityCoverage).toBe(0);
|
|
expect(source.issues).toContain('declared-severity-without-coverage');
|
|
expect(projection.priorities[0].sources[0].ranked).toBe(false);
|
|
expect(projection.diagnostics.some(entry =>
|
|
entry.code === 'report-priority-declared-severity-without-coverage:cis-cat')).toBe(true);
|
|
});
|
|
|
|
it('marks an undeclared source as unranked instead of inventing an order', () => {
|
|
const projection = buildReportProjection(input({
|
|
results: [result({ sourceTool: 'undeclared-tool', severity: 'high' })]
|
|
}));
|
|
expect(projection.sourcePriorities[0].applied.kind).toBe('undeclared');
|
|
expect(projection.sourcePriorities[0].issues).toContain('basis-not-declared');
|
|
expect(projection.priorities[0].sources[0].ranked).toBe(false);
|
|
expect(projection.findings[0].rankValue).toBeUndefined();
|
|
});
|
|
|
|
it('orders a numeric basis by its declared direction', () => {
|
|
const results = [
|
|
result({ sourceTool: 'pingcastle', score: 5, title: 'five' }),
|
|
result({ sourceTool: 'pingcastle', score: 40, title: 'forty' })
|
|
];
|
|
const projection = buildReportProjection(input({
|
|
results,
|
|
priorityBases: {
|
|
pingcastle: { kind: 'numeric', property: 'points', scale: '0-50+', direction: 'higher-is-worse' }
|
|
}
|
|
}));
|
|
expect(projection.priorities[0].sources[0].shown.map(ref => ref.title)).toEqual(['forty', 'five']);
|
|
});
|
|
|
|
it('caps a domain block without losing the true total', () => {
|
|
const results = Array.from({ length: 5 }, (_unused, index) =>
|
|
result({ sourceTool: 'alpha', severity: 'high', title: `item-${index}` }));
|
|
const projection = buildReportProjection(input({
|
|
results,
|
|
priorityBases: { alpha: { kind: 'severity' } },
|
|
priorityLimitPerDomain: 2
|
|
}));
|
|
expect(projection.priorities[0].sources[0].shown).toHaveLength(2);
|
|
expect(projection.priorities[0].sources[0].total).toBe(5);
|
|
});
|
|
|
|
it('keeps not-run and not-applicable out of the assessed base', () => {
|
|
const results = [
|
|
result({ outcome: 'fail' }),
|
|
result({ outcome: 'pass' }),
|
|
result({ outcome: 'not_run' }),
|
|
result({ outcome: 'not_applicable' }),
|
|
result({ outcome: 'execution_error' })
|
|
];
|
|
const projection = buildReportProjection(input({ results }));
|
|
expect(projection.coverage.total).toBe(5);
|
|
expect(projection.coverage.assessed).toBe(2);
|
|
expect(projection.coverage.notRun).toBe(1);
|
|
expect(projection.coverage.notApplicable).toBe(1);
|
|
expect(projection.coverage.executionErrors).toBe(1);
|
|
expect(projection.domains.find(entry => entry.domainId === 'identity')?.passRate).toBe(0.5);
|
|
});
|
|
|
|
it('leaves a pass rate absent rather than reporting zero when nothing was assessed', () => {
|
|
const projection = buildReportProjection(input({ results: [result({ outcome: 'not_run' })] }));
|
|
expect(projection.domains.find(entry => entry.domainId === 'identity')?.passRate).toBeUndefined();
|
|
});
|
|
|
|
it('keeps GPO equal-weight with its verdict counts and diagnostic flags separate', () => {
|
|
const policyEntries: ReportPolicyEntry[] = [
|
|
{
|
|
id: 'p1', verdict: 'Fail', conflict: false, duplicate: true, title: 'setting one',
|
|
scope: 'computer', identity: 'MinimumPasswordLength', backupInstanceIds: ['b1', 'b2']
|
|
},
|
|
{
|
|
id: 'p2', verdict: 'Not configured', conflict: false, duplicate: false, title: 'setting two',
|
|
scope: 'computer', identity: 'SomeOtherSetting', backupInstanceIds: []
|
|
},
|
|
{
|
|
id: 'p3', verdict: 'Fail', conflict: true, duplicate: false, title: 'setting three',
|
|
scope: 'computer', identity: 'PasswordComplexity', backupInstanceIds: ['b1']
|
|
}
|
|
];
|
|
const projection = buildReportProjection(input({ policyEntries, policyScopeLimited: true }));
|
|
expect(projection.gpo?.basis.kind).toBe('equal-weight');
|
|
expect(projection.gpo?.counts.Fail).toBe(2);
|
|
expect(projection.gpo?.counts['Not configured']).toBe(1);
|
|
expect(projection.gpo?.conflicts).toBe(1);
|
|
expect(projection.gpo?.duplicates).toBe(1);
|
|
expect(projection.gpo?.affectedSettings).toBe(2);
|
|
expect(projection.gpo?.affectedBackups).toBe(2);
|
|
expect(projection.gpo?.scopeLimited).toBe(true);
|
|
});
|
|
|
|
it('omits the remediation section entirely when no reviews exist', () => {
|
|
const projection = buildReportProjection(input({ results: [result()] }));
|
|
expect(projection.remediation).toBeUndefined();
|
|
expect(projection.workItems).toEqual([]);
|
|
});
|
|
|
|
it('curates work items through include and marks the summary as self reported', () => {
|
|
const selected = result({ title: 'selected' });
|
|
const skipped = result({ title: 'skipped' });
|
|
const projection = buildReportProjection(input({
|
|
results: [selected, skipped],
|
|
reviews: [
|
|
{
|
|
resultProjectionId: selected.resultProjectionId, include: true, status: 'Actionable',
|
|
priority: 'High', owner: 'team-ad', markers: ['quickwin'], riskAccepted: false
|
|
},
|
|
{
|
|
resultProjectionId: skipped.resultProjectionId, include: false, status: 'Open',
|
|
priority: 'Medium', markers: [], riskAccepted: false
|
|
}
|
|
]
|
|
}));
|
|
expect(projection.workItems.map(item => item.title)).toEqual(['selected']);
|
|
expect(projection.remediation?.selfReported).toBe(true);
|
|
expect(projection.remediation?.items).toBe(1);
|
|
expect(projection.remediation?.withoutDue).toBe(1);
|
|
expect(projection.remediation?.byStatus).toEqual({ Actionable: 1 });
|
|
});
|
|
|
|
it('never turns a self reported done status into a resolved finding', () => {
|
|
const open = result({ outcome: 'fail', title: 'still failing' });
|
|
const projection = buildReportProjection(input({
|
|
results: [open],
|
|
reviews: [{
|
|
resultProjectionId: open.resultProjectionId, include: true, status: 'Done',
|
|
priority: 'High', markers: [], riskAccepted: false
|
|
}]
|
|
}));
|
|
expect(projection.coverage.deviations).toBe(1);
|
|
expect(projection.findings).toHaveLength(1);
|
|
expect(projection.workItems[0].status).toBe('Done');
|
|
});
|
|
|
|
it('reads the stored comparison and derives no pairing of its own', () => {
|
|
const open = result({ sourceTool: 'alpha', title: 'persisting' });
|
|
const projection = buildReportProjection(input({
|
|
results: [open],
|
|
delta: {
|
|
comparisonId: 'synthetic-comparison',
|
|
contractVersion: '1.0.0',
|
|
referenceRunId: 'run-reference',
|
|
referenceObservedAt: '2026-06-01T00:00:00.000Z',
|
|
currentRunId: 'run-current',
|
|
currentObservedAt: '2026-09-01T00:00:00.000Z',
|
|
entries: [
|
|
{ resultProjectionId: open.resultProjectionId, toolId: 'alpha', state: 'confirmed-open', reasons: [] },
|
|
{ resultProjectionId: 'gone', toolId: 'alpha', state: 'unverified-absent', reasons: ['check-not-run'] },
|
|
{ resultProjectionId: 'fixed', toolId: 'beta', state: 'verified-remediated', reasons: ['fail-to-pass'] }
|
|
]
|
|
}
|
|
}));
|
|
expect(projection.delta?.referenceRunId).toBe('run-reference');
|
|
expect(projection.delta?.currentRunId).toBe('run-current');
|
|
expect(projection.delta?.counts['unverified-absent']).toBe(1);
|
|
expect(projection.delta?.counts['verified-remediated']).toBe(1);
|
|
expect(projection.delta?.counts['confirmed-open']).toBe(1);
|
|
expect(projection.delta?.perSource.map(entry => entry.toolId)).toEqual(['alpha', 'beta']);
|
|
expect(projection.delta?.perSource[0].counts['unverified-absent']).toBe(1);
|
|
expect(projection.delta?.entries[0].title).toBe('persisting');
|
|
});
|
|
|
|
it('has no delta at all without a stored comparison', () => {
|
|
expect(buildReportProjection(input({ results: [result()] })).delta).toBeUndefined();
|
|
});
|
|
|
|
it('adopts the declared basis of every registered profile', () => {
|
|
const bases = reportPriorityBases(toolProfileDefinitions);
|
|
expect(bases['purple-knight']).toEqual({ kind: 'severity' });
|
|
expect(bases.maester).toEqual({ kind: 'severity' });
|
|
expect(bases.pingcastle).toMatchObject({ kind: 'numeric', direction: 'higher-is-worse' });
|
|
expect(bases['cis-cat'].kind).toBe('equal-weight');
|
|
expect(bases.generic.kind).toBe('equal-weight');
|
|
});
|
|
|
|
it('treats a declared unavailable basis as a decision, not a missing declaration', () => {
|
|
const projection = buildReportProjection(input({
|
|
results: [result({ sourceTool: 'cis-cat', severity: 'unknown' })],
|
|
priorityBases: reportPriorityBases(toolProfileDefinitions)
|
|
}));
|
|
const [source] = projection.sourcePriorities;
|
|
expect(source.applied.kind).toBe('equal-weight');
|
|
expect(source.issues).toEqual([]);
|
|
expect(projection.priorities[0].sources[0].ranked).toBe(false);
|
|
expect(projection.diagnostics).toEqual([]);
|
|
});
|
|
|
|
it('ranks a real PingCastle points declaration worst first', () => {
|
|
const projection = buildReportProjection(input({
|
|
results: [
|
|
result({ sourceTool: 'pingcastle', score: 10, title: 'ten' }),
|
|
result({ sourceTool: 'pingcastle', score: 35, title: 'thirtyfive' })
|
|
],
|
|
priorityBases: reportPriorityBases(toolProfileDefinitions)
|
|
}));
|
|
expect(projection.priorities[0].sources[0].shown.map(ref => ref.title))
|
|
.toEqual(['thirtyfive', 'ten']);
|
|
});
|
|
});
|