Skip to content

Fix #35 (registries): migrate the framework registries off the swapped ids - #111

Merged
emmanuelgjr merged 3 commits into
mainfrom
fix/registry-prose-ids
Sep 18, 2026
Merged

emmanuelgjr merged 3 commits into
mainfrom
fix/registry-prose-ids

Conversation

@emmanuelgjr

Copy link
Copy Markdown
Contributor

Stacked on #110 — base is fix/swapped-control-ids, so review that one first. Together they close #35.

Why

extract-registry.js seeded data/frameworks/ from the mapping rows while the parser had the columns the wrong way round. 573 registry items across ten frameworks ended up with a requirement sentence as their identifier:

{ "control_id": "Hijacked agent with excess privilege causes larger blast radius",
  "title": "NHI-5 Over-Privileged NHI" }

These ids ship in the npm package, docs/frameworks-registry.js and the OSCAL catalogs.

What this does

Each item is re-read with the same grammar the parser now uses — the identifier comes out of the title, the remainder becomes the title, the description is kept. Nothing is invented:

  • items that already had a proper id are untouched, and win on the title when a converted item collapses onto them;
  • 14 CIS safeguards that mapping rows cite but the registry lacked are added, titled from the row;
  • registry ids also accept the CIS parent controls (CIS-16) that no mapping row cites, since the registry legitimately holds both levels;
  • ISO Annex B (B.2) is recognised alongside Annex A;
  • header rows that the double-table bug turned into registry items are dropped.
Framework Before After Framework Before After
SOC 2 209 46 EU AI Act 145 30
OWASP NHI Top 10 104 10 ISO/IEC 42001 87 56
CWE/CVE 76 61 PCI DSS v4.0 72 49
CIS Controls v8.1 70 68 NIST SP 800-82 59 26
OWASP AI Testing Guide 58 9 NIST SP 800-218A 52 12

Registry controls 1,541 → 991. Registry items 1,685 → 1,120.

The count falls because the removed entries were artefacts, not content: the seeding produced one registry item per mapping row, where the framework has one control. The OWASP AI Testing Guide has nine categories, not 58; the NHI Top 10 has ten entries, not 104. The requirement text those items carried is not lost — it lives in each mapping row's notes after #110.

Dangling references from mappings into registries: 153 → 1 (one FedRAMP row, unrelated to #35).

Guards

  • checkControlIdShapes() now validates registry ids as well as mapping ids.
  • The prose-id baseline in exports.test.mjs is emptied, not lowered, so no framework is exempt any more and any sentence-shaped id in an OSCAL export fails the build.

Verification

  • node scripts/validate.js: 0 errors, 87 warnings, 326 passed.
  • npm run test:scripts: 85 passed, 0 failed, twice.
  • npm run stats:check current; generator reruns clean.

For your attention

  1. A headline count moves. data/stats.json drops from 1,541 to 991 registry controls, and the webapp renders that from the data layer, so it updates by itself. But the About timeline still contains the historical line "Registry items typed by kind — 1,372 controls" from v4.0. It is a dated record of what shipped then, so I have not touched it (C2). Tell me if you want a new timeline row noting the correction.
  2. 345 U+FFFD replacement characters remain in 15 registry files — the encoding guard from T-ACC06 covers Markdown only. Titles I rewrote here are clean; the descriptions are not. Worth its own issue and fix.
  3. 35 mapping rows still name themselves after their id (mostly PCI Req 11.3), because neither the table nor the registry has a title for those ids. Filling them needs the published requirement text, not a guess.

🤖 Generated with Claude Code

emmanuelgjr and others added 2 commits September 18, 2026 04:33
`parseControlTable()` tried `looksLikeId()`, which rejects every identifier
containing a space — `Req 6.2.4`, `Art. 9`, `§5.3` — then fell through to
"column 0 is the id". On 611 rows that wrote the requirement prose into
`control_id` and the identifier into `control_name`, so an id could not be
followed back to its framework and the control-level join resolved by luck.

scripts/control-ids.js holds a per-framework identifier grammar for the ten
frameworks affected. A row is scanned for its framework's own id shape,
wherever that sits: the id becomes `control_id`, the rest of that cell the
name, and the requirement moves to `notes`. When the table has no name column
the name comes from the framework registry's title for that id rather than
repeating the id. Canonical forms follow the ruling on #35 — §5.3, Art. 9,
Req 6.2.4, NHI-5, bare ISO clauses, SP 800-218A keeping NIST's -PS — with CIS
identified by safeguard and the control carried as `parent` (A), and the SP
800-53 rows inside the 800-82 files kept and labelled `id_source` so they are
not read as 800-82 sections (B).

Two parser bugs surfaced on the way:
  - a section can hold more than one table, and the second table's header row
    was parsed as data. Splitting on the |---| separators drops those 10 junk
    rows and gives each table its own headers, which is what lets the SP
    800-53 rows keep their titles.
  - two-column tables lost the requirement text entirely once the id stopped
    being mis-filed; it now becomes `notes`.

Prose-shaped ids across the corpus: 611 -> 0. Mappings 3,781 -> 3,771, the
difference being those 10 header rows. 969 rows changed id; no row was
removed, and no severity, relationship or confidence changed.

checkControlIdShapes() in validate.js fails the build if any of the 1,337
rows in those frameworks stops carrying a well-formed identifier.

The registries were seeded from these broken rows and still hold prose ids;
migrating them changes headline control counts and the webapp, so it follows
separately.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…d ids

extract-registry.js seeded data/frameworks/ from the mapping rows while the
parser had the columns the wrong way round, so 573 registry items across ten
frameworks carried a requirement sentence as their `control_id` and the real
identifier in `title`:

  { "control_id": "Hijacked agent with excess privilege causes larger blast
     radius", "title": "NHI-5 Over-Privileged NHI" }

Nothing is invented here. Each item is re-read with the same grammar the
parser now uses, so the identifier comes out of the title, the remainder
becomes the title, and the description is kept. Items that already had a
proper id are left alone and win on the title when a converted item collapses
onto them. 14 CIS safeguards cited by mappings but missing from the registry
are added, titled from the row. Registry-side ids also allow the CIS parent
controls (CIS-16) that no mapping row cites.

  SOC 2                209 -> 46     EU AI Act            145 -> 30
  OWASP NHI Top 10     104 -> 10     ISO/IEC 42001         87 -> 56
  CWE/CVE               76 -> 61     PCI DSS v4.0          72 -> 49
  CIS Controls v8.1     70 -> 68     NIST SP 800-82        59 -> 26
  OWASP AI Testing      58 ->  9     NIST SP 800-218A      52 -> 12

Registry controls 1,541 -> 991; registry items 1,685 -> 1,120. The count falls
because the duplicates were artefacts: one prose item per mapping row, where
the framework has one control. Dangling references from mappings into the
registries: 153 -> 1 (a FedRAMP row, unrelated to #35).

validate.js now checks registry ids as well as mapping ids, and the prose-id
baseline in exports.test.mjs is emptied rather than lowered, so no framework
is exempt any more.

Found on the way, not fixed here: 345 U+FFFD replacement characters remain in
15 registry files. The encoding guard added by T-ACC06 covers Markdown only.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
# Conflicts:
#	scripts/control-ids.js
#	scripts/validate.js
@emmanuelgjr
emmanuelgjr merged commit e004f89 into main Sep 18, 2026
6 checks passed
@emmanuelgjr
emmanuelgjr deleted the fix/registry-prose-ids branch September 18, 2026 13:47
emmanuelgjr added a commit that referenced this pull request Sep 18, 2026
…m the published standards (#115)

* Fix #112: recover 328 replacement characters in the registries, and guard data files

Fourteen registry files carried 328 U+FFFD replacement characters, mostly
where an em dash belongs. They ship: the registries go out in the npm
package, in docs/frameworks-registry.js and in the OSCAL/STIX exports.

Every character is recovered from the source text rather than assumed. The
registries were extracted from the mapping rows, so for each corrupted string
the text either side of the character is looked up in the Markdown and the
entry JSON, and the character that sits between them is read off. A character
is taken only when every match agrees; the window narrows from 24 to 8
characters when a description concatenates several rows, and keeps the raw
slice when only one word follows, since trimming to a word boundary could
otherwise empty it.

All 328 resolved, every one to U+2014. None was guessed, and no other
character was found.

The guard from T-ACC06 only ever read Markdown, which is why this sat
unnoticed. It now also reads data/frameworks/*.json, data/entries/*.json and
data/*.json, checking for U+FFFD alone — the arrow heuristic is tuned for
prose and diagrams, not JSON. Negative-tested: a replacement character
injected into soc2.json fails validation, naming the file and line.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Title the 19 registry entries that carried only their identifier, from the
published standards

After #110 and #111, 35 mapping rows still named themselves after their id —
"Req 11.3 | Req 11.3" — because neither the mapping table nor the registry
held a title for those 19 ids. The text now comes from the standards
themselves, transcribed rather than written.

PCI DSS, 17 requirements: the headings as printed in PCI SSC's own "PCI DSS
v4.0 SAQ D for Merchants", extracted with pdftotext from the published PDF so
the wording is the document's, not a paraphrase. Req 11.3 reads "External and
internal vulnerabilities are regularly identified, prioritized, and
addressed."

SOC 2, PI1.3 and P7.1: TSP section 100, 2017 Trust Services Criteria (with
Revised Points of Focus — 2022). The AICPA download needs a session, so the
wording was taken from two independent copies of that document and compared;
both agree character for character.

Only entries whose title was empty or equal to their own id were filled, so
no curated title is overwritten. Rows with no published title available were
left alone — there are none left.

Mapping rows whose control_name is merely their control_id: 35 -> 0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Swapped control_id / control_name: 576 mapping rows hold prose where an identifier belongs

1 participant