Skip to content

ci(security): add fix-dependabot-alerts automation#328

Open
TalZaccai wants to merge 9 commits into
mainfrom
dev/talzacc/fix-dependabot-alerts
Open

ci(security): add fix-dependabot-alerts automation#328
TalZaccai wants to merge 9 commits into
mainfrom
dev/talzacc/fix-dependabot-alerts

Conversation

@TalZaccai
Copy link
Copy Markdown
Contributor

@TalZaccai TalZaccai commented Jun 2, 2026

⚠️ Depends on #321 — that PR adds the root Directory.Packages.props this script edits. Merge #321 first, then this one.

Summary

Ports the fix-dependabot-alerts automation from microsoft/TypeChat (#339) to this repo. Daily scheduled workflow consumes the repo's open Dependabot alerts and opens a PR applying any fixes that survive a full restore + build + test verification.

Fix strategy (single-mode for .NET / CPM)

This repo uses Central Package Management — every version lives in the root Directory.Packages.props. The fixer:

  1. Fetches open Dependabot alerts via gh api.
  2. For each alerted NuGet package:
    • If listed in Directory.Packages.props → bump its Version attribute to the advisory's first_patched_version.
    • Otherwise (transitive) → insert a new <PackageVersion Include="..." /> pin so CPM applies it to every project.
  3. Runs dotnet restore --force and parses NU1901–NU1904 audit warnings to verify the alert's GHSA is gone AND no new vulnerable transitive was introduced.
  4. Runs dotnet build /warnaserror -m:1 and dotnet test as the final gate.
  5. Rolls back via per-fix backup if any step fails, and records the rollback so the same broken bump isn't retried every scheduled run.

Safety properties

  • Never downgrades an existing central version (handles stale alerts / per-project VersionOverride); refuses to write when current and patched versions have incomparable prereleases.
  • Audit baseline captured pre-fix; rejects any change that introduces a new (package, GHSA) warning.
  • Sanitized env (no GH_TOKEN / GITHUB_TOKEN / NUGET_API_KEY) passed to dotnet restore/build/test so package-provided MSBuild props/targets can't exfiltrate CI credentials.
  • Cooldown on any post-fix failure (restore, audit, build, or test) keyed by (package, props sha).
  • NuGet-aware semver: 4-part legacy versions compared correctly (4.0.5.104.0.5.2); prereleases treated as lower than the same release.

Local verification

Validated end-to-end with DEP_MOCK_ALERTS_FILE (a local-only escape hatch that reads alerts from JSON instead of calling gh api):

  • Transitive pin path: seeded Microsoft.SemanticKernel 1.71→1.68, fed a mock critical alert on Microsoft.SemanticKernel.Core (1.71.0 patched). Fixer inserted the pin, dotnet restore came back clean, build + test passed, reported applied=1.
  • No-downgrade guard: with the same downgraded state, fed a stale alert demanding 1.65.0 (lower than current). Fixer correctly classified it as unfixable with no file modifications.

Files

  • tools/scripts/fix-dependabot-alerts.mjs — fixer script (~26KB).
  • .github/workflows/fix-dependabot-alerts.yml — daily workflow; GitHub App auth; rollback-state cache; final clean-build verification (wipes per-project bin/obj, prunes ./.git).

Repo-level prerequisites

The workflow expects these settings configured outside this PR:

  • Variable DEPENDABOT_APP_ID (Settings → Secrets and variables → Actions → Variables tab) — the GitHub App's ID. App IDs aren't sensitive, so this is a variable, not a secret.
  • Secret DEPENDABOT_APP_PRIVATE_KEY — the App's private key. The App needs Contents: write, Pull requests: write, Dependabot alerts: read.

Without those the workflow will fail at the auth step; it will not partially run.

Caveats / known limitations

  • Depends on Directory.Packages.props keeping its current <PackageVersion Include="X" Version="Y" /> adjacency convention; non-adjacent attribute orderings would silently miss the existing entry and add a duplicate pin. Worth replacing the regex with a real XML attribute parser if the convention ever changes.
  • Devcontainers/etc. are out of scope — Dependabot only supports security updates for ecosystems in its supported list, and this repo only consumes nuget + github-actions alerts (ci(dependabot): switch to security-only mode #327).
  • One package per PR run is intentional — easier to bisect a CI regression to a single bump.

TalZaccai and others added 6 commits June 1, 2026 21:25
Modernizes the .NET package management stack for the whole repo:

1. Central Package Management (CPM) - introduces root Directory.Packages.props as the single source of truth for every package version. All <PackageReference> entries across the 20 SDK-style csproj files become version-less; the previously per-project version sprawl (e.g. 3 different Microsoft.NET.Test.Sdk versions) collapses to a single declaration.

2. Central Transitive Pinning (CentralPackageTransitivePinningEnabled) - lets a <PackageVersion> entry in Directory.Packages.props force a transitive dependency to a specific version across every project, without having to add per-project top-level <PackageReference> entries. This becomes the override mechanism for fixing transitive security advisories.

3. Lockfile mode (RestorePackagesWithLockFile=true) - generates packages.lock.json next to each csproj on every restore, committed to source control. Enables deterministic CI restores via dotnet restore --locked-mode (opt-in) and gives Dependabot accurate transitive vulnerability detection.

4. NuGetAudit transitive mode (NuGetAuditMode=all) - surfaces known advisories for both direct and transitive dependencies as NU1901-NU1904 warnings at restore time. On by default in SDK 8.0+; this just enables transitive scanning.

Side effects (vulnerabilities discovered by the new audit and fixed in this PR):

- Microsoft.SemanticKernel 1.68.0 -> 1.71.0 (CVE-2026-25592 / GHSA-2ww3-72rp-wpp4, critical: arbitrary file write via AI agent function calling. Pulled in transitively as Microsoft.SemanticKernel.Core.)

- Microsoft.Bcl.Memory pinned to 10.0.4 via transitive pinning (CVE-2026-26127 / GHSA-73j8-2gch-69rq, high: DoS via Base64Url out-of-bounds read. Was being pulled in at 10.0.2 transitively via System.Text.Json 10.x.)

Other version reconciliations (chose the higher of conflicting versions):

- Microsoft.NET.Test.Sdk: tests on net8.0 use 18.0.1; the orphan net470 project uses VersionOverride=17.5.0

- xunit: net8.0 tests use 2.9.3; orphan net470 project uses VersionOverride=2.4.2

- xunit.runner.visualstudio: net8.0 tests use 3.1.5; orphan net470 project uses VersionOverride=2.4.5

- System.Text.Json 10.0.0 -> 10.0.2 (required by SK 1.71 transitive graph)

- Microsoft.Extensions.AI 9.10.2 -> 10.2.0 (required by SK 1.71)

- System.Numerics.Tensors 10.0.1 -> 10.0.2 (required by SK 1.71)

Verified locally:

- dotnet restore TypeChat.sln: clean, zero NU190x audit warnings

- dotnet restore tests/EmojiApp/Emoji.sln: clean

- dotnet build TypeChat.sln -c Release /warnaserror: 0 warnings, 0 errors

- dotnet test tests/TypeChat.UnitTests: 154 passed, 0 failed

Note: tests/TypeChat.Tests.Pre6 (net470) is an orphan project not included in TypeChat.sln and not built by CI; it does not generate a lockfile because TypeChat.TestLib only targets net8.0. This is a pre-existing condition unrelated to this change.

This PR is preparation for the upcoming fix-dependabot-alerts workflow port - with CPM + transitive pinning + lockfiles in place, automated transitive vulnerability remediation becomes a one-line edit to Directory.Packages.props per fix instead of editing many csproj files.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Per review feedback: NuGet lockfiles are an opt-in feature (not the .NET
community default; the dotnet/runtime repo does not commit them either)
and without CI enforcement (dotnet restore --locked-mode) they are
write-only churn that adds noise to PRs without providing determinism.

This commit removes them; CPM + transitive pinning still gives us a
single source of truth for versions, and NuGetAudit (mode=all) still
catches transitive CVEs at restore time. The 19 deleted files were
introduced by the previous commit on this branch and never enforced.

- Delete 19 packages.lock.json files
- .gitignore: add packages.lock.json so opportunistic local lockfile
  restores don't get committed again
- Directory.Build.props: drop RestorePackagesWithLockFile (audit kept)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
With RestorePackagesWithLockFile removed from Directory.Build.props the
default `dotnet restore` no longer produces lockfiles, so the ignore
pattern was paranoid. A developer who explicitly runs
`dotnet restore --use-lock-file` presumably wants the output visible.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Resolves conflicts from main commits #314 (Microsoft.Extensions.Configuration.Binder
10.0.1 -> 10.0.2) and #319 (Dependabot grouping config).

Conflict resolution:
- 3 csproj files (examples/typechat.examplesLib, src/typechat.meai,
  src/typechat.sk): kept ours (version-less PackageReferences, as
  required by CPM).
- Directory.Packages.props: bumped Microsoft.Extensions.Configuration.Binder
  PackageVersion from 10.0.1 to 10.0.2 to absorb the #314 change centrally
  so all consumers pick it up via CPM.
- .github/dependabot.yml, OpenAIConfig.cs, integration_tests.yml: merged
  in cleanly from main without conflict.

Validation:
- dotnet restore: clean, 0 NU190x warnings
- dotnet build /warnaserror: 0 warnings, 0 errors
- dotnet test tests/TypeChat.UnitTests: 154/154 passed

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds a daily-scheduled workflow that consumes the repo's open Dependabot
alerts, fixes them by editing Directory.Packages.props (Central Package
Management), and opens a PR with the result.

Fix strategy is single-mode for .NET:
- If the alerted package is in Directory.Packages.props, bump its
  Version to the minimum patched version from the advisory.
- Otherwise (transitive dependency), add a new PackageVersion entry
  pinning the transitive package to the patched version. CPM applies
  this pin to every project in the solution.

After each fix, verify by parsing 'dotnet restore --force' output for
NU1901-NU1904 warnings tagged with the alert's GHSA URL. If the warning
disappears, run a full clean build and unit-test pass before keeping
the change; otherwise roll back via the persisted backup.

Includes a DEP_MOCK_ALERTS_FILE env var escape hatch for local
seed-testing without hitting the GitHub API. Verified end-to-end:
seeded SK 1.71->1.68 downgrade, mocked alert, fixer added a
Microsoft.SemanticKernel.Core transitive pin at 1.71.0, restore clean,
build + test verified.

Modeled on the npm/pip equivalent in microsoft/TypeChat.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Sanitize env (drop GH_TOKEN etc.) on dotnet restore — package-
  provided MSBuild props/targets evaluated during restore should
  not see CI credentials.
- Capture an audit-warning baseline before applying fixes and reject
  any change that introduces a new (package, GHSA) audit warning that
  wasn't there pre-fix. Catches bumps that drag in a different
  vulnerable transitive.
- Never downgrade an existing central PackageVersion entry. If the
  current pin is already >= the advisory's first_patched_version
  (stale alert, VersionOverride at the project level, etc.) leave it
  alone and report unfixable instead of lowering the version.
- Record rollback cooldown on restore/audit failure (previously only
  recorded on build/test failure), so a broken bump isn't retried
  every scheduled run.
- Include NuGet's legacy 4th numeric segment in version comparisons so
  4.0.5.10 doesn't compare equal to 4.0.5.2.
- Workflow find now prunes ./.git when wiping bin/obj before final
  clean verification.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@TalZaccai TalZaccai requested a review from a team as a code owner June 2, 2026 06:23
@TalZaccai TalZaccai changed the base branch from dev/talzacc/cpm-lockfile-modernization to main June 2, 2026 06:27
@TalZaccai TalZaccai requested a review from Copilot June 2, 2026 06:32
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Ports a fix-dependabot-alerts automation to typechat.net: a Node script that consumes Dependabot alerts and edits the root Directory.Packages.props to raise vulnerable NuGet packages to their first_patched_version, verifying each change via dotnet restore (NuGetAudit), dotnet build /warnaserror, and dotnet test, with rollback + cooldown on failure. A daily GitHub Actions workflow runs the script under a GitHub App token and opens a single squash PR with the surviving fixes. Several csproj files and Directory.Build.props/Directory.Packages.props are included here (carried from the dependency PR #321) to provide the Central Package Management surface the script edits.

Changes:

  • New tools/scripts/fix-dependabot-alerts.mjs performing alert grouping, CPM-aware edits, restore/audit verification, build+test verification, and persistent rollback-state cooldown.
  • New .github/workflows/fix-dependabot-alerts.yml scheduled daily; uses an App token, restores rollback-state cache, runs the script, does a final clean build, and opens/dedups a PR.
  • CPM scaffolding (Directory.Packages.props, Directory.Build.props, version-less <PackageReference> entries across csproj files) carried over from #321.

Reviewed changes

Copilot reviewed 18 out of 18 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
tools/scripts/fix-dependabot-alerts.mjs Core remediation script: alert fetch/group, CPM edit, restore+audit verification, build/test gate, rollback + cooldown.
.github/workflows/fix-dependabot-alerts.yml Daily workflow: App token auth, runs script, final clean build, creates PR, dedups previous auto PRs.
Directory.Packages.props Central Package Management list of versions (from #321).
Directory.Build.props Enables NuGetAudit in all mode repo-wide (from #321).
src/Directory.Build.props, examples/Directory.Build.props Import the root Directory.Build.props to preserve the props chain.
src/typechat/TypeChat.csproj, src/typechat.schema/TypeChat.Schema.csproj, src/typechat.program/TypeChat.Program.csproj, src/typechat.sk/TypeChat.SemanticKernel.csproj, src/typechat.meai/TypeChat.Extensions.AI.csproj, src/package/nuget.props Drop per-project Version= for CPM.
tests/TypeChat.UnitTests/...csproj, tests/TypeChat.TestLib/...csproj, tests/TypeChat.IntegrationTests/...csproj, tests/TypeChat.Tests.Pre6/...csproj, tests/EmojiApp/Emoji.csproj, examples/typechat.examplesLib/TypeChat.ExamplesLib.csproj Drop Version=; Pre6 uses VersionOverride for net470-compatible pins.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread .github/workflows/fix-dependabot-alerts.yml Outdated
Comment thread .github/workflows/fix-dependabot-alerts.yml
Comment thread tools/scripts/fix-dependabot-alerts.mjs
Comment thread .github/workflows/fix-dependabot-alerts.yml Outdated
- Use bash heredocs for the commit message and PR body so the resulting
  text isn't rendered as a code block (every line was previously
  indented 10 spaces by YAML, which the markdown renderer treats as a
  code block at >=4 spaces).
- Bump runner Node to 24 (current LTS).
- Refuse to modify a central PackageVersion entry when the current
  version and the advisory's first_patched_version have incomparable
  prerelease tags (e.g. 1.2.3-alpha vs 1.2.3-beta). semverGte returns
  false in both directions for incomparable prereleases, which would
  have bypassed the no-downgrade guard. Now throws unfixable instead
  so a human can decide.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Match the TypeAgent workflow pattern: keep persist-credentials: false
on checkout (so the token isn't reachable from dotnet/MSBuild scripts
during the verify phase), but at the very end of the job re-inject the
workflow's own GITHUB_TOKEN (already scoped to contents:write at the
workflow level) for the git push.

The App token is now used only where it must be — gh api dependabot/
alerts (which the default token can't reach) and gh pr create (so the
PR identity is the bot, not github-actions). Means the App no longer
needs Contents permission at all.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
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.

3 participants