Skip to content

Harden supply chain: clear 7 npm advisories, attach release provenance, pin base image, add fuzzing - #5510

Merged
vivekchand merged 1 commit into
mainfrom
scorecard-hardening
Sep 4, 2026
Merged

Harden supply chain: clear 7 npm advisories, attach release provenance, pin base image, add fuzzing#5510
vivekchand merged 1 commit into
mainfrom
scorecard-hardening

Conversation

@vivekchand

Copy link
Copy Markdown
Owner

No-PRD: security-posture hardening driven by the public OpenSSF Scorecard report, not a product requirement.

OpenSSF Scorecard scores this repo 6.6/10. Four checks are fixable in the repo itself; this fixes all four. Expected result ≈ 8.8.

Check Weight Before After What was wrong
Vulnerabilities High 3 10 7 npm advisories in frontend/, which ships inside the wheel
Signed-Releases High 0 10 Releases had 10 assets, none signed or provenanced
Pinned-Dependencies Medium 3 ~8 Unpinned base image, curl | python pipes, npm install
Fuzzing Medium 0 10 No fuzzer

Vulnerabilities

All 7 are npm deps under frontend/, which publish.yml builds into the published wheel. Every fix required a major bump, which is why Dependabot had them spread over 13 PRs that each broke the build on their own:

  • vite 5.4.21 → 6.4.3 — also carries esbuild past 0.25.0 and drops launch-editor entirely
  • react-router-dom 6 → 7.18.0
  • i18next-http-backend 2 → 3.0.5

npm audit reports 0 vulnerabilities and tsc -b && vite build passes. Every router API in use (BrowserRouter, Routes, NavLink, Outlet, useLocation, useParams) is unchanged in v7. Vite 6 rather than 7 because CI builds the wheel on Node 20. This supersedes #5376, #5244, #5241, #5247 and the other frontend Dependabot PRs.

Signed-Releases

release-on-merge.yml is the workflow that actually cuts a release (publish.yml is never dispatched for auto-releases). It now mints build provenance over the same dist/ that twine uploads, then attaches the wheel, sdist and a .intoto.jsonl to the release. publish.yml does the same for a hand-pushed tag.

gh attestation verify clawmetry-<v>.whl --repo vivekchand/clawmetry

Uploads are non-blocking on purpose — the wheel is already on PyPI by that point, so a failed upload must not fail a completed release. That tolerance is exactly how the desktop-artifacts dispatch once broke silently on a 403, so tests/test_release_provenance_attached.py asserts the wiring instead of trusting it.

Pinned-Dependencies

  • Base image pinned by digest, plus a Dependabot docker ecosystem — a digest pin without an updater freezes the image at today's CVEs forever.
  • Three curl … | python3 -c pipes now download to a file and then parse. The payload was always data, but the line is indistinguishable from piping remote code into an interpreter.
  • The two npm installs with a committed lockfile are now npm ci.

Deliberately not fixed: the 63 pip install sites. Scorecard credits pip only with --require-hashes, so reaching 10 means hash-locked requirement files for every distinct tool set across 25 workflows — permanent CI friction (every tool bump needs regenerated hashes) for roughly +0.1 of final score. Happy to do it if you'd rather have the point.

Fuzzing

Atheris harnesses over the two places ClawMetry parses input it did not write: the OTLP/JSON decoder (any host that can reach the dashboard port can POST to /v1/traces, and it is an in-house stdlib parser) and the transcript event classifier (JSONL written by the agent runtimes). They assert contracts, not merely absence of crashes.

The OTLP harness found three real contract violations on its first run, all fixed here. decode() documents "raises ValueError on a malformed body", and _otlp_receive maps ValueError onto HTTP 400:

  1. {"resourceSpans": 3}TypeError. Every repeated field guarded each item with isinstance(x, dict) but never guarded the container, so a scalar where OTLP specifies an array crashed before the item guards ran. One bug in fifteen places — fixed with a _seq() helper rather than patching the one input I happened to find.
  2. Non-gzip body sent as Content-Encoding: gzipgzip.BadGzipFile (an OSError).
  3. Truncated gzip body → EOFError.

Verification

  • npm audit: 0 vulnerabilities; npm run build clean (TypeScript included)
  • tests/test_fuzz_targets.py: 204 pass — and 13 fail against the unfixed parser, so the guard is not vacuous
  • 60k random inputs through both harnesses: 0 violations on fixed code, 9,425 on unfixed
  • tests/test_release_provenance_attached.py: 10 pass, and goes red when id: attest or the .intoto.jsonl name is removed
  • Docker digest resolved independently against the registry, not taken from the scanner's suggestion
  • The repo's own test_workflow_yaml_valid.py caught a missing requests in the new fuzz job (pytest would have exited 4); fixed
  • Full targeted sweep: same 4 pre-existing failures on this branch and on pristine origin/main, isolated — zero regressions
  • ruff count on dashboard.py + clawmetry/: 5024 before, 5024 after. make lint-py fails identically on pristine main (local ruff is newer than CI's pin) — pre-existing, untouched here

Not addressed — neither is a code change

  • Code-Review (0, High) — 0/28 changesets had an approving review. Requiring approvals would block your own auto-merge flow on a solo-maintained repo, so this is your call, not mine.
  • Branch-Protection (−1) — the repo uses classic branch protection, which Scorecard's token cannot read, so the check errors and is excluded from the score. Migrating to a ruleset would make it readable, but with required_approving_review_count: 0 it would likely score low and drag the average down rather than up.
  • CII-Best-Practices (0, Low) — needs a registration at bestpractices.dev.

🤖 Generated with Claude Code

https://claude.ai/code/session_01ByTvE7AYmmqZKEGhpxFETx

…e, pin base image, add fuzzing

OpenSSF Scorecard scored this repo 6.6/10. Four checks were fixable in the
repo itself; this change fixes all four.

Vulnerabilities (3 -> 10). All 7 advisories were npm deps under frontend/,
which publish.yml builds into the shipped wheel. Every fix needed a major
bump, so Dependabot had them split across 13 PRs that each broke the build
alone: vite 5.4.21 -> 6.4.3 (also pulls esbuild past 0.25.0 and drops
launch-editor entirely), react-router-dom 6 -> 7.18, i18next-http-backend
2 -> 3. `npm audit` is now clean and `tsc -b && vite build` passes; the
router APIs in use (BrowserRouter/Routes/NavLink/Outlet/useParams) are
unchanged in v7. Vite 6 rather than 7 because CI builds the wheel on Node 20.

Signed-Releases (0 -> 10). Releases carried ten desktop installers and
nothing signed or provenanced, so a downloader had to take our word for what
produced them. release-on-merge.yml -- the workflow that actually cuts a
release -- now mints build provenance over the same dist/ that twine
uploads, and attaches the wheel, sdist and a .intoto.jsonl to the release.
publish.yml covers a hand-pushed tag the same way. Uploads are non-blocking
because the wheel is already on PyPI by then, so the wiring is asserted by
tests/test_release_provenance_attached.py rather than trusted.

Pinned-Dependencies (3 -> ~8). Base image pinned by digest, with a
Dependabot docker ecosystem added so the pin still receives CVE updates --
a digest pin without an updater freezes the image at today's CVEs. Three
`curl | python3` pipes now download to a file and then parse, and the two
`npm install`s that have a committed lockfile are `npm ci`. Left unpinned:
63 `pip install` sites, which Scorecard only credits with
`--require-hashes`; hash-locking those across 25 workflows is permanent CI
friction for roughly +0.1 of final score.

Fuzzing (0 -> 10). Atheris harnesses over the two places we parse input we
did not write: the OTLP/JSON decoder (any host that can reach the dashboard
port can POST to it) and the transcript event classifier. They assert
contracts, not just absence of crashes.

The OTLP harness found three real contract violations on its first run, all
fixed here. decode() documents "raises ValueError on a malformed body", and
the HTTP layer maps ValueError to a 400:

  1. `{"resourceSpans": 3}` raised TypeError. Every repeated field guarded
     each item with isinstance(x, dict) but never guarded the container, so
     a scalar where OTLP specifies an array crashed before the item guards
     ran -- one bug in fifteen places, fixed with a _seq() helper.
  2. A non-gzip body sent as Content-Encoding: gzip raised BadGzipFile.
  3. A truncated gzip body raised EOFError.

tests/test_fuzz_targets.py asserts the same contracts over the inputs that
broke them, so this is caught on every PR without Atheris installed; it goes
13-red against the unfixed parser.

Not addressed, because neither is a code change: Code-Review (0) needs
approving reviews on PRs, and Branch-Protection currently errors rather than
scoring because the repo uses classic protection that Scorecard's token
cannot read -- enabling it could lower the total rather than raise it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ByTvE7AYmmqZKEGhpxFETx
@8090-software-factory

Copy link
Copy Markdown

✅ Drift Bot (ClawMetry): no drift detected

Drift Bot analyzed the changed files against this project's blueprints and requirements and found no drift.

@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Visual diff

Comparing b8a0c66fea92 (head) against the PR base branch.

29 of 72 comparison(s) flagged (>1% pixel diff).

View Before After Diff
desktop overview ⚠️ before after diff · 100.00%
desktop flow before after diff · 0.05%
desktop brain before after diff · 0.01%
desktop usage before after diff · 0.00%
desktop crons before after diff · 0.51%
desktop memory before after diff · 0.01%
desktop security ⚠️ before after diff · 2.00%
desktop subagents before after diff · 0.02%
desktop transcripts before after diff · 0.40%
desktop logs ⚠️ before after diff · 5.14%
desktop skills ⚠️ before after diff · 1.87%
desktop models before after diff · 0.00%
desktop approvals ⚠️ before after diff · 2.57%
desktop alerts before after diff · 0.00%
desktop notifications ⚠️ before after diff · 2.15%
desktop limits before after diff · 0.00%
desktop clusters before after diff · 0.00%
desktop history before after diff · 0.00%
desktop channels before after diff · 0.41%
desktop dives before after diff · 0.02%
desktop harness ⚠️ before after diff · 1.92%
desktop inventory before after diff · 0.02%
desktop nemoclaw ⚠️ before after diff · 2.38%
desktop guard before after diff · 0.02%
desktop signals before after diff · 0.28%
desktop policy before after diff · 0.00%
desktop selfevolve before after diff · 0.28%
desktop swimlane before after diff · 0.00%
desktop tool-catalog before after diff · 0.01%
desktop tracing before after diff · 0.00%
desktop turn-anatomy before after diff · 0.00%
desktop version-impact before after diff · 0.00%
desktop context-economics ⚠️ before after diff · 2.38%
desktop agents before after diff · 0.02%
desktop evals before after diff · 0.17%
desktop bench before after diff · 0.00%
mobile overview ⚠️ before after diff · 100.00%
mobile flow before after diff · 0.05%
mobile brain ⚠️ before after diff · 1.23%
mobile usage before after diff · 0.00%
mobile crons before after diff · 0.02%
mobile memory ⚠️ before after diff · 100.00%
mobile security ⚠️ before after diff · 2.75%
mobile subagents before after diff · 0.00%
mobile transcripts ⚠️ before after diff · 1.80%
mobile logs ⚠️ before after diff · 4.38%
mobile skills before after diff · 0.01%
mobile models before after diff · 0.00%
mobile approvals ⚠️ before after diff · 100.00%
mobile alerts before after diff · 0.00%
mobile notifications before after diff · 0.01%
mobile limits before after diff · 0.00%
mobile clusters ⚠️ before after diff · 1.48%
mobile history before after diff · 0.00%
mobile channels ⚠️ before after diff · 1.48%
mobile dives before after diff · 0.00%
mobile harness before after diff · 0.52%
mobile inventory ⚠️ before after diff · 100.00%
mobile nemoclaw ⚠️ before after diff · 4.90%
mobile guard ⚠️ before after diff · 2.74%
mobile signals ⚠️ before after diff · 4.69%
mobile policy ⚠️ before after diff · 100.00%
mobile selfevolve ⚠️ before after diff · 3.39%
mobile swimlane ⚠️ before after diff · 100.00%
mobile tool-catalog before after diff · 0.58%
mobile tracing ⚠️ before after diff · 100.00%
mobile turn-anatomy before after diff · 0.02%
mobile version-impact before after diff · 0.00%
mobile context-economics before after diff · 0.04%
mobile agents ⚠️ before after diff · 2.97%
mobile evals ⚠️ before after diff · 18.30%
mobile bench ⚠️ before after diff · 100.00%

Folder: b8a0c66fea92. Full PNGs also attached as a workflow artefact.

Generated by visual-diff bot. Pixel diffs >1% flagged; eyeball the table before merging. This check is non-blocking — fail = bot bug, not a code problem.

@vivekchand
vivekchand merged commit 4c4d61d into main Sep 4, 2026
40 checks passed
@vivekchand
vivekchand deleted the scorecard-hardening branch September 4, 2026 17:04
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.

1 participant