Skip to content

deno.lock coverage: convert the npm subset to a CycloneDX SBOM in-lane (plan + DAG) #1

Description

@bdelanghe

osv-scanner 2.4.0 has no deno.lock extractor, so Deno-first repos in this org get a green check having scanned nothing — the gap flagged in infra#104. This issue is the plan to close the closable part of it.

The insight that makes this cheap

A v5 deno.lock already contains the fully-resolved npm dependency graph. Verified against front-desk-scheduler's lock: 107 entries in the npm section (names, exact versions, integrity hashes) vs 3 in jsr. Deno's npm-compat layer did the resolution work; we only need to re-encode it as something the scanner reads.

Also verified locally against the real v2.4.0 binary: a CycloneDX 1.5 file named *.cdx.json is auto-detected by scan source -r and scoped npm purls (pkg:npm/%40hono/node-server@1.19.15) extract cleanly. No flags, no scanner changes — drop a generated SBOM next to the lock and the existing scan picks it up.

Three buckets, stated honestly

bucket example covered?
npm-registry deps mysql2@3.13.0, @hono/node-server ✅ after this work
JSR deps via npm-compat @jsr/bounded-systems__verbspec ❌ — served from npm.jsr.io, not npmjs; no OSV advisory can exist under that purl, so including them would inflate the "scanned" count dishonestly. Excluded from the SBOM, counted in the log.
JSR-native / deno.land/x remotes jsr:@bounded-systems/verbspec ❌ — OSV has no JSR ecosystem (to re-verify: api.osv.dev is egress-blocked from the authoring session)

The lane's log will print all three counts per lock, so a green check states its own blind spot.

Determinism requirements (the Nix-flakes lens)

The converter is a pure function: same deno.lock bytes → byte-identical SBOM, forever, on any machine.

  • stdlib-only Python 3 — no pip, no third-party action, nothing to SHA-pin beyond what the lane already pins
  • no timestamp, no random serialNumber (both optional in CycloneDX; the usual UUID default is exactly the nondeterminism to refuse)
  • components sorted by purl; json.dumps(sort_keys=True), fixed separators, trailing newline
  • goldens enforce byte-equality, plus a double-run self-check
  • flake.nix provides the same Python + test runner so nix flake check reproduces CI's verdict locally. CI itself does not install Nix — the lane stays lean; the flake is the local-reproducibility face of the same test suite. (flake.lock needs one nix flake lock run on a machine with Nix — the authoring session has none.)

Single-definition constraint

A reusable workflow's steps run in the caller's checkout — tools/ from this repo isn't on disk there. So the script is embedded in osv-scan.yml as a heredoc, and tracked as tools/deno-lock-cdx.py for tests — with a drift check in self-test.yml that extracts the heredoc and byte-compares it against the file (the same keep-the-copies-honest pattern as infra's proofs/check-sync.mjs). Golden fixtures are synthetic (made-up package names) so a future real-world advisory can never redden the self-test for unrelated reasons; goldens are named *.golden, never *.cdx.json, so the scan job never ingests them.

Considered: deploy conversion/scanning as a service (Cloudflare or elsewhere)

Raised during planning. Split it in two:

  • As the converter — rejected. A hosted endpoint turns a deterministic 100-line transform into an availability dependency and a trust boundary (the lane would trust a network response instead of a local pure function), and it needs deploy credentials. By the org's own criterion — the zero-standing-grants property that made a reusable workflow beat an App in infra#90 — this is strictly worse.
  • As continuous monitoring — deferred, and it's the one real thing a service could add: today scans run on PR/push, so an advisory published after a lock merges goes unnoticed until the next change. But the cheap version of that is schedule: cron in each caller (rescan weekly, zero new infrastructure), which adopters can turn on per-repo. A central Worker that fetches locks across repos would need a standing GitHub credential — the exact thing the broker architecture exists to avoid. Revisit only if per-repo crons prove unmanageable at fleet scale.

DAG

flowchart TD
    A["A: inspect real deno.lock v5<br/>(107 npm / 3 jsr — done)"] --> B["B: tools/deno-lock-cdx.py<br/>pure, stdlib-only, sorted output"]
    A2["A2: verify *.cdx.json detection<br/>vs real binary (done)"] --> B
    B --> C["C: golden tests + double-run<br/>byte-equality check"]
    B --> D["D: embed heredoc in osv-scan.yml<br/>+ SBOM-generate step"]
    C --> E["E: drift check in self-test<br/>(heredoc ≡ tools file)"]
    D --> E
    C --> F["F: flake.nix — nix flake check<br/>runs the same tests"]
    E --> G["G: push; self-test green on runner"]
    G --> H["H: front-desk caller SHA bump<br/>→ first real deno.lock scan"]
    G --> I["I: infra caller SHA bump<br/>(no deno.lock — pin currency only)"]
    H --> J["J: report coverage counts on infra#104<br/>npm / @jsr / jsr-native"]
    F -.->|"human: nix flake lock once"| F2["F2: flake.lock committed"]

    style A fill:#2d5,stroke:#161,color:#000
    style A2 fill:#2d5,stroke:#161,color:#000
Loading

Checklist

  • A — lock format inspected (v5; npm section keys carry _peer@ver suffixes; names may contain __)
  • A2 — SBOM filename detection verified against the binary
  • B — converter
  • C — golden + determinism tests
  • D — workflow integration
  • E — heredoc↔file drift check
  • F — flake.nix (F2: nix flake lock needs a human with Nix)
  • G — self-test green
  • H — front-desk adoption (SHA bump)
  • I — infra pin currency (optional, defer OK)
  • J — coverage numbers reported on infra#104
  • re-verify "OSV has no JSR ecosystem" from a machine with open egress

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    Status
    Todo

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions