feat: volledige afhankelijkheidsgraaf, supplier-veld, en een verouderingspoort die kan falen #545

Merged
brenno merged 4 commits from feat/sbom-diepte into main 2026-07-22 12:01:55 +00:00
Owner

Summary

Closes #517 apart from the dependency upgrades, which stay out deliberately.

1. The graph was one layer deep

before after
CycloneDX dependencies entries 1 192
edges (CDX dependsOn / SPDX DEPENDS_ON) 46 661
components in no relation at all 153 0

Edges come from each resolved package's own pubspec.yaml, narrowed to what pub actually resolved. Transitive dev_dependencies stay out — they are in none of our artefacts. Bundled JS, fonts and SDKs hang off the root, so "shipped but named nowhere" no longer exists.

Missing and empty are kept distinct: a component whose manifest could not be read gets no dependencies entry, which CycloneDX reads as "unknown" — not an empty list, which would claim it has none.

It pays off immediately: opencv_core@1.4.5 → dartcv4@1.1.8 is now one query instead of a guess.

Guarded by five new tests: no orphan component, graph deeper than the root entry, no edge to an unknown ref, SPDX edges == CDX edges.

2. supplier — 194 of 200, and one deliberate gap

Derived, never invented: declared repository/homepage on a forge → the account; SDK packages → the SDK vendor; vendored forks → the upstream account; fonts → the rightsholder from their own OFL text; OciDeck → Stichting LibreKAT.

A registry or CDN yields nothing: jsDelivr delivers DOMPurify, it does not supply it. So the six vendored JS/CSS bundles keep an empty supplier — locally we hold only their CDN URL. Typing cure53 in from memory would be writing down something nothing checks. If you want this closed, a repository field belongs in assets/web_export/MANIFEST.json at the next bundle refresh.

CycloneDX also carries the URL the supplier was derived from, so the derivation is checkable. SPDX 2.3 has no field for that; recorded in docs/SBOM.md rather than inventing an externalRef.

3. The gate that could not go red

Both halves were wrong. The bundled MIAUW "version" was 2026-07-16 — the day we adopted the schema — while the probe returned 2024-12-06, the date the source carries. Two different clocks compared with >. An adoption date is by definition later than the source change, so latest > bundled could never be true.

  • bundled version corrected to the value the source carries;
  • the comparison is !=, not >. Any divergence alarms, including an upstream value that looks older — then our bookkeeping is wrong. This also realigns it with StandardFreshness.isOutdated in the app, which the tool had drifted from;
  • new probePath: the probe watches the workbook itself, not releases (green through a workbook change without a release) and not the repo as a whole (red from a README typo). A path that no longer exists upstream is now a named finding, not a silent "unknown".

Verified in both directions, twice over. Live: current data → clean; old bookkeeping restored → OUTDATED; renamed probePath → a finding that says so. And in the suite, via an injectable Prober over the real register — one test is called "the old MIAUW bookkeeping is now actually seen", so if that ever goes green the comparison has been reverted.

Not done

dartcv4 1.1.8 → 2.2.1+4 stays. Out of scope by instruction, and rightly: it is held by opencv_core: ^1.4.5 — now visible as a single edge in the new graph — so the route runs through opencv_core, not through dartcv4. If no opencv_core release allows 2.x it becomes a dependency_override across a transitive boundary, which is a decision rather than maintenance. And it must be exercised with DARTCV_LIB_PATH set (otherwise the native half sleeps under flutter test) against malformed and truncated image data plus the goldens. A major on the layer that decodes untrusted image data is not flipped unattended.

Verification

Built by a subagent in an isolated worktree; rebased onto current main by me, and make check re-run after the rebase: exit 0, 5510 tests, 86.2% coverage. make sbom-verify and make deps-check both clean. pubspec.yaml untouched; sbom/ regenerated and committed.

## Summary Closes #517 apart from the dependency upgrades, which stay out deliberately. ## 1. The graph was one layer deep | | before | after | |---|---|---| | CycloneDX `dependencies` entries | 1 | 192 | | edges (CDX `dependsOn` / SPDX `DEPENDS_ON`) | 46 | **661** | | components in no relation at all | **153** | **0** | Edges come from each resolved package's own `pubspec.yaml`, narrowed to what pub actually resolved. Transitive `dev_dependencies` stay out — they are in none of our artefacts. Bundled JS, fonts and SDKs hang off the root, so "shipped but named nowhere" no longer exists. **Missing and empty are kept distinct**: a component whose manifest could not be read gets *no* `dependencies` entry, which CycloneDX reads as "unknown" — not an empty list, which would claim it has none. It pays off immediately: `opencv_core@1.4.5 → dartcv4@1.1.8` is now one query instead of a guess. Guarded by five new tests: no orphan component, graph deeper than the root entry, no edge to an unknown ref, SPDX edges == CDX edges. ## 2. `supplier` — 194 of 200, and one deliberate gap Derived, never invented: declared `repository`/`homepage` on a forge → the account; SDK packages → the SDK vendor; vendored forks → the upstream account; fonts → the rightsholder from their own OFL text; OciDeck → Stichting LibreKAT. **A registry or CDN yields nothing**: jsDelivr *delivers* DOMPurify, it does not supply it. So the six vendored JS/CSS bundles keep an empty supplier — locally we hold only their CDN URL. Typing `cure53` in from memory would be writing down something nothing checks. If you want this closed, a `repository` field belongs in `assets/web_export/MANIFEST.json` at the next bundle refresh. CycloneDX also carries the URL the supplier was derived from, so the derivation is checkable. SPDX 2.3 has no field for that; recorded in `docs/SBOM.md` rather than inventing an `externalRef`. ## 3. The gate that could not go red **Both halves were wrong.** The bundled MIAUW "version" was `2026-07-16` — the day *we* adopted the schema — while the probe returned `2024-12-06`, the date the *source* carries. Two different clocks compared with `>`. An adoption date is by definition later than the source change, so `latest > bundled` could never be true. - bundled version corrected to the value the source carries; - the comparison is `!=`, not `>`. Any divergence alarms, including an upstream value that looks older — then our bookkeeping is wrong. This also realigns it with `StandardFreshness.isOutdated` in the app, which the tool had drifted from; - new `probePath`: the probe watches the workbook itself, not releases (green through a workbook change without a release) and not the repo as a whole (red from a README typo). A path that no longer exists upstream is now a named finding, not a silent "unknown". **Verified in both directions, twice over.** Live: current data → clean; old bookkeeping restored → **OUTDATED**; renamed `probePath` → a finding that says so. And in the suite, via an injectable `Prober` over the real register — one test is called *"the old MIAUW bookkeeping is now actually seen"*, so if that ever goes green the comparison has been reverted. ## Not done **`dartcv4` 1.1.8 → 2.2.1+4 stays.** Out of scope by instruction, and rightly: it is held by `opencv_core: ^1.4.5` — now visible as a single edge in the new graph — so the route runs through `opencv_core`, not through `dartcv4`. If no `opencv_core` release allows 2.x it becomes a `dependency_override` across a transitive boundary, which is a decision rather than maintenance. And it must be exercised with `DARTCV_LIB_PATH` set (otherwise the native half sleeps under `flutter test`) against malformed and truncated image data plus the goldens. A major on the layer that decodes untrusted image data is not flipped unattended. ## Verification Built by a subagent in an isolated worktree; rebased onto current `main` by me, and `make check` re-run after the rebase: **exit 0**, 5510 tests, 86.2% coverage. `make sbom-verify` and `make deps-check` both clean. `pubspec.yaml` untouched; `sbom/` regenerated and committed.
Twee gaten uit de release-review, in dezelfde generatorpas omdat ze uit
dezelfde bron komen: het pubspec.yaml van elk opgelost pakket.

**De graaf was één laag diep.** 46 DEPENDS_ON-relaties over 200 componenten, en
153 componenten kwamen in géén enkele relatie voor. pubspec.lock is een platte
lijst — die zegt wát er in de build zit, niet wie het binnenhaalt — dus een
graaf die alleen daaruit komt kan de vraag niet beantwoorden die 's ochtends
gesteld wordt als er een CVE op een transitieve parser landt: wat raakt dit, en
kan het weg? Elk pakket draagt nu zijn eigen randen; de gebundelde JS, de fonts
en de SDK's hangen aan de wortel. 46 → 661 randen, 153 → 0 zwevend.

Het verschil tussen ontbrekend en leeg is dragend: een component waarvan we het
manifest niet konden lezen krijgt géén dependencies-regel (CycloneDX leest dat
als "onbekend"), niet een lege lijst — dat laatste zou beweren dat we hebben
gekeken.

**Supplier ontbrak volledig** (NTIA-minimumelement). Nu afgeleid, nooit
verzonnen: de verklaarde repository levert het account op waaronder een pakket
staat, een sdk-pakket erft de leverancier van de SDK waarin het meereist, een
font erft de rechthebbende uit zijn eigen OFL-tekst. Een registry of CDN levert
niéts op — jsdelivr bezorgt DOMPurify, het levert het niet. Daarmee dragen 194
van de 200 componenten een leverancier; de zes gevendorde JS-bundels niet, want
lokaal hebben we van hen alleen de CDN-URL. Dat veld blijft dan leeg.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
De verouderingscontrole meldde MIAUW als actueel, en dat was geen uitspraak
maar een structurele onmogelijkheid. De gebundelde "versie" was 2026-07-16 —
de dag waarop wíj het schema overnamen — en de probe leverde 2024-12-06, de
datum die de bron draagt. Twee verschillende klokken, vergeleken met `>`. Een
overnamedatum ligt per definitie ná de bronwijziging, dus "is upstream
nieuwer?" kon daar nooit met ja worden beantwoord.

Drie dingen, en ze horen bij elkaar:

* De gebundelde versie is nu de waarde die de bron draagt (2024-12-06), niet
  onze overnamedag. `miauwBundledVersion` legt uit welke van de twee het is,
  want ze zien er hetzelfde uit.
* De vergelijking is `!=` in plaats van `>`: elke afwijking alarmeert, ook een
  upstreamwaarde die ouder lijkt — dan klopt onze boekhouding niet, en dat mag
  niet stil blijven. Dat is dezelfde regel als StandardFreshness.isOutdated in
  de app; dat de poort en het instellingenoverzicht hetzelfde antwoord geven
  hoort een eigenschap te zijn en geen toeval.
* De probe kijkt naar het werkboek zelf (nieuw: probePath), niet naar de
  releases en niet naar de repo als geheel. Repobreed zou rood worden van een
  README-typefout; op de releases zou groen blijven bij een werkboekwijziging
  zonder release. Allebei leren ze mensen wegkijken. Een pad dat upstream niet
  meer bestaat is nu een bevinding met een naam, geen stille "onbekend".

De poort is opengebroken tot een injecteerbare probe zodat er in twee
richtingen op te toetsen valt: schoon op het echte register, en rood op een
geplante verouderde bron — inclusief letterlijk de boekhouding die hem groen
hield. Buiten de suite ook live nagelopen: exit 0 nu, exit 1 met de oude datum,
exit 1 met een hernoemd bronpad.

githubReleaseDate is vervallen; niets gebruikte het nog.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
docs(changelog): de diepere onderdelenlijst en de poort die kón falen
Some checks failed
CI / Gate (Linux) · Format · Analyze · Coverage (push) Has been cancelled
CI / Test (macos-latest) (push) Has been cancelled
CI / Test (windows-latest) (push) Has been cancelled
CI / Web hardening (push) Has been cancelled
CI / Docs links (push) Has been cancelled
CI / Supply-chain (Trivy · advisory) (push) Has been cancelled
CI / Gate (Linux) · Format · Analyze · Coverage (pull_request) Has been cancelled
CI / Test (macos-latest) (pull_request) Has been cancelled
CI / Test (windows-latest) (pull_request) Has been cancelled
CI / Web hardening (pull_request) Has been cancelled
CI / Docs links (pull_request) Has been cancelled
CI / Supply-chain (Trivy · advisory) (pull_request) Has been cancelled
87fb17655d
Twee dingen die een lezer van buiten merkt: de SBOM zegt nu ook hoe onderdelen
samenhangen en wie ze levert, en de verouderingscontrole voor MIAUW deed jaren
alsof ze iets vaststelde.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
brenno merged commit 379b28641f into main 2026-07-22 12:01:55 +00:00
brenno deleted branch feat/sbom-diepte 2026-07-22 12:01:55 +00:00
Sign in to join this conversation.
No description provided.