Skip to content

Commit 3e13ad4

Browse files
Krishcalinclaude
andcommitted
Ten frameworks that reached no screen
`modules/compliance_mapping.py` maps findings onto ISO/IEC 27001:2022, NIST CSF 2.0, NIST SP 800-53 Rev 5, SOX ITGC, DORA, CIS Controls v8, TISAX, SOC 2, EU GDPR and NERC CIP. Its only consumers were the offline HTML, PDF and PPTX generators — so a customer who works in the console and never exports a report saw none of it. NIST CSF had a screen because it has its own module; the other nine had nowhere to appear. So: /compliance, in the left pane above NIST CSF, and a posture strip on the dashboard that links to it. Live against the sample estate, 1,369 findings: ISO 39/39 controls flagged, NERC CIP 15/15, GDPR 3/3 over 871. NO PERCENTAGE ANYWHERE, and a screen is exactly where one gets invented — as a bar, a donut, a "9 of 12 green". The module forbids one and says why: this product reads configuration exports, not the control environment, so a "% compliant" would be a claim about evidence it does not hold. Compliance.test.tsx renders the page and asserts the text carries no \d+% and no <progress>. A FRAMEWORK WITH NOTHING MAPPED IS SHOWN, NOT DROPPED, and says "that is not a statement that its controls are met — only that nothing this scan produced is evidence against them". Dropping it would leave a reader unable to tell "we map this and found nothing" from "we do not map this at all", which is the distinction the whole module is built around. THE DENOMINATOR IS OURS AND THE PAGE SAYS SO. "1 of 15 mapped controls flagged" means fifteen controls THIS PRODUCT MAPS, not fifteen of the hundreds ISO 27001 contains. The two readings differ by an order of magnitude and only one is true. The endpoint carries the caveat in its payload as well, so an API consumer that never opens the screen still gets the sentence with the numbers. One test of mine was wrong in a way worth recording: it banned the word "percentage" from the page source and failed on the page's own comment explaining why there is none — a test forbidding the vocabulary of its own rule. The claim worth holding is about OUTPUT, so it now lives in the console test, where output exists. NOT BUILT: CISA. The request is ambiguous between CISA's Cross-Sector Cybersecurity Performance Goals, which would slot in like NERC CIP, and CISA KEV, which this product already consumes as exploited-note provenance. Left for the asker rather than guessed at. 7 new console tests, 4 new server tests. 4885 Python and 139 frontend pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 47174c2 commit 3e13ad4

11 files changed

Lines changed: 520 additions & 5 deletions

File tree

frontend/src/App.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { CrqInputs } from './routes/CrqInputs'
1515
import { DomainDetail } from './routes/DomainDetail'
1616
import { Domains } from './routes/Domains'
1717
import { TopRisks } from './routes/TopRisks'
18+
import { Compliance } from './routes/Compliance'
1819
import { Csf } from './routes/Csf'
1920
import { CsfFunction } from './routes/CsfFunction'
2021
import { PathDetail } from './routes/PathDetail'
@@ -76,6 +77,7 @@ export default function App() {
7677
{/* /domains/:id takes a domain id, so it has no nav entry: it is
7778
reached from the twelve tiles on /domains. */}
7879
<Route path="/domains/:id" element={<DomainDetail />} />
80+
<Route path="/compliance" element={<Compliance />} />
7981
<Route path="/csf" element={<Csf />} />
8082
{/* /csf/:fn takes a Function id, so it has no nav entry: it is
8183
reached from the six Function tiles on /csf, and from the CSF

frontend/src/api/client.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import type {
3232
CheckIndexEntry, ChokepointsView,
3333
ServiceRequest, SeveringSet, Coverage, CrqControlsView,
3434
CrqParametersView, CrqQuantifyResult, CrqTrendPoint, CsfFunctionView,
35+
ComplianceView,
3536
CsfView, Dashboard, DomainsView, FindingDetail, FindingFilters, TopRisksView,
3637
FindingHistory, FindingPage, GeneratedPassword, Health, Journey, Landscape,
3738
Me, PathView, PathsOverview, RequirementDoc, ResolvedView, RiskView,
@@ -504,6 +505,15 @@ export function crqTrend(limit = 12): Promise<{ points: CrqTrendPoint[] }> {
504505
return get<{ points: CrqTrendPoint[] }>(`/crq/trend?limit=${limit}`)
505506
}
506507

508+
// ══ Compliance posture, across every framework ══════════════════════════════
509+
//
510+
// Ten frameworks that reached no screen until this existed: the mapper's only
511+
// consumers were the offline HTML, PDF and PPTX generators, so a customer who
512+
// reads the console and never exports a report saw none of it.
513+
export function compliance(): Promise<ComplianceView> {
514+
return get<ComplianceView>('/compliance')
515+
}
516+
507517
// ══ Top risks, per domain ═══════════════════════════════════════════════════
508518
//
509519
// A DIFFERENT QUESTION FROM `findings()`, which answers "what is worst in the

frontend/src/api/types.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1126,6 +1126,41 @@ export interface Measured {
11261126
stale: boolean
11271127
}
11281128

1129+
/** modules/compliance_mapping.py — findings mapped onto a control framework.
1130+
*
1131+
* NO PERCENTAGE EXISTS ON THIS TYPE, deliberately. The module forbids one:
1132+
* this product reads configuration exports, not the control environment, so a
1133+
* "% compliant" would be a claim about evidence it does not hold. */
1134+
export interface ComplianceControl {
1135+
id: string
1136+
name: string
1137+
themes: string[]
1138+
crit: number
1139+
high: number
1140+
med: number
1141+
low: number
1142+
total: number
1143+
}
1144+
1145+
export interface ComplianceFramework {
1146+
id: string
1147+
name: string
1148+
subtitle: string
1149+
controls: ComplianceControl[]
1150+
/** Controls carrying at least one finding, out of the controls THIS PRODUCT
1151+
* maps — never out of the framework's own total, which is a different
1152+
* denominator entirely. */
1153+
controls_flagged: number
1154+
total_controls: number
1155+
mapped_findings: number
1156+
}
1157+
1158+
export interface ComplianceView {
1159+
frameworks: ComplianceFramework[]
1160+
findings_considered: number
1161+
note: string
1162+
}
1163+
11291164
export interface CsfView {
11301165
measured: Measured | null
11311166
framework: string

frontend/src/lib/nav.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {
2-
Calculator, CircleAlert, ListOrdered, CircleDollarSign, Landmark, LayoutDashboard, LayoutGrid, Scissors, ShieldCheck, TrendingUp, Upload, UserCog, Waypoints, type LucideIcon,
2+
Calculator, CircleAlert, ListOrdered, ScrollText, CircleDollarSign, Landmark, LayoutDashboard, LayoutGrid, Scissors, ShieldCheck, TrendingUp, Upload, UserCog, Waypoints, type LucideIcon,
33
} from 'lucide-react'
44
import type { Role } from '../api/types'
55

@@ -48,6 +48,10 @@ export const NAV_MAIN: NavItem[] = [
4848
// all sit in one domain.
4949
{ to: '/top-risks', label: 'Top5Risk', icon: ListOrdered },
5050
{ to: '/crq', label: 'Quantify Risk', icon: Calculator },
51+
// Every framework this product maps. NIST CSF keeps its own entry below
52+
// because it has its own module and a Function-level screen; the other
53+
// nine had nowhere to appear until this existed.
54+
{ to: '/compliance', label: 'Compliance', icon: ScrollText },
5155
{ to: '/csf', label: 'NIST CSF', icon: Landmark },
5256
{ to: '/upload', label: 'Upload', icon: Upload, minRole: 'analyst' },
5357
]
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
/**
2+
* Compliance posture, across every framework this product maps.
3+
*
4+
* THE RULE THIS SCREEN EXISTS UNDER, and the one a screen is most likely to
5+
* break: a control carrying findings has open gaps, and the ABSENCE of findings
6+
* against a control is not an assertion of compliance with it. This product
7+
* reads configuration exports, not the control environment.
8+
* `modules/compliance_mapping.py` forbids a percentage in as many words — and a
9+
* page is exactly where one gets invented, as a progress bar, a donut, or a
10+
* "9 of 12 green". None of those may appear, and these tests say so.
11+
*
12+
* THE OTHER TRAP IS THE DENOMINATOR. "4 of 15 controls flagged" means four of
13+
* the fifteen controls THIS PRODUCT MAPS, not four of the hundreds ISO 27001
14+
* contains. The two readings differ by an order of magnitude and only one is
15+
* true, so the page states which.
16+
*/
17+
import { render, screen } from '@testing-library/react'
18+
import userEvent from '@testing-library/user-event'
19+
import { MemoryRouter } from 'react-router'
20+
import { beforeEach, describe, expect, it, vi } from 'vitest'
21+
22+
const compliance = vi.fn()
23+
24+
vi.mock('../api/client', () => ({
25+
compliance: (...a: unknown[]) => compliance(...a),
26+
ApiError: class ApiError extends Error {
27+
status: number
28+
constructor(status: number, message: string) { super(message); this.status = status }
29+
},
30+
}))
31+
vi.mock('../lib/title', () => ({ useTitle: () => {} }))
32+
33+
import { Compliance } from './Compliance'
34+
35+
const NOTE = 'A control carrying findings has open gaps. The absence of '
36+
+ 'findings against a control is NOT an assertion of compliance with it.'
37+
38+
function control(over: Record<string, unknown> = {}) {
39+
return {
40+
id: 'A.8.8', name: 'Management of technical vulnerabilities',
41+
themes: ['Vulnerability & patch management'],
42+
crit: 3, high: 7, med: 2, low: 0, total: 12,
43+
...over,
44+
}
45+
}
46+
47+
function framework(over: Record<string, unknown> = {}) {
48+
return {
49+
id: 'iso27001', name: 'ISO/IEC 27001:2022', subtitle: 'Annex A controls',
50+
controls: [control()], controls_flagged: 1, total_controls: 15,
51+
mapped_findings: 12,
52+
...over,
53+
}
54+
}
55+
56+
function view(frameworks: unknown[]) {
57+
return { frameworks, findings_considered: 1369, note: NOTE }
58+
}
59+
60+
function draw() {
61+
return render(<MemoryRouter><Compliance /></MemoryRouter>)
62+
}
63+
64+
beforeEach(() => { vi.clearAllMocks() })
65+
66+
describe('the compliance posture screen', () => {
67+
it('lists every framework, not only the ones with findings', async () => {
68+
// Dropping an empty framework leaves a reader unable to tell "we map this
69+
// and found nothing" from "we do not map this at all".
70+
compliance.mockResolvedValue(view([
71+
framework(),
72+
framework({ id: 'gdpr', name: 'EU GDPR', controls: [],
73+
controls_flagged: 0, total_controls: 3, mapped_findings: 0 }),
74+
]))
75+
draw()
76+
expect(await screen.findByText('ISO/IEC 27001:2022')).toBeInTheDocument()
77+
expect(screen.getByText('EU GDPR')).toBeInTheDocument()
78+
})
79+
80+
it('refuses to call an unmapped framework compliant', async () => {
81+
compliance.mockResolvedValue(view([
82+
framework({ id: 'gdpr', name: 'EU GDPR', controls: [],
83+
controls_flagged: 0, total_controls: 3, mapped_findings: 0 }),
84+
]))
85+
draw()
86+
expect(await screen.findByText(/not a statement that\s+its controls are met/))
87+
.toBeInTheDocument()
88+
})
89+
90+
it('states the caveat before any number is read', async () => {
91+
compliance.mockResolvedValue(view([framework()]))
92+
draw()
93+
expect(await screen.findByText(/gap map, not a\s+certification/))
94+
.toBeInTheDocument()
95+
expect(screen.getByText(new RegExp('NOT an assertion of compliance')))
96+
.toBeInTheDocument()
97+
})
98+
99+
it('says whose denominator it is', async () => {
100+
// "4 of 15" is four of the controls WE map. Read as four of ISO's
101+
// hundreds it would be a wildly different claim.
102+
compliance.mockResolvedValue(view([framework()]))
103+
draw()
104+
expect(await screen.findByText(/not of everything the framework contains/))
105+
.toBeInTheDocument()
106+
expect(screen.getByText(/1 of 15 mapped controls flagged/))
107+
.toBeInTheDocument()
108+
})
109+
110+
it('shows no percentage anywhere', async () => {
111+
// The module forbids one. A screen is where it gets invented.
112+
compliance.mockResolvedValue(view([framework()]))
113+
const { container } = draw()
114+
await screen.findByText('ISO/IEC 27001:2022')
115+
expect(container.textContent).not.toMatch(/\d+\s?%/)
116+
expect(container.querySelector('progress')).toBeNull()
117+
})
118+
119+
it('shows the controls behind a framework on request', async () => {
120+
compliance.mockResolvedValue(view([framework()]))
121+
draw()
122+
await userEvent.click(await screen.findByRole('button',
123+
{ name: /Show the 1 control carrying findings/ }))
124+
expect(screen.getByText('A.8.8')).toBeInTheDocument()
125+
expect(screen.getByText('Management of technical vulnerabilities'))
126+
.toBeInTheDocument()
127+
// The themes are shown, because they are why the control was flagged.
128+
expect(screen.getByText('Vulnerability & patch management')).toBeInTheDocument()
129+
})
130+
131+
it('draws a zero severity count as a dash rather than a nought', async () => {
132+
// A column of noughts reads as a measurement. This one means "none of
133+
// this severity", which the dash says without implying precision.
134+
compliance.mockResolvedValue(view([
135+
framework({ controls: [control({ crit: 0, high: 0, total: 4 })] }),
136+
]))
137+
draw()
138+
await userEvent.click(await screen.findByRole('button', { name: /Show the 1/ }))
139+
expect(screen.getAllByText('—').length).toBeGreaterThanOrEqual(2)
140+
})
141+
})

0 commit comments

Comments
 (0)