CCoE conformance: OID identification, CBOM releases, attestation register and policy packs - #2
Open
rajnishprecogs wants to merge 32 commits into
Open
CCoE conformance: OID identification, CBOM releases, attestation register and policy packs#2rajnishprecogs wants to merge 32 commits into
rajnishprecogs wants to merge 32 commits into
Conversation
Foundations for the JLR CCoE PQC CBOM requirements register. GEN-004 — authoritative algorithm identifiers. data/algorithm-oids.json carries IANA/NIST CSOR/ANSI/IETF OIDs for PQC (FIPS 203/204/205), classical and composite signature algorithms, plus curves and mode-specific AES. src/utils/oid-registry.js resolves scanner-emitted names to them, flags draft/provisional standardisation status as GEN-004 requires, and marks a resolution approximate rather than guessing silently. PQC entries carry "verify": true — cross-check against the live CSOR before issuing artefacts. Service model — JLR assesses conformance per service, not per requirement: 18 cross-cutting requirements x 7 services + 42 service-specific = 168 assessable pairs, 124 of them Must Have. src/compliance/service-profiles.js generates that grid, scores it, and ranks requirements by weighted cells unlocked. Validated against JLR's own Service Matrix. Cross-service consistency checks (XS-001..XS-010) reconcile a declaration made under one service against evidence from another — an algorithm declared "not implemented" but observed in use, a PQC implementation declared hardware where evidence indicates a software stub, a CA key claiming HSM protection with no resolvable artefact. Only computable where discovery and declaration share one model, which is what CA-SVC asks for. Attestation register — 25 requirements cannot be answered by scanning and 22 more need a declaration to complete a discovered fact. data/attestation-schemas.json defines the field set per requirement so a declaration is structured and checkable; src/attestation validates it, tracks staleness against the GEN-007 cadence, and renders into the native CycloneDX 1.6 `declarations` object (assessors/attestations/claims/evidence) rather than a vendor extension. Provenance and confidence are mandatory on every declaration — telling a discovered fact from an asserted one is what the register is for. 27 tests added (92 total, all passing). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
GEN-009 is a Must Have across all seven services and asks for eight things per infrastructure component: (a) host key algorithms with sizes, (b) permitted user auth types, (c) key exchange including PQC and hybrid, (d) ciphers, (e) MACs, (f) implementation and version, (g) PQC KEX status, (h) host key storage with an HSM cross-reference. (a)-(f) come from discovery — live SSH_MSG_KEXINIT enumeration and sshd_config parsing were already there but produced findings, not a structured declaration. (g) and (h) cannot be observed and come from the GEN-009 attestation schema. Two JLR constraints drive the design. "SSH host keys must be declared per infrastructure component, a single blanket declaration is not acceptable" — so buildSshProfile throws without a component, and summarise() reports per-component rather than collapsing to one answer. "SSH host keys that are software-stored rather than HSM-resident must be flagged as a risk item" — so software-stored storage always produces a HIGH risk finding. Deliberate restraint on what is not knowable: an RSA host key algorithm name does not encode modulus size, so keyBits is null and keyBitsDetermined false rather than a guessed 2048. An unparseable banner is undetermined, not assumed. Undeclared and unobserved (g)/(h) stay undetermined. PQC key exchange is recognised for ML-KEM hybrids (OpenSSH 10), sntrup761x25519 (OpenSSH 8.5+) and the OQS/Amazon Kyber variants, and the profile distinguishes ratified ML-KEM from non-standardised NTRU Prime — PQC-002 requires that distinction. Observed PQC outranks a declaration and records the contradiction when a supplier declared it unavailable. CA-004 deprecation flagging covers all four algorithm classes and names the standards that deprecate each, including the three JLR calls out explicitly: ssh-rsa with SHA-1, diffie-hellman-group1-sha1 and 3des-cbc. network-scanner now returns the negotiated cipher and MAC name-lists; they were computed for weak-algorithm findings and then discarded. 26 tests added (118 total, all passing). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
GEN-005 requires each CBOM release to carry a semantic version and a signed change log of additions, removals and modifications. Cited by UNECE R155 Annex 5 and ISO/SAE 21434 Clause 12; JLR's rationale is "required for UNECE R155 audit evidence" — the change log is what a type-approval auditor reads. Kept separate from output/diff.js, which is a CI regression gate answering a different question (does this scan introduce weak crypto?). Two correctness properties drive the design. Identity is semantic, not textual. The engine mints a fresh uuid for any component whose finding carries no stable id, so bom-ref alone is not a key — a re-scan would read as "everything removed and recreated". bom-ref is used only when unique on both sides; everything else pairs on a content identity of assetType + name + location (+ subject/issuer for certificates, type for key material and protocols). Mutable attributes are deliberately excluded: an initial version hashed parameterSetIdentifier and mode into the identity, which made every genuine update read as a removal plus a creation. Tests for re-ordering, uuid churn, line movement and path-separator differences all assert zero delta. An unreachable source is not a removal. Components from a source a scanner could not reach are `stale` and prior state is retained. Recording them as removed would let an outage silently delete assets from JLR's register. Versioning is stated so an auditor can check it: major for a removal or weakened posture (both change what the CBOM asserts about risk), minor for new assets, patch for metadata, and no bump at all when nothing changed — an unchanged estate should not manufacture releases. The change log carries a canonical digest computed over a key-sorted serialisation excluding the signature slot, so signing is deterministic and tampering is detectable. The signer is injected rather than hardcoded, and a classical signature is explicitly marked gen006Compliant:false with a note — GEN-006 requires ML-DSA-65 at minimum and a classical signature must never be silently presented as satisfying it. 27 tests added (145 total, all passing). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CLM-SVC was the only one of JLR's seven services with no discovery path at all — five service-specific requirements plus eighteen cross-cutting ones resting on nothing. It is also the incumbent competitor's home ground, which made a visible blank there the most expensive one on the board. Adapters for Venafi TLS Protect (TPP), Keyfactor Command and EJBCA/PrimeKey. EJBCA is worth first-class support for an automotive customer: it is widely deployed for V2X and vehicle PKI, which is the on-board half of the estate. Every adapter maps its own API shape onto one normalised inventory, and the requirement profiles read only the normalised form. CLM-001, CLM-003 and CLM-004 are therefore answered identically whatever the platform — the consistency that the cross-cutting requirements are scored on. Adding a fourth platform is one adapter and nothing else. The map* functions are pure, taking a parsed API response, so the field mapping — where the bugs actually are — is tested without a live platform. Field names follow each vendor's documented shapes and carry an explicit warning to verify against the deployed version during onboarding: a silently mis-mapped field yields a confidently wrong CBOM, which is worse than an absent one. Deliberate restraint on what is not known. A missing certificate count is `countDetermined: false`, never zero — zero would understate migration scope, and CLM-001 exists to communicate that scope. Partial counts are reported as incomplete rather than presented as a total. CLM-003 is never complete from discovery alone, because JLR requires the PQC revocation size impact for constrained V2X and embedded environments to be declared. CLM-004 resolves allow and block lists to algorithm OIDs as JLR requires rather than leaving them as names, reports anything unresolvable, and implements XS-005: an algorithm both permitted by policy and deprecated under CNSA 2.0 / SP 800-131A Rev 2 / BSI TR-02102 is a governance contradiction, not merely a weak setting. Metadata only throughout — no connector requests private key material or has the capability to. 28 tests added (173 total, all passing). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Consumes the CycloneDX 1.6 output of the on-premise PKCS#11 agent and turns one scan into five requirement profiles: HSM-001 (PQC support), HSM-004 (key agreement and HNDL), PCAM-001 (physical inventory), PQC-007 (hardware PQC readiness) and KMS-001 (managed key types). The more important output is the **artefact identity**. PKI-005, CS-003, KMS-003 and KMS-007 each require a CA key, signing key or key tier to cross-reference "the HSM CBOM artefact", and XS-001/002/003 fire when that pointer does not resolve. The agent mints a fresh CBOM serialNumber and fresh bom-refs on every scan, so neither can be the reference — the id is derived from the device serial number, falling back to the token label. A test asserts XS-001 resolves cleanly against a real artefact id and still fires against a missing one. Two bugs the tests caught. A naive trailing-digits split parsed ML-DSA-65 as "ML-DSA with a 65-bit key"; PQC names carry their parameter set as part of the identifier, as do hash and MAC names, so both families are now kept whole. An HSM with neither serial nor label has no stable identity at all, which is now a HIGH finding rather than a silent unresolvable reference. Restraint where PKCS#11 cannot answer. Hardware-vs-firmware execution is not observable through the mechanism list, so HSM-001 reports `undetermined` until attested. KMS-001 states that supported-but-unused key types need the mechanism list rather than presenting provisioned types as the whole inventory. PCAM-001 reports make, model and firmware as missing rather than guessing. Material findings beyond conformance: a private key marked CKA_EXTRACTABLE undermines the HSM protection claim at PKI-005 and CS-003 regardless of algorithm strength, and AES-128 is flagged as non-compliant for new provisioning under CNSA 2.0. Known gap, tracked separately: the Java agent does not yet call C_GetTokenInfo, so manufacturer, model, firmware version and serial number are unavailable from discovery and must be attested. That is a small agent change and would complete PCAM-001 from discovery alone. 23 tests added (196 total, all passing). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…false Running the real Java agent output through the ingest — rather than a hand-written fixture — exposed a contract gap. The agent emits no per-key PKCS#11 attributes at all, so `hsm:sensitive` and `hsm:extractable` were absent, defaulted to false, and every correctly configured private key raised a false HIGH finding. Absent is not false. Both attributes are now tri-state, findings fire only on a reported value, and an agent that does not report them raises one INFO noting the HSM protection claim at PKI-005 and CS-003 is unevidenced — a coverage gap, not a compliance failure. The fixture is now genuine CbomBuilder output rather than an approximation of it, which is what made the difference. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
GEN-006 requires the CBOM artefact to be signed with a PQC-capable scheme, ML-DSA-65 at minimum, and permits hybrid during transition. It is a Must Have across all seven services — 14 weighted cells, the largest single item in the register. JLR's rationale: "an unsigned CBOM cannot be trusted." Until now signing used RSASSA-PKCS1-v1_5-SHA256. Signing a post-quantum inventory with a Shor-vulnerable signature is the kind of thing an evaluator notices; output/sign.js is retained for the existing CLI path but is not GEN-006 conformant and the change log already said so explicitly. Implementation notes worth knowing: @noble/post-quantum 0.6.1 is ESM-only and this engine is CommonJS, so the module loads through a cached dynamic import. Signer *construction* is therefore async while sign() and verify() stay synchronous, which keeps cbom-release.signChangeLog(log, signer) synchronous as already written and tested. The 0.6 API is sign(msg, secretKey) — the argument order changed from earlier releases, and getting it wrong fails loudly rather than silently. Pure JavaScript, no native addon, so the pkg binaries still build — that is what answers CA-005: hand JLR's auditor a binary and let them verify our CBOM themselves rather than trusting the SaaS. The GEN-006 floor is enforced rather than documented: ML-DSA-44 is refused by both generateKeyPair and createSigner, and a key whose length does not match its declared parameter set is rejected before it can produce a signature that would never verify. Tests assert the FIPS 204 sizes directly — 1952 / 4032 / 3309 for ML-DSA-65. Hybrid signs the same digest with both components and requires both to verify. A hybrid that passes on one component alone offers no more assurance than that component by itself, and there is a test forging exactly that case. The composition is declared as concatenation with standardisationStatus 'non-standardised' — PQC-002 forbids presenting a non-standardised construction as settled, and explicitly rejects vague claims of hybrid support. Signatures are over a canonical key-sorted digest, so reordering a document cannot invalidate a signature and altering it always does. Both directions are tested. 17 tests added (215 total, all passing). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Groundwork for CycloneDX 1.7. `specVersion` was a hardcoded '1.6' in the CBOM
and QBOM generators and the validator loaded one schema by filename, so
supporting another version meant editing three places and hoping nothing else
had its own copy.
GEN-001 imposes two different obligations, and conflating them is how a CBOM
tool ends up emitting something it cannot validate:
EMIT — only versions whose official schema is bundled.
INGEST — 1.4 upward, because GEN-001 exists to stop legacy SBOM data being
lost, and our RFI response commits to ingesting 1.4 and 1.5.
The load-bearing rule: a version with no bundled schema can be ingested but
never emitted. Emitting a document labelled 1.7 while holding no 1.7 schema
would assert a conformance nobody has checked, which is worse than not
offering the version at all. `resolveEmitVersion('1.7')` therefore throws
today, and will start working the moment schemas/bom-1.7.schema.json exists —
no code change.
Validation is now per declared version rather than always-1.6, and reports
`validated: false` for a version whose schema is absent. Unvalidated and
invalid are different claims: conflating them either overstates conformance or
rejects a perfectly good document.
1.4 and 1.5 are refused for emit for a second, separate reason — they have no
cryptographic asset model at all. That is the substance of clarification C4 to
JLR: GEN-001 cites 1.4, but cryptoProperties, assetType and algorithmProperties
were upstreamed in 1.6.
A version newer than this build knows is still ingested, unvalidated, rather
than discarded.
Caught by the tests: the qbom generator took the new specVersion field but a
regex patch silently failed to add the resolver call, so it emitted
`specVersion: undefined` and four schema tests failed. Worth noting because
the failure mode — a document that looks fine until something validates it —
is exactly what this module exists to prevent.
15 tests added (230 total, all passing).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CycloneDX 1.7
Fetched the official bom-1.7 schema, plus cryptography-defs.schema.json — 1.7
splits algorithmFamiliesEnum and ellipticCurvesEnum into a side schema, and
without it the 1.7 validator will not compile at all. The validator now loads
whichever side schemas are present rather than a fixed list, since the set is
version-dependent.
What 1.7 actually adds to the crypto model, having diffed it rather than
trusting our own RFI copy:
algorithmProperties + algorithmFamily, ellipticCurve
certificateProperties + serialNumber, fingerprint, certificateState,
creation/activation/revocation/destruction
dates, certificateExtensions
relatedCryptoMaterialProperties + fingerprint, relatedCryptographicAssets
protocolProperties + relatedCryptographicAssets
relatedCryptographicAssets is the one that matters here: it is a native way to
express the cross-asset references PKI-005, CS-003, KMS-003 and KMS-007
require, which we currently carry as vendor properties. Worth migrating onto
once 1.7 is the agreed target — noted, not done in this change.
1.7 is additive over 1.6, and a test asserts the same CBOM validates as both.
GEN-004 in output
The OID registry existed and was tested but nothing called it, so emitted CBOMs
carried no cryptoProperties.oid — the requirement was satisfied in the library
and not in the artefact. algorithmComponent now populates it, with
standardisation status as component properties since CycloneDX has no field
for it, and emits algorithmProperties.curve when resolution finds one.
Caught while wiring it: ECDSA-P256 resolved its algorithm OID but not its
curve, because the alias map held P-256 while the name splits to P256. Curve
lookup now normalises the hyphen-less and NIST-prefixed spellings, so P256,
NISTP256, nist-p-384, secp256r1 and prime256v1 all resolve. Scanners and
PKCS#11 emit all of these.
An unresolvable algorithm still gets no OID and is marked unresolved — a wrong
identifier is worse than an absent one, and GEN-004 is about authoritative
identification.
7 tests added (237 total, all passing).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The first release fell through to the "created" branch of nextVersion, so an opening inventory was versioned 0.1.0 and its reason described the assets as having been "added" — added to nothing. An *empty* first CBOM meanwhile hit the fallback and returned 1.0.0, so the two cases disagreed with each other. A register's first issue is its baseline, not a pre-release. Both now return 1.0.0 under a new `initial` bump, and the second release bumps from there. Three tests added: none of the existing 237 covered the first release, which is how this survived. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The built-in frameworks say what the industry requires. A customer's own cryptography standard is usually stricter and is the one their auditors test against, so a finding citing an external standard makes the reader do the mapping themselves. A pack expresses that standard as data: algorithm states and strengths, which cipher modes and RBGs are permitted, how long a strength stays valid, and what requires named approval. Findings then cite the customer's own clause. Packs are data rather than code for two reasons. A customer standard is normally confidential, so it must be deployable without being published in this repository — no pack ships here, and the tests use an invented policy. And a standard is reissued on its own review cycle, so it must be replaceable without a software release. Two decisions worth stating: - An algorithm the pack does not mention is `unknown`, never compliant. Silence in a policy is not permission, and an estate full of unrecognised algorithms is a finding about the policy rather than a clean result. - Approval rules read tags from the *policy's* entry as well as the finding's. A scanner has no concept of "post-quantum"; keyed only on finding-supplied tags, a PQC approval rule would never fire and the requirement would silently never be raised. Caught by running a real pack rather than a fixture. 24 tests; 264 total. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`evaluate` covers the industry frameworks; `evaluatePolicy` covers the customer's own standard. Both reachable from one module so a caller does not need to know the pack machinery exists. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… CycloneDX registry
Coverage of algorithm identification was 21 families, each hand-written. The
CycloneDX registry describes 96 families as data — names, variant patterns,
standards, curves — so it is vendored rather than transcribed, and compiled
rather than reimplemented.
Three layers, only the middle one hand-maintained:
data/registry/ upstream, vendored, never edited by hand
data/registry-bindings.json ours: errata, slot vocabularies, spec mapping
src/utils/crypto-registry.js the compiler both feed
Vendoring pins by sha256, not by the registry's own lastUpdated stamp: the
published copy (96 families) and master (98) carry an identical stamp with
different contents, so the stamp cannot identify what a CBOM was generated
against. The published copy is vendored because an attestation should cite a
release; `--source master` previews what is coming.
The variant patterns are a parser specification. One compiler serves every
family, giving family, primitive and parameter set without per-family code.
Two things the patterns do not supply on their own:
Slots are unbounded, so `HMAC[-{hashAlgorithm}]` would match almost anything
beginning HMAC-. Each slot is bound to a vocabulary, and the curve and hash
vocabularies are derived from the registry itself, so they track upstream.
Patterns overlap, so matches are ranked by how much of the name was matched
literally rather than absorbed by a slot. Names that stay ambiguous —
RSA-PKCS1-1.5 is both an encryption and a signature scheme — report their
alternatives rather than picking a winner silently.
Matching runs strict then loose, and reports which applied: HMAC-SHA-256 is
the registry spelling, HMAC-SHA256 is what people write, and a caller should
be able to tell the two bases apart.
Errata correct two defects in the published copy without touching its bytes,
which must stay verifiable against the published sha256. BLS13-381 does not
exist (the curves are BLS12-381 and BLS12-377, both listed elsewhere in the
same file) and the GOST standard is 28147-89, not 38147. Both are already
fixed upstream; a test fails when an erratum stops applying, so corrections
cannot outlive the defect.
Registry primitives are mapped per spec version — 1.7 added key-wrap, 1.6 has
no equivalent, so it degrades to `other` rather than emitting a document that
fails schema validation.
96 of 96 families identify across a 406-name corpus expanded from the
registry's own patterns. Algorithm OIDs remain overlay work.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The registry says what an algorithm is called. It says nothing about whether you should be using it. That assessment is the overlay — the only one of the three layers that is hand-maintained. Keyed by registry family identifier, which is what keeps it honest: a family renamed upstream orphans its entry and the drift test fails, rather than the family quietly losing its assessment while everything still appears to work. The overlay also records the sha256 of the registry copy it was built against, so the two cannot drift apart unnoticed. Every one of the 96 families carries a status and a quantum exposure: broken 11 deprecated 7 legacy 14 active 63 test-only 1 shor 21 grover 68 safe 5 depends 2 OIDs are resolved, not restated. The registry carries curve OIDs but no algorithm OIDs, and sets additionalProperties:false on the family object, so there is nowhere upstream to put one. Rather than open a second OID table to contradict data/algorithm-oids.json, each family declares how to resolve against the existing verified set: statically for a family with one OID, from the parameter set where the OID varies (AES by size and mode, ML-KEM by parameter set). 26 families resolve an OID today. The rest report the absence with a reason — an OID no authority has assigned cannot be cited, and the family identifier is the identifier of record in that case. This carries the RSA split. The registry has no RSA family; it has four, and the padding distinction the old single "RSA" entry collapsed is exactly the one that decides whether a finding is raised. PKCS#1 v1.5 encryption is deprecated (Bleichenbacher, ROBOT); the v1.5 signature scheme is merely legacy; OAEP and PSS are active. Selector semantics live here too, not in the compiler. The registry states that AES offers (128|192|256) — that the first choice means a key length is judgement, so the compiler reports selectors positionally and the overlay names them. A test asserts no family names more selectors than its patterns can produce, which caught 17 families whose key length and mode are open slots rather than enumerated choices. Detection patterns are declared but not yet populated; families are identified by name, not yet hunted for in source. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The registry was vendored complete but only half consumed: 246 curves arrived, 125 of them carrying an OID, and exactly one field was being read — the name, to build the pattern-matching vocabulary. Curve OID lookups still ran off the nine hand-written entries in data/algorithm-oids.json, so a certificate on brainpoolP512r1 or B-571 identified its algorithm and reported no curve. Curve resolution now runs off the vendored data. 125 of 125 assigned OIDs resolve, up from 16. The curve form comes with them — Montgomery, Edwards, TwistedEdwards or Weierstrass — which is what separates a curve used for key agreement from one used for signatures. All nine hand-written OIDs agree with the registry, so nothing needed overriding. A test asserts that agreement rather than leaving two sources of truth to diverge quietly. Two properties of the upstream data shaped the design: A curve registered by several bodies has several OIDs, all correct. P-224 is 1.3.132.0.33 under SECG and 2.23.43.1.4.12 under the WTLS arc. Precedence in the bindings decides which is reported, ranked by which identifier actually turns up in a certificate; the others stay on the result, since a certificate in the field may carry either. Aliases are cross-references between full entries, not shorthand for entries that do not exist — every alias but one is also a curve in its own right, in the group that named it. Resolution therefore has to agree on the OID whichever name is presented, and preserves the name it was given so a report can echo what the source actually said. Spellings the registry does not enumerate are normalised: P256, NIST P-256 and nistp256 all reach 1.2.840.10045.3.1.7. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…try table Detection and identification were conflated. The scanners find algorithm names in source competently — a known API call, a constant, a config value — but then resolved those names against a curated table of about fifty entries. Anything outside it fell through to a default of LOW risk with pqcVulnerable false. That default was the bug. ECIES, EC-ElGamal, SRP, ECMQV, J-PAKE, SM2 and BLS are all broken outright by Shor, and every one of them was reported as low-risk and quantum-safe — a false negative in the one claim the product exists to make. Names now resolve against the registry when the curated table has no entry, so the table's hand-tuned scores and notes still win where it has an opinion. Coverage of registry-generated names goes from 61 of 378 to 376. The two remaining are A5/1 and A5/2, lost before the registry sees them because the scanner's normaliser splits on "/" to handle Java transform strings such as AES/GCM/NoPadding; fixing that is a separate change with its own regression risk. Findings now record how the name was obtained, on an ordered ladder adapted from the identity priority ladder in the Enterprise CBOM Aggregation Engine design. That ladder ranks evidence for two components being the same asset; this one ranks evidence for a string being an algorithm. Same principle: first tier that applies wins, and weak tiers carry a confidence score and a review flag rather than being discarded. 1 api-call-exact 1.00 registry spelling at a known call site 2 api-call-normalised 0.90 real-world spelling at a known call site 3 api-constant 0.85 4 config-value 0.75 5 import-inferred 0.60 available, not necessarily used 6 string-literal 0.50 review 7 identifier-name 0.30 review — a name can outlive its algorithm 8 ambiguous 0.25 review — more than one family accepts it Quantum exposure now raises the risk floor: an active but Shor-vulnerable algorithm grades MEDIUM rather than SAFE, because harvest-now-decrypt-later makes it a scheduled problem rather than a solved one. Registry matching gains a third and last pass for names that differ only in separator spelling — CTR-DRBG for the registry's CTR_DRBG. Which separator a name uses carries no meaning and tooling rewrites them freely. The pass is reported as `respelled`, never as an exact match. Appendix A is now generated from the vendored registry rather than asserting a count in prose, and cites the sha256 of the copy it was generated from. A test fails when the committed copy goes stale. npm run appendix regenerate npm run appendix:check fail if stale Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The registry carries a standards reference with a DOI or publisher URL for almost every family, and nothing read it. identify() has been returning the citations since the compiler landed and they went straight in the bin. They are what separates a claim from evidence. The appendix asserted that a family is deprecated, or falls to Shor, on Precogs' say-so; an assessor's reasonable next question is "says who?". Each family now carries the document it is defined by, taken from the vendored registry and passed through unmodified — so the answer is upstream attribution rather than our word. Variant-level citations are included alongside family-level ones, because several families define individual modes in separate documents: AES-GCM is SP 800-38D, not FIPS 197, and AES alone cites fifteen standards across its modes. Folding in the variant citations also recovered GOST, leaving A5/1 as the only family with no upstream attribution at all — stated in the appendix rather than left as an unexplained blank. A new A.7 lists all 108 referenced standards with their URLs, and says plainly that none of them is added, edited or supplied by us. Tests assert every attributed family shows its attribution, every referenced URL appears in A.7, and that A5/1 remains the single documented exception — so a family losing its citation upstream fails the build rather than quietly appearing unsourced. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…and fix the AES tiebreak Three identification gaps, found by testing our coverage against the alias table in the Unified-CBOM-Engine rather than assuming we would win. Bare family names did not identify at all. ML-KEM, ML-DSA, SLH-DSA, XMSS, Argon2, SHA-2 and RSASSA-PSS all mandate a parameter set in their patterns — ML-KEM-(512|768|1024) admits no bare ML-KEM — but a CBOM component routinely carries the family in one field and the parameter set in another, so ingesting a vendor document lost them silently. A fourth and weakest match pass resolves a name that is exactly a family. It reports its own `family-name` mode and invents no parameters, and grades at its own tier: the family is established, the parameters that decide its strength are not, and AES-128 and AES-256 are not the same finding. Pre-standardisation names did not identify either. The registry knows ML-KEM; code written between 2019 and 2024 says Kyber. Names are now rewritten before matching, with parameter sets translated where the mapping is exact (Dilithium3 -> ML-DSA-65). The alias is reported on the result rather than swallowed, because the lineage is not equivalence — FIPS 203 ML-KEM differs from round-3 Kyber in ways that matter for interoperability. A bare AES resolved to primitive `key-wrap`. Every AES variant makes its distinguishing group optional, so all four tie on literal weight, and the tiebreak then compared *declared* slots — by which measure the key-wrap variant looks tighter than the block cipher merely because it declares fewer. Compare filled slots instead, since an optional the name never exercised cost the pattern nothing, then prefer a family whose name is exactly the subject, then fall back to registry declaration order. That last ordering also settles ChaCha20, which both the ChaCha20 and ChaCha families accept; the family actually called ChaCha20 now wins. It flips the bare RSA-PKCS1-1.5 to the signature reading, which is neither better nor worse — the name is irreducibly ambiguous and both readings are still reported — so the round-trip test's exclusion set records the pair rather than a winner. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…omits The registry carries ML-KEM, ML-DSA and SLH-DSA and nothing else from the post-quantum field. Falcon, HQC, Classic McEliece, BIKE, FrodoKEM and NTRU are all absent — verified against the vendored copy — so a scan of a PQC-experimental codebase reported them as unidentified. That is a blind spot exactly where this tool should be strongest, and it is the first real cost of the decision to vendor upstream rather than hand-maintain. The overlay could not absorb it: the overlay is keyed by registry family, so a family upstream does not have has nowhere to live. data/registry-extensions.json is therefore a fourth layer, and deliberately the smallest — six families, each mirroring the registry's own shape so the same parser, compiler and vocabulary machinery serves them with no special-casing. Every extension is marked. The flag survives onto the identification result, `families()` excludes them, and coverage counts registered families only, so nothing that claims registry coverage can quietly include them. A test fails if an extension ever collides with a registered family, which is what will happen when upstream adds one — the signal to delete the extension rather than let both survive a refresh. Two of the six are not NIST selections and say so in their assessment: BIKE and NTRU are marked legacy, sound but off the standardisation path. FrodoKEM stays active because BSI and ANSSI recommend it precisely for avoiding the algebraic structure ML-KEM depends on. Bare `Falcon` also joins the pre-standardisation alias table, since FIPS 206 is still in draft and both spellings are in use. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The emit path predated the registry work and still scraped what it needed out of
the algorithm name. Two consequences, one of them a shipped defect:
parameterSetIdentifier was extracted with /-(\d{3,4})/, which on
RSA-PKCS1-1.5-SHA-256-2048 matched the *hash* and reported 256 as the key
length. It now comes from the registry's extracted parameters, so that name
emits 2048.
algorithmFamily was never emitted at all — grep found it only in a comment —
despite the registry computing exactly that. It is now emitted on 1.7, along
with ellipticCurve, and never on 1.6, where algorithmProperties forbids unknown
fields. Primitives map through primitiveForSpec, so key-wrap degrades to `other`
on 1.6 rather than producing a document that fails validation.
Three constraints in the 1.7 schema shaped this, all found by validating the
output rather than assuming it:
Cipher mode is a closed lowercase enum. The registry knows XTS, CTS, OCB, SIV,
KW and KWP; CycloneDX knows none of them, so anything outside the enum
degrades to `other`. The precise mode survives on the component properties.
ellipticCurve qualifies a curve by its standards group — brainpoolP512r1 is
written brainpool/brainpoolP512r1. The registry carries the group, so the
qualified form is constructed rather than guessed.
algorithmFamily is a closed 93-entry enum, and the registry published
alongside it defines 96. ANSI-KDF, TLS-PRF and SP800-56C are therefore
families a conformant 1.7 document cannot name, as are the Precogs
extensions. Those algorithms are still identified, still assessed and still
emitted as components; the one constrained field is omitted rather than
filled with a value that would fail validation.
Both enums are read from the schema this repo ships, so they cannot drift from
what actually validates.
Tests emit one component per registry family and validate the result against
both schemas, so a future version-gating mistake fails the build rather than
producing a document a customer's validator rejects.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…cribing it The hand-written registry appendix was wrong in six places, and every one was a transcription error rather than a mistake of understanding: BLS(13-381|13-377|BN254) no BLS13 curve exists — our errata already fix it GOST38147 the standard is GOST 28147-89 — likewise AES: four standards the registry cites fifteen across its modes GOST: no standards it cites RFC4357 through a variant Ed25519-ph, Ed448-ctx the pattern is Ed(25519|448)[(ph|ctx)] — no hyphen CBOM example unversioned algorithmFamily is a 1.7 field; 1.6 rejects it The appendix was therefore less accurate than the engine it described, since the engine corrects the first two at load time. Generating it from the vendored file makes all six impossible: patterns come from the parsed registry with errata applied, standards from the registry's own citations, worked examples from expanding each pattern and confirming the name identifies back to the same family, and the CBOM example from actually running the emit path. Precogs extensions are listed separately and stated plainly as not being part of the registry, so no count above them can be read as including them. Two emit defects surfaced while generating the example, both pre-existing: classicalSecurityLevel parsed the key size out of the raw name, so RSA-PKCS1-1.5-SHA-256-2048 found none and fell to the 80-bit floor. It now takes the key length the registry extracted, and reports 112. cryptoFunctions came from the scanner's coarse category, so a signature scheme was described as encrypt/decrypt. It now follows the registry primitive. The old key-exchange branch also emitted `keyDerive`, which the CycloneDX enum rejects — it is lowercase `keyderive` — so any key-exchange finding produced a document that failed validation. Tests pin each of the six defects and re-verify every name the appendix calls valid by identifying it independently. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Seven defects in the certificate path, four found by reading it and three only by running a scan against a live endpoint and looking at what came out. All seven were silent: wrong or missing data in the artefact, no error anywhere. The policy pack was assessing no certificates at all. `assess()` opens with `const algorithm = f.algorithm || f.name; if (!algorithm) continue;` and a certificate finding carried neither — it has `signatureAlgorithm` and `publicKeyAlgorithm`. Nothing downstream supplied them: `aggregate()` does not, and `applyRuleScore()` only reads `finding.algorithm`. So every certificate hit the `continue` before classification, was not even recorded in `unknown[]`, and a certificate estate assessed against JLR-STN-12418 returned `assessed: 0` with no violations — which reads as a clean pass. This is the failure mode the `available` flag was added to prevent, arriving through a different door. EC certificates were never checked for key strength. The rule tested `publicKeyAlgorithm === 'RSA'`, and `summarizeCert` left `publicKeySize` null for EC, so RSA-1024 scored CRITICAL/92 while P-192 — weaker — scored LOW/10. Curves are now mapped to equivalent security strength, because a curve's field size is not its strength: comparing 256 against an RSA threshold would condemn every P-256 certificate. New `publicKeyStrength` field, comparable across families. `pqcVulnerable` tested a hardcoded RSA/EC/DSA list, but `summarizeCert` could only ever return RSA, EC or 'unknown' — forge does not structure DSA or Ed25519 keys — so the DSA arm was dead and an unidentified key type was silently treated as not quantum-vulnerable. The algorithm now falls back to the SubjectPublicKeyInfo OID, and the test is a named set so an unrecognised family is a visible omission rather than an implicit pass. Certificates had no identity. `updateCorrelations` keys entirely on `f.fingerprint` and no scanner has ever set one, so correlation was dead code: the same certificate uploaded twice, found in a repository and served by an endpoint produced three unrelated findings, and key reuse was undetectable. Every certificate now carries SHA-256 over its DER, applied in all three places certificates are produced so the identity is the same wherever it was found. Found by running it, not by reading it: - Node's peer-certificate object has no `sigalg` field, so `cs.sigAlg && /sha1|md5/` was always false and a network endpoint serving a SHA-1 certificate was never flagged. `cert.raw` is present, so the served certificate now goes through the same parser the file scanners use — one parser, one field set, and an endpoint's certificate is directly comparable with the same certificate on disk. - The emitter derived the certificate's encoding with `path.extname(finding.file)`, and a network finding's `file` is `host:port`, so `precogs.ai:443` was written into the CBOM as encoding `ai:443`. A certificate inlined in a config file became `yaml`-encoded. Encoding now comes from the scanner's own `format`, is `der` for anything discovered over the wire, and falls back to the filename only when the extension is a real certificate encoding. - `notValidBefore` was absent from every network certificate. The fingerprint is emitted as CycloneDX `hashes` rather than a `cbom:` property — it is the standard location, so a tool reading the BOM can match certificates without knowing our conventions. Without this the identity existed only on the internal finding and the exported artefact, which is what an auditor actually receives, still could not be de-duplicated. Key size, curve, strength, expiry and the self-signed and CA flags go in properties, because CycloneDX `certificateProperties` has no field for any of them. Scores change: EC certificates below 112-bit equivalent strength move to CRITICAL, and endpoints serving SHA-1-signed certificates are flagged for the first time. Both are corrections. Two gap documents record what is still missing across the certificate and network probes, including two header claims that overstate the code — there is no STARTTLS handling despite the module claiming "STARTTLS awareness on 389", and the certificate chain is fetched and discarded despite the header promising it. Neither is fixed here. 18 tests; 282 total. Output validates against the official CycloneDX 1.6 schema and emits identically at 1.6 and 1.7. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016EqC9sbjYFQijhSebThTWu
…s variants as ambiguity Both found by running the identification path against a representative finding from each discovery surface rather than against names the registry generates. Unqualified RSA key material did not identify at all. The registry models RSA by scheme — RSAES-OAEP, RSAES-PKCS1, RSASSA-PKCS1, RSASSA-PSS — and carries no bare RSA family, but a certificate, an HSM or a key scanner reports "RSA-2048" and says nothing about padding, because padding is a property of how a key is used rather than of the key. So the most common finding in any estate returned null, and a null identification means no enrichment, which means `pqcVulnerable` stays false. Every RSA key would have reported as quantum-safe. RSA is therefore a registry extension, marked like the others. Where the scheme IS stated the registry families still match first, so this only fires for the unqualified name. Separately, every AES finding was being demoted to the `ambiguous` review tier. The ambiguity rule fired whenever a name matched more than one *variant*, but every AES name satisfies the block-cipher, AEAD, key-wrap and MAC patterns at once — that is one family described four ways, not a contest. Ambiguity now requires two different families, so AES-256-GCM is exact and RSA-PKCS1-1.5, which really is both an encryption and a signature scheme, stays ambiguous. Tests assert one finding per discovery surface — certificate, key, endpoint, vault, firmware, source — identifies with the right family and quantum posture. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…m/PrecogsAI/precogs-ai-cbom into feat/ccoe-conformance-attestation
A missing intermediate is the most common TLS misconfiguration there is, and it is invisible from the leaf alone — which was the only thing either probe looked at. `getPeerCertificate(true)` has always returned the chain as a linked list and `summarizePeerCert` has always read element zero and discarded the rest, so an endpoint serving a leaf with no intermediate produced a clean certificate finding and nothing else. Walking that list needs care that a first attempt would not survive: Node terminates the chain by pointing the root at *itself* rather than at null, so the obvious loop never ends. `flattenPeerChain` stops on self-reference, on a repeated fingerprint, and on a depth cap, in that order — the last two also cover a server presenting a deliberately cyclic or pathological chain, which is not hypothetical for a scanner pointed at an untrusted host. Every certificate the endpoint presents is now its own finding carrying `chainDepth`, with `chainLength` on the leaf. Intermediates are inventoried rather than scored: an asset register that records only leaves is incomplete, but an intermediate's weak key is the CA's problem, not this endpoint's, and scoring it against the endpoint would misattribute it. Expiry is the exception, because an expired intermediate breaks this endpoint today. Chain completeness moved out of pki-scanner into utils/cert-chain, because it is the same fault wherever the certificates came from — a keystore, a PEM bundle, a PKCS#12 file or a handshake. The certificate probe now runs it on any upload yielding more than one certificate, so uploading a leaf without its intermediate is caught at the point of upload rather than only during a repository scan. One certificate is deliberately not enough to trigger it: a single certificate is not a set, nothing about a chain can be concluded from it, and flagging it would fire on every single-cert upload. This is completeness, not validation. No signature is verified and no trust store is consulted, so the findings say "incomplete" and never "untrusted". An untrusted root remains undetected and both gap documents say so. Plain LDAP was reporting as a clean result. Port 389 is cleartext by default and TLS arrives only via StartTLS, which this scanner does not implement, so the direct handshake failed and produced the generic LOW/10 "no TLS handshake succeeded" — indistinguishable from a host that simply is not there, for what may be an unencrypted directory. It now yields an explicit cleartext finding at MEDIUM/55 stating that StartTLS support is *undetermined*: the scanner cannot negotiate it, so it cannot rule it out either. The module header claimed both "STARTTLS awareness on 389" and that it captured the certificate chain. Neither was true. The chain claim is now true; the STARTTLS one is gone, and the header states what the probe does not do — no cipher-suite enumeration, no host-key fingerprint, no trust validation. A header that overstates the code is worse than no header, because a reader has no way to tell which sentences to believe. One item planned for this change is deliberately absent. The SSH host-key fingerprint was scheduled as wiring, and it is not: the probe sends only the version banner and reads the server's KEXINIT, while the host key arrives in the KEX reply, which requires sending our own KEXINIT and completing a key exchange. That is protocol implementation and belongs with the cipher-enumeration work. 12 tests; 294 total. Verified against a live endpoint: precogs.ai:443 presents precogs.ai -> YR2 -> Root YR -> ISRG Root X1, complete, no gap reported. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016EqC9sbjYFQijhSebThTWu
… handshake Reporting the suite a default handshake settled on answers a question nobody asked. A server that *prefers* AES-GCM but still *accepts* 3DES looks clean, and "what will this endpoint agree to" is the question a TLS assessment exists to answer. Enumerating properly turned out to be blocked in a way that is worth recording, because it dictates the whole shape of this change. `tls.connect` can only offer what the local OpenSSL implements, and OpenSSL 3 removed RC4, single DES, 3DES, EXPORT, NULL and every anonymous suite: `tls.getCiphers()` on this build returns none of them. The library route therefore cannot ask about precisely the suites a scanner most needs to ask about. A server accepting RC4 would have been reported clean, and no amount of iterating over `tls.getCiphers()` would have found it. So scanners/tls-clienthello.js builds the ClientHello by hand. Only the handshake's first flight is needed — offer a suite list, read the ServerHello, close — so no keys are derived, no certificate is validated and no cryptography of our own is implemented. The cost of that independence is that we now own the byte layout, which is why the module documents what a ServerHello does and does not prove: a selected suite means accepted, a handshake_failure alert means not accepted, and anything else is `unknown` and stays `unknown` rather than being folded into "not accepted" where it would read as a clean result. Enumeration offers everything, notes what the server picks, removes it and repeats — one handshake per accepted suite plus one, rather than one per candidate. Re-offering the accepted list reversed answers a second question for one extra connection: whether the server applies its own preference or takes the client's. That distinction is the finding, not trivia. A server honouring client order negotiates the weakest suite any client asks for, so its own ordering protects nobody. With a single accepted suite, preference is reported as null — undetermined, because one suite says nothing about ordering. data/tls-cipher-suites.json catalogues 58 suites by IANA wire value. `fs` and `aead` are separate flags because a CBC suite is neither broken nor good, and those two are what separate a merely working configuration from a sound one. STARTTLS follows, for SMTP, IMAP and POP3, on the enumeration path as well as the handshake — each enumeration round is its own connection, so the upgrade is re-driven every time. Two things this got wrong first: - The protocol was identified from the port. These services routinely run somewhere other than 25 or 143, and keying on the well-known port reports those as having no TLS at all. It now reads the greeting. - POP3 greets with "+OK", which is also its STLS success reply, so testing the success pattern against the greeting reported the upgrade as complete before STLS had been sent. The state machine always starts at `greeting` now, and the comment says why so it does not get "simplified" back. LDAP's StartTLS is deliberately absent: it is a BER-encoded extended request rather than a text command and does not fit this shape, so 389 stays reported as a cleartext port with an undetermined upgrade path. SSH server-to-client cipher and MAC lists are now scanned. The two directions are configured independently, so a weak cipher on the return path alone was invisible. One finding per algorithm listing every directive that offers it, rather than one per direction — a symmetric configuration is the normal case and would otherwise double every finding. Also fixed: SNI was being set to an IP literal, which RFC 6066 forbids and Node warns about. Enumeration is on by default, because a cipher inventory that silently omits what the server would agree to is the wrong default; `enumerateCiphers: false` turns it off. Measured at 1.5s for a two-version endpoint accepting three suites each. Still absent, and stated in the header rather than left to be discovered: no SSH host-key fingerprint (the key arrives in the KEX reply, which needs a full key exchange), no revocation, no trust validation, and no SSLv2/SSLv3 — Node cannot negotiate them. 18 tests; 312 total. Enumeration and STARTTLS are tested against servers started inside the test process, so the suite stays offline and the accepted suites and preference behaviour are known in advance rather than asserted loosely. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016EqC9sbjYFQijhSebThTWu
Checked against the KTSecure PKCS#11 agent's own CbomBuilder, which documents its property names as "the contract the platform's HSM ingest reads". Two things were wrong on this path. PKCS#11 does not name algorithms the way the registry does. CKK_EC is reported as "EC", which is no registry family — the registry separates ECDSA from ECDH and PKCS#11 does not, because on a token that distinction is CKA_SIGN and CKA_DERIVE rather than the key type. So every EC key in an HSM went unidentified. The usage flags now decide it, and where the agent reported none, ECDSA stands as the more common case and is marked approximate; both readings fall to Shor and share the id-ecPublicKey OID, so the approximation costs a label and nothing that affects a migration decision. More seriously, the HSM path emitted only *posture* findings — an extractable private key, an unreported CKA_SENSITIVE — which are statements about how the device is configured. The keys themselves never appeared as cryptographic assets: no inventory entry, no CBOM component, no quantum posture. An RSA-2048 signing key in a Luna is the same asset as an RSA-2048 key found in a vault and now reads that way, through a new `assets` result shaped like every other scanner's output so the platform can feed it through the same posture pipeline. Keys now carry algorithmFamily, algorithmOid, algorithmStatus and curveOid — secp256r1 resolves to 1.2.840.10045.3.1.7 — with the regex classifier kept as the fallback for token types the registry does not recognise, so an unusual key still gets a posture rather than silently reading as safe. hsm:hardwareVersion was being dropped. C_GetTokenInfo reports hardware and firmware versions separately and PCAM-001 asks about the physical asset: a device can be re-flashed without the board changing. hsm:curve and hsm:objectClass are now read as well, alongside the standard CycloneDX fields that already carried them. No agent code here, and none needed — this is the receiving half. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Closes the manual-submission half of FR2. A supplier sends the CBOM for the ECU they ship, an air-gapped site hands over a file, another tool exports what it found — all of it now lands in the same inventory as our own scans without ever becoming indistinguishable from them. Three rules, and they are the whole design. Provenance is not optional. Every imported finding carries the sha256 of the document, the tool that wrote it and when it arrived, and `detection: imported` so a supplier's claim can never render as something we measured. Ids derive from the document hash and bom-ref, so re-importing updates rather than duplicates. Their names are re-identified, not trusted. A foreign CBOM says whatever its tool prefers; running every name through the registry is what makes a supplier's document comparable with our scans rather than merely adjacent to them. The KTSecure PKCS#11 agent's EC keys are the interesting case — "EC" is no registry family and only the usage flags say whether it signs or agrees — so the HSM profile's resolver is reused rather than reimplemented. Their grading is their policy and is not adopted. Where the document asserts an OID and we resolve a different one, both are kept and the disagreement is reported. Either their tool is wrong or we mean different things by the same name, and neither is visible if one is silently preferred. Round-tripping our own emitter caught the design mistake worth recording: names were being reassembled from algorithmFamily and parameterSetIdentifier, which turns AES-256-GCM into AES-256 — and AES-256 resolves to the CBC OID, so the importer reported a disagreement with a document we had just written. Candidate names are now tried most-faithful-first, the whole component name ahead of any reconstruction, and placeholder parameter sets like "default" are not concatenated into nonsense. What the document could not say is reported rather than defaulted: components read, cryptographic assets found, how many carried no location, which names went unidentified. CycloneDX 1.4 and 1.5 predate cryptoProperties entirely, so those are accepted with the reason stated — importing an SBOM silently and reporting zero findings would read as "this supplier uses no cryptography". This does not merge. Correlating two CBOMs of the same asset is a different problem with its own engine. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…n' into feat/ccoe-conformance-attestation
…ick the version FR3 asks for JSON and XML. Only JSON existed — there was no XML anywhere in the engine — and the CLI could only emit 1.6, so "1.6 and 1.7" was reachable from the library but not from the command line. XML is a serialisation of the same document rather than a second generation path, which is what makes the two equivalent in content rather than merely similar. The JSON BOM stays the source of truth; XML is produced from it and parsed back into it, so "no data is lost" is a claim that can be checked. It is: a test round-trips a CBOM through JSON to XML to JSON on both 1.6 and 1.7 and asserts the structures are identical. Three CycloneDX XML rules the writer encodes, because each produces a document that looks right and fails every validator: Some fields are attributes, not elements — type and bom-ref on a component, name on a property, serialNumber and version on the bom. Arrays are wrapped, and the singular is not always the plural minus an "s": dependencies wraps dependency. A property carries its value as element text, not as a value element. An empty array emits an empty wrapper rather than being dropped, so it returns as [] and not undefined. "No dependencies" and "we did not say" are different statements and CycloneDX can express both. Import accepts either format, detected from content rather than filename, because an air-gapped transfer arrives named whatever somebody typed. The provenance hash is taken over the bytes that arrived rather than a converted form, so it stays checkable against the file the supplier actually sent. The CLI gains --spec-version and --bom-encoding. Passing --spec-version 1.7 also fixed a smaller inconsistency: the QBOM did accept a version and was never given one, so a 1.7 CBOM was emitted beside a 1.6 QBOM — two documents about one scan disagreeing about their own model. xml2js was already present transitively and is now a declared dependency, since the engine imports it directly. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two claims in the FR3 response were not true. Both are now. "CycloneDX is the native internal representation, not an export-time conversion." The internal model is findings[] and CycloneDX is generated at output, so as an architectural statement this was simply wrong — and it would have stayed wrong short of a rewrite that made the working model worse. What the claim is reaching for is that nothing is lost either way, and that is now true and tested: a findings → CBOM → findings round trip dropped 9 of 18 fields, and drops none. The fields CycloneDX has no home for — the line column, the risk grade, the description and remediation text, how confidently the algorithm was identified — travel as precogs:finding:* properties and are restored on import. A foreign CBOM carries none of them and is unaffected: it is still re-identified, still re-graded, still marked as imported with an id derived from the document. The distinction is exactly the one that matters — our own document coming home is not a supplier making a claim. Note detection is restored rather than overwritten. How an algorithm was found is a fact about the scan that produced it and stays true after the document travels; that this record arrived by import is recorded separately in `imported`, so both are stated rather than one displacing the other. "SPDX SBOM ingestion." Not implemented — the spdx.schema.json in the repo is only the licence-expression enum CycloneDX $refs. Now implemented for SPDX 2.2, 2.3 and 3.0 JSON-LD. SPDX has no cryptography profile in any version; 3.0.1's Security profile covers vulnerabilities, not cryptographic assets. So an SPDX document cannot describe an algorithm, a key or a certificate, and every import says so — not only when the result is empty, because a document that silently yielded zero cryptographic findings would read as "this supplier uses no cryptography". What it does yield is real discovery: the packages, with the cryptographic implementations among them flagged against the library table. Knowing an ECU ships OpenSSL 3.0.1 says which implementation is present. The tag-value serialisation is declined by name rather than mis-parsed as JSON. src/ingest/index.js dispatches on content rather than filename, so one entry point takes CycloneDX or SPDX, JSON or XML, and an air-gapped transfer named whatever somebody typed still lands. supportedFormats() advertises what each format can carry, with cryptographicAssets machine-readable rather than only in prose. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…to 41
The OID column sat at 26 of 96 and I had described that as close to the real
ceiling. Half of that was wrong. Of the 70 families without an identifier, 25
had one assigned by some authority and simply were not in our verified table —
a gap of ours rather than of the world's — and only 45 genuinely have none.
The table had been built around what a Western enterprise codebase typically
hits, and never extended to the national arcs. That matters for an automotive
supply chain: SM2, SM3 and SM4 are mandatory in the Chinese market, and ARIA and
SEED in the Korean one, so a JLR supplier scan would have reported the region's
standard cryptography with no authoritative identifier at all.
Every value here came from a primary specification and was corroborated against
a second source before being written down.
ShangMi GM/T 0002/0003/0004, corroborated via OpenSSL's object database:
member-body(156) -> ISO-CN, 10197 -> oscca, 1 -> sm-scheme
ARIA RFC 5794 Appendix B, corroborated by OpenSSL
SEED RFC 4010, corroborated by OpenSSL
PKCS#5 PBKDF2, PBES2 and PBMAC1 under 1.2.840.113549.1.5
Legacy CAST5, IDEA, RIPEMD-160, Whirlpool, Camellia-CBC, scrypt, HKDF
One correction worth recording for whoever reads RFC 5794 next: its ASN.1
module header says korea(400), which is a typo. The operative id-algorithm
definition three lines later says korea(410), which matches Korea's ISO
member-body code, SEED's arc and every implementation. The OIDs here use 410.
SM9 is deliberately left without one. GM/T 0044 assigns it under the OSCCA arc,
but it appears in neither an RFC nor the OpenSSL database, and an unverified
identifier in a compliance artefact is worse than an absent one — the family
identifier remains the identifier of record, which is what GEN-004 accepts.
The remaining 55 are mostly modern designs that ship as RFCs and IETF registries
rather than OID arcs: bcrypt, Argon2, BLAKE3, SPAKE2, X3DH, HPKE, ChaCha,
Poly1305 and the 3GPP set. For those the absence is the fact, not the gap.
A test pins 41 as a floor, so losing any of these fails the build.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Builds out the engine against the JLR CCoE cryptographic requirements register:
OID identification, CBOM versioning and signing, per-service conformance
profiles, an attestation register, and organisational policy packs.
What changed
Identification and schema
standardisation status carried alongside (
GEN-004). OIDs are now emitted inCBOMs rather than resolved and discarded.
the official schemas (
GEN-001). A version with no bundled schema can beingested but never emitted — we do not sign artefacts we cannot validate.
Release management
versioning, canonical-digest change log and stale-source handling (
GEN-005).Reordered or re-serialised CBOMs produce no delta.
halves must verify (
GEN-006).Conformance
cross-service consistency checks.
integrity, review workflow and staleness, rendering to the native CycloneDX
declarationsobject.GEN-009clauses (a)–(f)from discovery and (g)–(h) from declaration.
Policy packs
and strengths, permitted cipher modes and RBGs, strength validity dates, and
approval requirements. Findings then cite the customer's own clause rather
than an external standard the reader has to map onto it.
Notes for review
confidential and are supplied by the deployment; the tests use an invented
policy. This is deliberate, not an omission.
1.0.0under a newinitialbump, not0.1.0. An opening inventory is a baseline, not apre-release, and an empty first CBOM already returned
1.0.0— the twocases disagreed. Three tests added; none of the previous 237 covered it.
undeterminedrather thanfalse. Defaulting tofalseproduced confidentHIGH findings against correctly configured keys. Caught by running a real
agent rather than a fixture.
finding's. A scanner has no concept of "post-quantum", so a rule keyed only on
finding-supplied tags would never fire and the requirement would silently
never be raised.
Testing
264 tests, all passing (
npm test). New suites cover the OID registry, serviceprofiles, attestation, SSH profiles, CBOM release and diff, PQC signing, spec
versions, CLM, HSM profiles and policy packs.