Skip to content

P1: Supply-chain security — Dependabot, npm audit gate, SBOM (#175) - #192

Merged
dkijania merged 2 commits into
mainfrom
ci/supply-chain
Aug 24, 2026
Merged

P1: Supply-chain security — Dependabot, npm audit gate, SBOM (#175)#192
dkijania merged 2 commits into
mainfrom
ci/supply-chain

Conversation

@dkijania

@dkijania dkijania commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

What & why

Part of the production-readiness epic (#163). Refs #175.

There were no supply-chain controls: no Dependabot, no audit gate, no SBOM.

Changes

  • .github/dependabot.yml — weekly updates for npm (production/development grouped), GitHub Actions, and Docker (keeps the pinned base-image digest from P1: Container hardening — node as PID 1 / tini, HEALTHCHECK, pin digest #171 fresh).
  • .github/workflows/security.yaml:
    • npm audit — hard gate on critical advisories in production deps (what actually ships in the image/npm package), plus a full informational audit for visibility.
    • CycloneDX SBOM generation, uploaded as a build artifact.

Notes

Testing

  • npm audit --omit=dev --audit-level=critical → exit 0 (the blocking gate passes)
  • prettier --debug-check . — clean; YAML validated
  • No application code changed.

🤖 Generated with Claude Code

@dkijania dkijania added production-readiness Work toward making the API production-ready / publicly available P1 Strongly recommended before GA labels Jun 28, 2026
dkijania added a commit that referenced this pull request Jul 17, 2026
Pins both Dockerfile stages to node:22-alpine@sha256:16e22a55… (v22.23.1,
verified by pull and build). Without this, moving to a floating 22 tag
would silently drop the digest pin #189 adds, reverting its
reproducible/tamper-evident build hardening depending on merge order.
Once #192 lands, Dependabot's docker ecosystem keeps the digest fresh.

Bumps @types/node to ^22 so the types match the runtime rather than
staying on 20.

Adds tests pinning the GraphQL-over-HTTP behaviour the mina-explorer
depends on: validation errors return 200 (its client throws on non-2xx
before reading the body) and carry the literal "Cannot query field" (its
fallbacks key on that string). Both were verified by hand across the
4 → 5 upgrade and are unchanged, but they rest on an implicit
content-negotiation default — yoga only returns 400 under an Accept
header that client never sends — so a later bump could flip them
unnoticed. Now a standing guard rather than a one-time check.

Addresses review feedback on #194.

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

Copy link
Copy Markdown
Collaborator

Verdict: MERGEABLE

CI-only (.github/dependabot.yml, .github/workflows/security.yaml), no runtime code, so none of the downstream constraints are in play: GraphQL error text, CORS defaults, and the blocks/events/actions query shapes are untouched. mina-explorer's bestChainFilter.ts includes('inBestChain') match and mina-explorer-api's SCHEMA_ERROR_MARKERS tier fallback (app/upstream/graphql.py) cannot regress from this diff. So the only real question is whether the CI is honest — and it mostly is.

What I checked

  • The gate actually gates. security.yaml:27 is npm audit --omit=dev --audit-level=critical with no || true, no continue-on-error, no set +e. The informational pass at :30 is the only || true and is correctly labelled as such. Verified against the real run (28326439353, job npm audit): full tree = 115 vulnerabilities (8 low, 60 moderate, 42 high, 5 critical); --omit=dev = 12 vulnerabilities (8 moderate, 4 high). So --omit=dev genuinely scopes (5 criticals exist today, all dev-only) and the gate is a live tripwire rather than a decoration — it would fire the moment one of those criticals reaches a prod path. critical (not high) is the right call for the merge train: high would be red today on the 4 OpenTelemetry/fast-uri transitives and would block all 20+ open PRs.
  • Trigger and permissions are correct. pull_request at :4, not pull_request_target — no write token or secrets exposed to untrusted PR code. permissions: contents: read at :12-13 covers both jobs. No ${{ }} interpolation of PR title/branch/body into any run: block, so no script-injection surface.
  • Trivy is not in this PR despite the title. The body explains why (dropped rather than shipped red, hence Refs #175). Fine by me — a Trivy job without ignore-unfixed and with a ghcr.io DB pull is exactly the flaky-permanently-red job that gets disabled a week later. Better absent than fake. Please drop "Trivy" from the PR title so the merge log doesn't claim something that isn't there.
  • SBOM is real and clean. Pulled artifact 7935838041: valid CycloneDX from syft v1.42.3 scan dir:. -o cyclonedx-json, 157 components (123 npm + 9 workflow files). No secrets, tokens, or connection strings; only absolute /home/runner/... paths on the workflow-file entries, which is harmless. Worth knowing: syft excluded dev deps from package-lock.json (no artillery/eslint/ts-node/typescript present; 123 npm entries vs 1586 lockfile packages / 138 non-dev), so this is a shipped-artifact SBOM, not a full-tree one. That is arguably the right scope — just don't let anyone read it as complete.
  • Dependabot config is valid, but see the queue note below.

Non-blocking nits

  1. The SBOM job is green-by-construction (security.yaml:38 and :45, both continue-on-error: true). If syft breaks, the generation step swallows it, then upload-artifact runs with a missing file and if-no-files-found: warn — a green check and no SBOM. Since SBOM generation is not a merge gate for anyone's PR, let it fail loudly:
      sbom:
        name: SBOM
        runs-on: ubuntu-latest
        steps:
          - uses: actions/checkout@v4
          - name: Generate CycloneDX SBOM
            uses: anchore/sbom-action@v0
            with:
              path: .
              format: cyclonedx-json
              output-file: sbom.cyclonedx.json
          - name: Upload SBOM artifact
            uses: actions/upload-artifact@v4
            with:
              name: sbom
              path: sbom.cyclonedx.json
              if-no-files-found: error
  2. npm ci runs 1586 packages' lifecycle scripts in the supply-chain job. Mildly ironic, and unnecessary: npm audit resolves from the lockfile and does not need node_modules. Either drop the install step or, if you want to keep it deterministic, use - run: npm ci --ignore-scripts. Low risk today (job holds only contents: read and no secrets), but it is free to fix.
  3. anchore/sbom-action@v0 is a third-party action on a floating major tag (:39). On a PR whose subject is supply-chain security, pin it to a full commit SHA with a # v0.x.y trailer — the github-actions Dependabot ecosystem you're adding will keep SHA pins updated automatically.
  4. Dependabot PR volume vs. the merge backlog. npm is capped at 10 (dependabot.yml:9) and the github-actions / docker ecosystems have no limit set, so they default to 5 each — worst case ~20 bot PRs landing on a queue that already has 20+ human PRs. Suggest capping and grouping the two CI ecosystems:
      - package-ecosystem: github-actions
        directory: '/'
        schedule:
          interval: weekly
        open-pull-requests-limit: 3
        groups:
          github-actions:
            patterns: ['*']
    
      - package-ecosystem: docker
        directory: '/'
        schedule:
          interval: weekly
        open-pull-requests-limit: 2
    Also worth dropping the npm limit from 10 to ~5; with both groups defined, routine version updates collapse into two PRs anyway and the limit only matters for the ungrouped security updates.
  5. Tightening path. Once P1: Dependency upgrades — graphql-yoga 4→5, Node 20→22 LTS #176 (Yoga 5) clears the fast-uri/OTel highs, flip :27 to --audit-level=high and keep --omit=dev. Leaving the informational pass at :30 is good — but consider npm audit --json > audit.json || true plus an artifact upload so the dev-side 5 criticals are trackable over time instead of scrolling past in a log.
  6. Adding a concurrency: { group: security-${{ github.ref }}, cancel-in-progress: true } block would stop redundant runs on force-pushes. Trivial.

None of these change the answer: this is a net-positive, honestly-wired gate that will not block the merge train. Rebase (mergeStateStatus: BEHIND) and ship.

Automated second-pass review — focus: downstream compatibility with mina-explorer / mina-explorer-api.

SanabriaRusso
SanabriaRusso previously approved these changes Aug 18, 2026

@SanabriaRusso SanabriaRusso left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving on the basis of the second-pass review comment above: no mid-to-high severity security, compatibility, or degradation issue found, and the downstream contract with mina-explorer / mina-explorer-api holds — GraphQL validation error text reaches errors[].message verbatim, the browser SPA's cross-origin access is preserved, and the real consumer query shapes (including the 2000-block analytics query and the 500-row page crawl) still pass.

Two things this approval does not mean:

  • It does not close the non-blocking items in the review comment. Several are worth fixing before or shortly after merge; they are written up there with patches.
  • It does not by itself mean the branch is ready to merge. main requires branches to be up to date, so this needs an update-branch (or a rebase, if the branch is conflicting) first, and a few PRs in this series have cross-PR ordering constraints called out in their review comments.

Automated second-pass review — focus: downstream compatibility with mina-explorer / mina-explorer-api.

dkijania added a commit that referenced this pull request Aug 24, 2026
Pins both Dockerfile stages to node:22-alpine@sha256:16e22a55… (v22.23.1,
verified by pull and build). Without this, moving to a floating 22 tag
would silently drop the digest pin #189 adds, reverting its
reproducible/tamper-evident build hardening depending on merge order.
Once #192 lands, Dependabot's docker ecosystem keeps the digest fresh.

Bumps @types/node to ^22 so the types match the runtime rather than
staying on 20.

Adds tests pinning the GraphQL-over-HTTP behaviour the mina-explorer
depends on: validation errors return 200 (its client throws on non-2xx
before reading the body) and carry the literal "Cannot query field" (its
fallbacks key on that string). Both were verified by hand across the
4 → 5 upgrade and are unchanged, but they rest on an implicit
content-negotiation default — yoga only returns 400 under an Accept
header that client never sends — so a later bump could flip them
unnoticed. Now a standing guard rather than a one-time check.

Addresses review feedback on #194.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@dkijania dkijania changed the title P1: Supply-chain security — Dependabot, npm audit gate, Trivy, SBOM (#175) P1: Supply-chain security — Dependabot, npm audit gate, SBOM (#175) Aug 24, 2026

@SanabriaRusso SanabriaRusso left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving. e61d725e fixes the green-by-construction problem, and I verified the gate is real without being permanently red.

The SBOM job is no longer green-by-construction. continue-on-error does not appear anywhere in .github/workflows/security.yaml now — I read the whole file at head, not just the diff. if-no-files-found is error (:52), so a failed SBOM generation fails the job instead of warning. anchore/sbom-action is SHA-pinned (e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0).

The audit gate genuinely fails the build. security.yaml:29-30 runs npm audit --omit=dev --audit-level=critical with no || true and no continue-on-error. The only || true remaining is line 33, the explicitly-labelled "Full audit (informational)" step — that's a deliberate gate/signal split, not a masked gate.

And it won't block every merge. I ran the exact gated command against main's lockfile: exit 0, with prod-only severity counts {low: 0, moderate: 8, high: 2, critical: 0}. Scoping to --omit=dev and gating at critical is the right pragmatic calibration for a first gate — a gate that is always red is worse than none.

Security posture of the workflow itself is good. Workflow-level permissions: contents: read (:12-13) inherited by both jobs with no job-level escalation; trigger is pull_request, not pull_request_target (:4), so the classic fork-code-with-write-token hole is absent; no secrets referenced; and npm ci --ignore-scripts (:27) stops a malicious PR's lifecycle scripts executing on the runner. concurrency with cancel-in-progress is set. Nicely constructed.

Dependabot volume is bounded. Three ecosystems, all interval: weekly, with open-pull-requests-limit 5 / 3 / 2 = 10 PRs per week worst case, and the grouping (production-dependencies / development-dependencies for npm, patterns: ['*'] for actions) collapses most weeks to 2–4. Nowhere near a firehose. The dependency-type: production|development group syntax is valid.

Non-blocking nits

  1. The docker ecosystem entry is inert right now. .github/dependabot.yml:28-32 targets / and the comment says it keeps "the pinned base-image digest" fresh — but main's Dockerfile currently uses a floating node:20-alpine tag, which the docker updater won't bump. It starts doing anything once #189 or #194 lands a digest pin. Worth knowing so it isn't later mistaken for broken.

  2. The informational audit reports green. npm audit || true shows a green check even when it finds things, so nobody will click into it. Same non-blocking semantics, better visibility:

    -      # Informational: surface the full picture (incl. dev tooling) without blocking.
    -      - name: Full audit (informational)
    -        run: npm audit || true
    +      # Informational: surface the full picture (incl. dev tooling) without blocking.
    +      # continue-on-error (rather than `|| true`) so a finding shows as a visible
    +      # non-blocking failure instead of a green check nobody opens.
    +      - name: Full audit (informational)
    +        continue-on-error: true
    +        run: npm audit --audit-level=high
  3. critical is loose given today's baseline. There are already 2 HIGH prod advisories (picomatch ReDoS + POSIX-class glob matching, and uuid buffer bounds via jaeger-client). npm audit reports the picomatch one as fixable without a breaking change. Tightening to --audit-level=high is a reasonable follow-up once those are cleared — I wouldn't do it in this PR, since it would land red.

  4. node-version: '20' at security.yaml:22 will be stale once #194 moves everything to 22. New file, so no textual conflict — worth a follow-up sweep. (nightly-devnet-dump.yaml:56 is in the same boat; I've noted it on #194.)

  5. Inconsistent action pinning: anchore/sbom-action is SHA-pinned while actions/checkout@v4, setup-node@v4 and upload-artifact@v4 are tag-pinned (mutable). Defensible for first-party actions — just flagging that the file applies two standards.

  6. The title still advertises Trivy, which isn't in the diff (dropped deliberately per the earlier review). Worth editing the title so the commit history doesn't claim something the repo doesn't have.

One note on scope, not a request: this workflow is not among main's required status checks (Run-Tests and Linting only), so even a red audit run won't hard-block a merge today. If the intent is a real gate rather than a signal, npm audit needs adding to branch protection — same observation I made on #198 about Check Schema.

Downstream: none. CI/repo configuration only — no application code, no schema, no runtime behaviour. Zero risk to either consumer.

dkijania added a commit that referenced this pull request Aug 24, 2026
Pins both Dockerfile stages to node:22-alpine@sha256:16e22a55… (v22.23.1,
verified by pull and build). Without this, moving to a floating 22 tag
would silently drop the digest pin #189 adds, reverting its
reproducible/tamper-evident build hardening depending on merge order.
Once #192 lands, Dependabot's docker ecosystem keeps the digest fresh.

Bumps @types/node to ^22 so the types match the runtime rather than
staying on 20.

Adds tests pinning the GraphQL-over-HTTP behaviour the mina-explorer
depends on: validation errors return 200 (its client throws on non-2xx
before reading the body) and carry the literal "Cannot query field" (its
fallbacks key on that string). Both were verified by hand across the
4 → 5 upgrade and are unchanged, but they rest on an implicit
content-negotiation default — yoga only returns 400 under an Accept
header that client never sends — so a later bump could flip them
unnoticed. Now a standing guard rather than a one-time check.

Addresses review feedback on #194.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
dkijania and others added 2 commits August 24, 2026 22:22
There were no supply-chain controls: no Dependabot, no audit gate, no SBOM.

- `.github/dependabot.yml`: weekly updates for npm (production/development
  grouped), GitHub Actions, and Docker (keeps the pinned base-image digest
  fresh).
- `.github/workflows/security.yaml`:
  - npm audit — hard gate on **critical** advisories in production deps (what
    actually ships), plus a full informational audit. Production deps currently
    have 0 critical, so the gate passes; the 4 highs are OpenTelemetry/fast-uri
    transitives that Dependabot / the Yoga 5 upgrade (#176) will clear.
  - CycloneDX SBOM generation, uploaded as an artifact.

Dependency/image vulnerability scanning (Trivy/Grype) is deferred so the scanner
action can be verified separately rather than shipped red — Dependabot (npm +
docker) already surfaces vulnerable deps and base images in the meantime.

Refs #175.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QSuak9smCHbp4N17xjjLF6
@dkijania
dkijania merged commit 30db51c into main Aug 24, 2026
8 checks passed
@dkijania
dkijania deleted the ci/supply-chain branch August 24, 2026 20:53
dkijania added a commit that referenced this pull request Aug 25, 2026
Pins both Dockerfile stages to node:22-alpine@sha256:16e22a55… (v22.23.1,
verified by pull and build). Without this, moving to a floating 22 tag
would silently drop the digest pin #189 adds, reverting its
reproducible/tamper-evident build hardening depending on merge order.
Once #192 lands, Dependabot's docker ecosystem keeps the digest fresh.

Bumps @types/node to ^22 so the types match the runtime rather than
staying on 20.

Adds tests pinning the GraphQL-over-HTTP behaviour the mina-explorer
depends on: validation errors return 200 (its client throws on non-2xx
before reading the body) and carry the literal "Cannot query field" (its
fallbacks key on that string). Both were verified by hand across the
4 → 5 upgrade and are unchanged, but they rest on an implicit
content-negotiation default — yoga only returns 400 under an Accept
header that client never sends — so a later bump could flip them
unnoticed. Now a standing guard rather than a one-time check.

Addresses review feedback on #194.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
dkijania added a commit that referenced this pull request Aug 26, 2026
Pins both Dockerfile stages to node:22-alpine@sha256:16e22a55… (v22.23.1,
verified by pull and build). Without this, moving to a floating 22 tag
would silently drop the digest pin #189 adds, reverting its
reproducible/tamper-evident build hardening depending on merge order.
Once #192 lands, Dependabot's docker ecosystem keeps the digest fresh.

Bumps @types/node to ^22 so the types match the runtime rather than
staying on 20.

Adds tests pinning the GraphQL-over-HTTP behaviour the mina-explorer
depends on: validation errors return 200 (its client throws on non-2xx
before reading the body) and carry the literal "Cannot query field" (its
fallbacks key on that string). Both were verified by hand across the
4 → 5 upgrade and are unchanged, but they rest on an implicit
content-negotiation default — yoga only returns 400 under an Accept
header that client never sends — so a later bump could flip them
unnoticed. Now a standing guard rather than a one-time check.

Addresses review feedback on #194.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
dkijania added a commit that referenced this pull request Aug 26, 2026
Pins both Dockerfile stages to node:22-alpine@sha256:16e22a55… (v22.23.1,
verified by pull and build). Without this, moving to a floating 22 tag
would silently drop the digest pin #189 adds, reverting its
reproducible/tamper-evident build hardening depending on merge order.
Once #192 lands, Dependabot's docker ecosystem keeps the digest fresh.

Bumps @types/node to ^22 so the types match the runtime rather than
staying on 20.

Adds tests pinning the GraphQL-over-HTTP behaviour the mina-explorer
depends on: validation errors return 200 (its client throws on non-2xx
before reading the body) and carry the literal "Cannot query field" (its
fallbacks key on that string). Both were verified by hand across the
4 → 5 upgrade and are unchanged, but they rest on an implicit
content-negotiation default — yoga only returns 400 under an Accept
header that client never sends — so a later bump could flip them
unnoticed. Now a standing guard rather than a one-time check.

Addresses review feedback on #194.

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

Labels

P1 Strongly recommended before GA production-readiness Work toward making the API production-ready / publicly available

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants