187 lines
7.0 KiB
TypeScript
187 lines
7.0 KiB
TypeScript
import { describe as suite, it, expect } from 'vitest'
|
|
import {
|
|
type CapabilityReport,
|
|
cite,
|
|
citeVerbose,
|
|
describe as describeDetermination,
|
|
describeMeasurement,
|
|
evidence,
|
|
formatReport,
|
|
formatRollUp,
|
|
label,
|
|
overall,
|
|
rollUp,
|
|
validateReport,
|
|
} from '../src/index.js'
|
|
|
|
suite('determination', () => {
|
|
it('never renders a non-pass as a pass', () => {
|
|
expect(label('not-assessed')).toBe('NOT ASSESSED')
|
|
expect(label('not-applicable')).toBe('N/A')
|
|
expect(describeDetermination('not-assessed')).toContain('not a pass')
|
|
})
|
|
|
|
it('rolls an empty set up to not-assessed, not pass', () => {
|
|
// [].every(...) === true is the exact trap this closes.
|
|
expect(rollUp([])).toBe('not-assessed')
|
|
})
|
|
|
|
it('lets one failure dominate', () => {
|
|
expect(rollUp(['pass', 'pass', 'fail'])).toBe('fail')
|
|
})
|
|
|
|
it('does not let an unassessed item average away', () => {
|
|
expect(rollUp(['pass', 'not-assessed'])).toBe('not-assessed')
|
|
})
|
|
|
|
it('reports all-N/A as N/A rather than pass', () => {
|
|
expect(rollUp(['not-applicable', 'not-applicable'])).toBe('not-applicable')
|
|
})
|
|
})
|
|
|
|
suite('citations', () => {
|
|
it('cites source as path:line so a terminal can open it', () => {
|
|
expect(cite(evidence.file('docker-compose.yml', 42, 'ports: ["2222:22"]'))).toBe('docker-compose.yml:42')
|
|
})
|
|
|
|
it('cites a row as table#id with the deciding column', () => {
|
|
expect(cite(evidence.record('requirements', 'a1b2c3', 'parent_id', 'null')))
|
|
.toBe('requirements#a1b2c3 (parent_id)')
|
|
})
|
|
|
|
it('carries sample count into the citation, because 100% of nothing is not 100%', () => {
|
|
const none = evidence.measurement('uptime', null, 0, { unit: '%', window: '2026-07' })
|
|
const real = evidence.measurement('uptime', 98.3, 8640, { unit: '%', window: '2026-07' })
|
|
expect(cite(none)).toContain('not computable')
|
|
expect(cite(none)).toContain('0 samples')
|
|
expect(cite(real)).toContain('98.3%')
|
|
expect(cite(real)).toContain('8640 samples')
|
|
})
|
|
|
|
it('reads a zero-sample measurement as uncomputable, not as a value', () => {
|
|
const s = evidence.measurement('Availability', null, 0, { window: '2026-07' }).source
|
|
expect(describeMeasurement(s as never)).toBe(
|
|
'No samples for Availability in 2026-07; it cannot be computed.',
|
|
)
|
|
})
|
|
|
|
it('includes excerpt and note in the verbose form', () => {
|
|
const lines = citeVerbose(evidence.file('server/api/x.ts', 7, 'const q = sql(raw)', 'unparameterised'))
|
|
expect(lines[0]).toBe('server/api/x.ts:7')
|
|
expect(lines.join('\n')).toContain('const q = sql(raw)')
|
|
expect(lines.join('\n')).toContain('unparameterised')
|
|
})
|
|
})
|
|
|
|
const clean: CapabilityReport = {
|
|
capability: 'SideDoor',
|
|
scope: '49 compose services',
|
|
examined: 49,
|
|
findings: [],
|
|
}
|
|
|
|
suite('report', () => {
|
|
it('derives not-assessed from an empty scan instead of reporting clean', () => {
|
|
const empty: CapabilityReport = { capability: 'SideDoor', scope: 'nothing', examined: 0, findings: [] }
|
|
expect(overall(empty)).toBe('not-assessed')
|
|
const out = formatReport(empty)
|
|
expect(out).toContain('NOT ASSESSED')
|
|
expect(out).not.toContain('✓')
|
|
})
|
|
|
|
it('reports a genuine clean run as a pass', () => {
|
|
expect(overall(clean)).toBe('pass')
|
|
expect(formatReport(clean)).toContain('✓')
|
|
})
|
|
|
|
it('rejects a conclusion with no evidence behind it', () => {
|
|
const asserted: CapabilityReport = {
|
|
capability: 'CertPack',
|
|
scope: '3 controls',
|
|
examined: 3,
|
|
findings: [{ id: 'f1', summary: 'AC-2 implemented', determination: 'pass', severity: 'info', evidence: [] }],
|
|
}
|
|
const problems = validateReport(asserted)
|
|
expect(problems).toHaveLength(1)
|
|
expect(problems[0]!.problem).toContain('no evidence')
|
|
})
|
|
|
|
it('allows a not-assessed finding to carry no evidence', () => {
|
|
const unassessed: CapabilityReport = {
|
|
capability: 'CertPack',
|
|
scope: '3 controls',
|
|
examined: 3,
|
|
findings: [{ id: 'f1', summary: 'AC-2', determination: 'not-assessed', severity: 'high', evidence: [] }],
|
|
}
|
|
expect(validateReport(unassessed)).toHaveLength(0)
|
|
})
|
|
|
|
it('catches duplicate finding ids', () => {
|
|
const dup: CapabilityReport = {
|
|
capability: 'X', scope: 's', examined: 2,
|
|
findings: [
|
|
{ id: 'f1', summary: 'a', determination: 'pass', severity: 'info', evidence: [evidence.file('a.ts')] },
|
|
{ id: 'f1', summary: 'b', determination: 'pass', severity: 'info', evidence: [evidence.file('b.ts')] },
|
|
],
|
|
}
|
|
expect(validateReport(dup).some(p => p.problem === 'duplicate finding id')).toBe(true)
|
|
})
|
|
|
|
it('shows a missing citation loudly rather than silently', () => {
|
|
const r: CapabilityReport = {
|
|
capability: 'X', scope: 's', examined: 1,
|
|
findings: [{ id: 'f1', summary: 'something is wrong', determination: 'fail', severity: 'high', evidence: [] }],
|
|
}
|
|
expect(formatReport(r)).toContain('(no evidence attached)')
|
|
})
|
|
|
|
it('does not label an unassessed finding as a missing citation', () => {
|
|
// Having nothing to cite IS the content of a not-assessed finding.
|
|
// Calling it a missing citation reads as a defect in the report rather
|
|
// than the gap the report is describing.
|
|
const r: CapabilityReport = {
|
|
capability: 'X', scope: 's', examined: 1,
|
|
findings: [{ id: 'f1', summary: 'rule covered nothing', determination: 'not-assessed', severity: 'medium', evidence: [] }],
|
|
}
|
|
const out = formatReport(r)
|
|
expect(out).toContain('(nothing was observed)')
|
|
expect(out).not.toContain('(no evidence attached)')
|
|
})
|
|
|
|
it('sorts worst first so a critical cannot hide under info lines', () => {
|
|
const r: CapabilityReport = {
|
|
capability: 'SideDoor', scope: '2 services', examined: 2,
|
|
findings: [
|
|
{ id: 'f2', summary: 'loopback bind', determination: 'pass', severity: 'info', evidence: [evidence.file('a.yml', 3)] },
|
|
{ id: 'f1', summary: 'SSH published on a host port', determination: 'fail', severity: 'critical', evidence: [evidence.file('b.yml', 9, '2222:22')] },
|
|
],
|
|
}
|
|
const out = formatReport(r)
|
|
expect(out.indexOf('SSH published')).toBeLessThan(out.indexOf('loopback bind'))
|
|
expect(overall(r)).toBe('fail')
|
|
})
|
|
|
|
it('prints every citation in full mode', () => {
|
|
const r: CapabilityReport = {
|
|
capability: 'X', scope: 's', examined: 1,
|
|
findings: [{
|
|
id: 'f1', summary: 'drift', determination: 'fail', severity: 'high',
|
|
evidence: [evidence.file('a.ts', 1), evidence.file('b.ts', 2)],
|
|
}],
|
|
}
|
|
expect(formatReport(r, { evidence: 'inline' })).toContain('(+1 more)')
|
|
const full = formatReport(r, { evidence: 'full' })
|
|
expect(full).toContain('a.ts:1')
|
|
expect(full).toContain('b.ts:2')
|
|
})
|
|
})
|
|
|
|
suite('roll-up', () => {
|
|
it('does not let healthy capabilities dilute an unrun one', () => {
|
|
const unrun: CapabilityReport = { capability: 'UnrunCheck', scope: 'none', examined: 0, findings: [] }
|
|
const out = formatRollUp([clean, unrun])
|
|
expect(out).toContain('NOT ASSESSED')
|
|
expect(rollUp([overall(clean), overall(unrun)])).toBe('not-assessed')
|
|
})
|
|
})
|