|
| 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