From 8b28c620ecdcfdb5b16b10012eedc82dbc282d72 Mon Sep 17 00:00:00 2001 From: Eduardo Contreras Date: Mon, 28 Sep 2026 18:01:44 -0600 Subject: [PATCH] Fix the broken ADSB-AT-01 link and the aireplay-ng command rendering RFSAM-ADSB-AT-01 was listed as existing in the coverage map with no control file, so the roadmap linked to a 404. It is now planned, and validation fails when the map and the controls directory disagree. The aireplay-ng --test command in RFSAM-RES-11 is now in code formatting, so its two hyphens are no longer rendered as a dash. --- scripts/validate.mjs | 23 +++++++++++++++++++++++ scripts/validate.test.mjs | 13 ++++++++++++- src/content/resources/rfsam-res-11.md | 2 +- src/data/coverage-map.js | 2 +- 4 files changed, 37 insertions(+), 3 deletions(-) diff --git a/scripts/validate.mjs b/scripts/validate.mjs index 3d99bc1..568e074 100644 --- a/scripts/validate.mjs +++ b/scripts/validate.mjs @@ -65,6 +65,25 @@ export function checkTool({ data, file }, reg) { return errs; } +// The coverage map and the controls directory must agree: an entry marked +// 'existing' has a control file, and every control file is listed in the map. +export function checkCoverage(coverageMap, controlIds) { + const errs = []; + const mapped = new Set(); + for (const p of coverageMap) { + for (const c of p.controls ?? []) { + mapped.add(c.id); + const has = controlIds.has(c.id); + if (c.status === 'existing' && !has) errs.push(`coverage-map: ${c.id} is 'existing' but has no control file`); + if (c.status !== 'existing' && has) errs.push(`coverage-map: ${c.id} is '${c.status}' but a control file exists`); + } + } + for (const id of controlIds) { + if (!mapped.has(id)) errs.push(`coverage-map: control ${id} is not listed`); + } + return errs; +} + function idsFromDir(dir, field) { const out = new Set(); for (const f of readdirSync(dir).filter((f) => f.endsWith('.md'))) { @@ -88,10 +107,14 @@ export async function runValidation() { const dir = 'src/content/controls'; const files = readdirSync(dir).filter((f) => f.endsWith('.md') && !f.startsWith('_')); const all = []; + const controlIds = new Set(); for (const f of files) { const { data, content } = matter(readFileSync(join(dir, f), 'utf8')); + controlIds.add(data.id); all.push(...checkControl({ data, body: content, file: f }, reg)); } + const { coverageMap } = await import('../src/data/coverage-map.js'); + all.push(...checkCoverage(coverageMap, controlIds)); // Toolchain integrity: every referenced tool slug must exist, and every // hardware tool's `software` slugs must exist. diff --git a/scripts/validate.test.mjs b/scripts/validate.test.mjs index 2eb60f6..ce48ea3 100644 --- a/scripts/validate.test.mjs +++ b/scripts/validate.test.mjs @@ -1,6 +1,6 @@ import { test } from 'node:test'; import assert from 'node:assert/strict'; -import { checkControl, checkTool } from './validate.mjs'; +import { checkControl, checkTool, checkCoverage } from './validate.mjs'; const registries = { bsamKeys: new Set(['BSAM-EN-01']), @@ -92,3 +92,14 @@ test('a successor must resolve and is only accepted with eol or stale', () => { test('lifecycle fields without a status are rejected', () => { assert.match(checkTool(tool({ statusNote: 'x' }), registries)[0], /status is not/); }); + +test('coverage map and control files must agree', () => { + const map = [{ id: 'BLE', controls: [ + { id: 'RFSAM-BLE-SP-01', status: 'existing' }, + { id: 'RFSAM-BLE-AT-02', status: 'planned' }, + ] }]; + assert.deepEqual(checkCoverage(map, new Set(['RFSAM-BLE-SP-01'])), []); + assert.match(checkCoverage(map, new Set())[0], /'existing' but has no control file/); + assert.match(checkCoverage(map, new Set(['RFSAM-BLE-SP-01', 'RFSAM-BLE-AT-02']))[0], /a control file exists/); + assert.match(checkCoverage(map, new Set(['RFSAM-BLE-SP-01', 'RFSAM-BLE-LL-09']))[0], /is not listed/); +}); diff --git a/src/content/resources/rfsam-res-11.md b/src/content/resources/rfsam-res-11.md index a8c2eb0..dfaa123 100644 --- a/src/content/resources/rfsam-res-11.md +++ b/src/content/resources/rfsam-res-11.md @@ -2,4 +2,4 @@ id: RFSAM-RES-11 title: Wi-Fi monitor-mode capture and survey --- -Put a monitor-mode-capable adapter (e.g. ALFA AWUS036ACH / RTL8812AU, or ESP32-C6 Minino) into monitor mode, channel-hop across 2.4/5/6 GHz, and capture all 802.11 frames. Confirm injection support with aireplay-ng --test before relying on active controls. Add GPS for wardriving/geolocation of APs. +Put a monitor-mode-capable adapter (e.g. ALFA AWUS036ACH / RTL8812AU, or ESP32-C6 Minino) into monitor mode, channel-hop across 2.4/5/6 GHz, and capture all 802.11 frames. Confirm injection support with `aireplay-ng --test` before relying on active controls. Add GPS for wardriving/geolocation of APs. diff --git a/src/data/coverage-map.js b/src/data/coverage-map.js index 8181438..a6f3fbd 100644 --- a/src/data/coverage-map.js +++ b/src/data/coverage-map.js @@ -70,7 +70,7 @@ export const coverageMap = [ { protocol: 'ADSB', controls: [ { id: 'RFSAM-ADSB-PHY-01', title: 'Message capture and decode', layer: 'PHY', status: 'existing', objective: 'Assess capture and decoding of ADS-B messages.' }, { id: 'RFSAM-ADSB-LL-01', title: 'Message authenticity assessment', layer: 'LL', status: 'existing', objective: 'Assess what authenticity guarantees, if any, the link provides.' }, - { id: 'RFSAM-ADSB-AT-01', title: 'Forge and inject (contained lab)', layer: 'AT', status: 'existing', objective: 'Assess resilience to forged/injected ADS-B frames (authorised, contained-lab testing only).' }, + { id: 'RFSAM-ADSB-AT-01', title: 'Forge and inject (contained lab)', layer: 'AT', status: 'planned', objective: 'Assess resilience to forged/injected ADS-B frames (authorised, contained-lab testing only).' }, ]}, { protocol: 'NR5G', controls: [ { id: 'RFSAM-NR5G-SP-01', title: 'Cell identification and capture', layer: 'SP', status: 'existing', objective: 'Assess identification and capture of the target 5G NR cell.' },