Skip to content

Harden dependency pipeline against supply-chain risk (lock files, cooldown, SCA gate) #224

Description

@FrankRay78

Summary

Harden NetPace's dependency-update pipeline against supply-chain risk without removing the automated Dependabot flow. The human merge gate stays; this issue adds assurance around it so that a reviewed-and-merged bump provably ships exactly what was reviewed, a same-day compromised publish is never ingested, and a known-vulnerable dependency fails the build.

The reasoning, alternatives, and trade-offs are recorded in a Change Intent Record: docs/change-intent-records/2026-07-13-dependency-supply-chain-hardening.md (already drafted on branch feature/dependency-supply-chain-hardening). Base your implementation on that branch so the CIR and the implementation ship together.

Background

Current state (verified July 2026):

  • Dependabot: .github/dependabot.yml — monthly NuGet PRs, assigned to the maintainer, no auto-merge. This human gate is kept.
  • Versions pinned exactly in each .csproj, except Roslynator, which floats [4.15.0, ) in src/Directory.Build.props.
  • No packages.lock.json anywhere — transitive dependencies float and nothing records content hashes.
  • No nuget.config — single implicit nuget.org feed.
  • CodeQL (.github/workflows/codeql.yml) is SAST over our own code; it does not scan dependencies for known advisories.

Scope

In scope (three changes):

  1. Lock files + locked-mode restore on CI/release only.
  2. 14-day Dependabot cooldown.
  3. Hard-failing vulnerable-dependency (SCA) check in CI.

Out of scope (deliberately deferred — do not implement):

  • Package source-mapping / nuget.config — no second feed exists yet, so there is no dependency-confusion vector to close.
  • Explicitly pinning Roslynator — locking (change 1) freezes the floating range as a side effect, so this is no longer a security gap. Leaving it as a follow-up.
  • Any SLSA / build-provenance attestation.

Implementation tasks

1. Lock files + locked-mode restore

Outcome: committed packages.lock.json files, and CI/release restores fail if the resolved dependency graph differs from what is locked. Developer machines restore normally (unlocked), so routine local work isn't blocked.

Recommended mechanism:

  • In src/Directory.Build.props, enable lock-file generation for all projects:
    • <RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
    • Gate locked-mode to CI so local restores stay unlocked, e.g. <RestoreLockedMode Condition="'$(CI)' == 'true'">true</RestoreLockedMode> (GitHub Actions sets CI=true; this also covers the implicit restores inside dotnet publish/dotnet pack, which a per-command --locked-mode flag would miss).
  • Run dotnet restore src locally once to generate a packages.lock.json per project, and commit them.
  • Confirm the four explicit restore steps still succeed under lock — no need to add --locked-mode to each if the MSBuild property approach above is used, but if you instead go the per-command route, cover all of these plus the implicit restores in publish/pack:
    • .github/workflows/dotnet.yml:21
    • .github/workflows/codeql.yml:41
    • .github/workflows/publish-nuget.yml:56
    • .github/workflows/release-binaries.yml:78 (plus the dotnet publish/dotnet pack steps that restore on the fly)

Note for the maintainer: after accepting any future dependency bump, packages.lock.json must be regenerated. Dependabot does this within its own PRs; a hand-edited version bump that skips regeneration will (by design) turn CI red.

2. Dependabot cooldown (14 days)

Outcome: Dependabot does not open a version-bump PR until the candidate release is at least 14 days old.

  • Add a cooldown: block to .github/dependabot.yml for the existing nuget update entry, with a 14-day window applied to all dependency updates.

3. Hard-failing SCA check

Outcome: CI fails the build when any direct or transitive dependency has a known advisory.

  • Add a step to the PR build pipeline (.github/workflows/dotnet.yml) that runs dotnet list package --vulnerable --include-transitive and fails the job if any vulnerability is reported. Note: this command exits 0 even when vulnerabilities are found, so the step must inspect the output (e.g. grep for advisory markers) and exit non-zero itself.
  • Place it after restore/build so transitive resolution is available.

Acceptance criteria

  • AC1 — A CI or release-pipeline restore fails when the resolved dependency graph (including transitive packages) differs from the committed lock files; a developer's local dotnet restore on an unchanged graph still succeeds without locked-mode friction.
  • AC2packages.lock.json files are committed for every restorable project under src/.
  • AC3 — Dependabot does not raise a version-bump PR for a release younger than 14 days.
  • AC4 — When a dependency (direct or transitive) has a known advisory, the PR build fails; when none do, it passes.
  • AC5 — The three changes leave the existing human review gate intact: Dependabot still opens maintainer-assigned PRs and nothing auto-merges.
  • AC6 — The CIR (docs/change-intent-records/2026-07-13-dependency-supply-chain-hardening.md) ships in the same PR.

Verification

  • dotnet build src and dotnet test src pass locally with zero warnings (warnings are errors in this repo).
  • Locally simulate CI locked-mode: CI=true dotnet restore src succeeds on the committed lock files; then bump a version in a .csproj without regenerating the lock file and confirm CI=true dotnet restore src fails (then revert).
  • Confirm the SCA step fails the job when pointed at a known-vulnerable package (can be demonstrated in a scratch commit, then reverted) and passes on the clean tree.
  • Open the PR and confirm all workflow jobs run to completion under the new lock/SCA constraints.

Maintainer manual step (cannot be automated in-repo)

Enable Dependabot security alerts and security updates in the repository Settings → Code security. This is the SCA layer that runs on GitHub's side, independent of the version-update PRs, and complements the in-CI check from task 3. Note this in the PR description as a required post-merge action.

Constraints / notes for the implementing agent

  • Follow CLAUDE.md and the constitution. These changes are build/CI configuration, not production C#, so the RED-GREEN TDD cycle does not cleanly apply; verification is via the CI runs and the local simulation above.
  • Per CLAUDE.md: touching release-binaries.yml requires checking whether docs/RELEASING.md needs a corresponding update (the restore/lock contract).
  • Do not hard-wrap markdown prose; one line per paragraph/bullet.
  • Raise a PR that requests an @claude review when complete.

Metadata

Metadata

Assignees

No one assigned

    Labels

    dependenciesenhancementNew feature or requestreadyIssue is fully defined and ready to be implemented

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions