The core insight

Most supply chain attacks against NuGet packages are detected within days of a compromised version being published — by the community, by security researchers, or by automated scanning. This creates an exploitable window, but also a defensible one: a version that has been in the ecosystem for weeks without incident is meaningfully safer than one published yesterday.

This does not mean old packages are safe. It means that recently published versions — including new versions of packages you already trust — deserve additional scrutiny before they land in your project.

What "zero-trust" means here

In this context, zero-trust does not mean trusting nothing. It means:

The tool enforces none of this directly. It provides the visibility. The policy is a team commitment, reinforced by the lock file and CI gate.

The detection window

Supply chain attacks on NuGet packages follow a consistent pattern:

  1. An attacker publishes a compromised version — through a hijacked account, a typosquatted package ID, or a legitimate-looking new package.
  2. The version propagates into projects via restore, Dependabot, or template updates.
  3. The attack is detected — typically within days, sometimes within hours.

The implication: a deliberate delay between a version being published and it being allowed into your project captures most of this window at low cost. A 14-day threshold (the tool default) covers the large majority of detected attacks. A 30-day threshold covers nearly all of them, at the cost of slower dependency updates.

This threshold applies to all packages regardless of trust status. A verified Microsoft.* package published two days ago carries more risk than one published six months ago. Account compromise, insider threat, and build pipeline compromise do not respect the verified flag.

New projects

Stable release SDK templates

dotnet new with a stable release SDK produces a project whose packages fall into two categories that were both pinned at SDK ship time, weeks or months before you create the project. They are outside the detection window by definition — a stable SDK release goes through extensive vetting, and the packages it references have been in the ecosystem long enough that any compromise would have been detected.

For most teams this means no pre-restore review is needed for SDK templates — audit immediately after the automatic restore instead.

If your organisation requires full coverage before any package reaches the machine, note that even first-party Microsoft packages can pull in transitive dependencies owned by private individuals rather than organisations. The audit will surface these, but by then they are already in your NuGet cache. To review the graph first, suppress the automatic restore and run preview-restore before allowing anything to download:

# Optional: review the full graph before any packages reach the machine
dotnet new blazor --no-restore
nuget-audit preview-restore --path .
# Review output — check for transitives owned by private individuals or unknown publishers.
# If satisfied, proceed with restore. If not, remove the project and reconsider your template choice.
dotnet restore
# Standard path:

# 1. Create the project — restores automatically, which is fine for most teams
dotnet new blazor

# 2. Initialize trust config and audit the restored graph
nuget-audit init --path .
nuget-audit audit --path .

# 3. For each flagged package: review and add to TrustConfig.json, then re-run to confirm
nuget-audit trust-owner <owner> --path .        # verified publishers you trust broadly
nuget-audit trust-package <id> <ver> --path .  # everything else
nuget-audit audit --path .

# 4. Lock the graph — add both properties to Directory.Build.props:
#    <Project>
#      <PropertyGroup>
#        <RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
#        <RestoreLockedMode>true</RestoreLockedMode>
#      </PropertyGroup>
#    </Project>
#    RestorePackagesWithLockFile generates packages.lock.json for every project.
#    RestoreLockedMode enforces it — restore fails if the graph would change.
dotnet restore --force-evaluate

# 5. Add .nuget-audit-ok to .gitignore (machine-local sentinel created by Directory.Build.targets)

# 6. Commit everything
git add packages.lock.json Directory.Build.props Directory.Build.targets .gitignore TrustConfig.json
git commit -m "Lock NuGet graph and add audit enforcement"

Third-party installed templates

Third-party templates (installed via dotnet new install) also restore automatically during dotnet new. By running dotnet new install you have already implicitly trusted the template author. The audit's role here is not to validate that decision; it is to establish TrustConfig.json entries for the packages the template introduced. Expect more flags on first audit — packages from publishers not in the default trusted owners list.

Setting up trust

When the audit flags a package, you have two options in TrustConfig.json:

Add the owner to trustedOwners

Trusts all current and future packages from that nuget.org account, regardless of version. Appropriate for publishers you have high confidence in across their entire catalog: well-known organizations, publishers whose packages you use broadly and intend to track as they update.

nuget-audit trust-owner opentelemetry --path .

Add to trustedPackages

Trusts only that package at that exact version. Any version change re-flags for review. This is the more conservative, zero-trust default — even if you trust the publisher, pinning a version means you explicitly review each update before it enters your project. The version-pinning cost is highest exactly where scrutiny should be highest: unverified publishers.

nuget-audit trust-package SomePackage 2.0.0 --path .

Use nuget-audit audit --package-list to generate formatted copy-paste entries for all flagged packages at once.

Adding and updating packages

With RestoreLockedMode=true in place, dotnet restore fails if the lock file is out of date — requiring a deliberate dotnet restore --force-evaluate. That deliberate act is the entry point for this workflow.

Do not use the VS NuGet Package Manager for this workflow.

VS Package Manager restores immediately on package selection, bypassing the preview step. Edit .csproj or Directory.Packages.props by hand, then run dotnet restore --force-evaluate manually.

# 1. Preview before restore — runs dotnet restore into a temp dir; nothing in your real cache
nuget-audit preview-update SomePackage --version 2.0.0 --path .

# 2. Review the output carefully:
#    - Unexpected new packages from unknown publishers?
#    - VersionChanged on a package you didn't intend to update?
#    - Recently published flag on a package from an unfamiliar owner?
#    If anything raises concern, stop here.

# 3. Edit .csproj or Directory.Packages.props by hand

# 4. Restore, forcing the lock file to be re-evaluated
dotnet restore --force-evaluate

# 5. Audit to confirm the restored graph matches the preview
nuget-audit audit --path .

# 6. Update TrustConfig.json for newly flagged packages, re-run to confirm clean

# 7. Commit the updated lock file alongside the package reference change
git add packages.lock.json
git commit -m "Update SomePackage to 2.0.0"
Preview accuracy

By default, preview-update runs an exact dotnet restore into a temp directory — the resolved graph matches what would actually be installed. Step 5 (nuget-audit audit) is still the confirmation gate; preview is for early warning before lock file changes are committed. Use --fast for a quicker approximate result if speed matters more than exact accuracy.

Cloning an existing project

Repo has a lock file

The lock file constrains restore to exactly the versions that were audited when it was committed. Clone and open normally — the restore cannot silently pull in different versions. Run nuget-audit audit after cloning to verify the current state before building.

Repo has no lock file

Clone with the git CLI and do not open in VS yet — opening in VS triggers an automatic restore, closing the pre-restore review window. The same applies any time you edit package references: run preview-restore before dotnet restore to see the full graph first.

# 1. Preview explicit packages and their transitives before anything restores
nuget-audit preview-restore --path .

# 2. Review output — untrusted packages, recently published flag, unrecognised owners
#    If anything raises concern, do not proceed.

# 3. Restore
dotnet restore

# 4. Run the full audit to confirm the complete graph
nuget-audit audit --path .

# 5. Update TrustConfig.json, re-run to confirm clean

# 6. Lock the graph and set up VS enforcement (see above)

Pulling a teammate's package change

When a teammate adds or updates a package following this workflow, they commit three things: the updated package reference, the updated packages.lock.json, and any TrustConfig.json changes. When you pull:

  1. Restore runs automatically (VS on solution open, or dotnet restore on CLI). With RestoreLockedMode=true and the updated lock file, this restore is constrained to exactly the versions the teammate resolved. No unexpected packages can land.
  2. The sentinel goes stale. The pulled packages.lock.json has a newer timestamp than your local .nuget-audit-ok. The MSBuild target detects this and runs --check before the next build.
  3. If the teammate followed the workflowTrustConfig.json is updated, --check passes, build proceeds.
  4. If the teammate did not update TrustConfig.json--check fails and the build is blocked. CI Gate 2 also fails. The build stays blocked until TrustConfig.json is updated.

The one gap: there is no pre-restore review window on your local machine — packages download before the audit runs at build time. This is mitigated by the lock file: what downloads is constrained to exactly the locked versions. The recently published flag in the --check output will surface any package within the detection window, giving you the opportunity to raise a concern before the code is built.

The lock file and RestoreLockedMode

Two properties work together — both belong in Directory.Build.props:

error NU1004: The lock file is out of date. To update the lock file run the
following command: dotnet restore --force-evaluate

This means editing a .csproj or Directory.Packages.props and running dotnet restore is not enough — the deliberate --force-evaluate flag is required. The lock does not prevent package changes; it prevents unintentional or unreviewed ones.

The lock file is project state and belongs in source control. Every team member and every CI/CD run works from the same frozen graph.

nuget-audit warns if it detects a packages.lock.json without RestoreLockedMode=true set — setup is self-checking.

Visual Studio pre-build enforcement

Without this, VS may trigger a build immediately after restore — closing the window before anyone acts. A pre-build MSBuild target closes this gap automatically.

nuget-audit init creates Directory.Build.targets at the solution root. If you skipped init or need to recreate it, run:

nuget-audit init --path .

The target runs nuget-audit audit --check before each build whenever the lock file has changed, using a sentinel file (.nuget-audit-ok) to skip builds where the lock file is unchanged. Add .nuget-audit-ok to .gitignore — it is machine-local state. CI does not use the sentinel; it always audits unconditionally.

nuget-audit warns — and --check exits 1 — if RestoreLockedMode=true is set but this target is not present at the solution root, so incomplete setup does not go unnoticed.

Multi-project solutions

Directory.Build.targets evaluates per project. Each project has its own packages.lock.json; the sentinel file lives at the solution root. The first project whose lock file is newer than the sentinel triggers one audit for the whole solution and updates the sentinel. Subsequent projects in the same build skip it.

Prerequisites

CI/CD gates

Two independent gates together enforce the full workflow:

GateWhat it catches
dotnet restore with RestoreLockedMode=true Package reference edited without updating the lock file
nuget-audit audit --check Packages needing review, deprecated packages, known vulnerabilities, and setup advisory conditions (missing lock file, RestoreLockedMode, pre-build target, or Package Source Mapping)
# In your CI pipeline:
dotnet restore                         # fails with NU1004 if lock file is out of date
nuget-audit audit --check --path .    # fails with exit 1 if any packages need review
ScenarioGate 1Gate 2
Package added/updated following the workflowPassesPasses
Package added/updated, lock file not committedFailsNot reached
Package added/updated, TrustConfig.json not updatedPassesFails
No package changesPassesPasses — quiet

Dependabot and automated updates

Dependabot PRs can be merged — or auto-merged — within hours of a compromised version being published, bypassing the detection window entirely. See the Dependabot integration guide for the full mitigation pattern, PR review workflow, and fake PR detection.

Where this policy breaks down

Understanding the gaps is part of the policy:

ScenarioGapMitigation
VS NuGet Package Manager Restore happens before any preview Team policy: use CLI only for package operations; pre-build sentinel catches it before the next build
VS auto-restore on solution open Triggered by .csproj changes from git pull Pre-build sentinel runs audit before build; CI catches it post-restore
Git pull with teammate's package change No pre-restore review window locally Lock file constrains restore to locked versions; sentinel triggers audit before next build; recently published flag surfaces packages within the detection window
Preview accuracy Preview resolves transitives via the NuGet API and may not exactly match what dotnet restore selects Always follow preview with a full audit after restoring to confirm the graph
Preview or nightly SDK Package versions may be within the detection window with less vetting than a stable release Not handled — treat the resulting project as an unvetted dependency graph and audit manually, or accept the additional risk
Private-feed packages in preview Supported when credentials are available via NuGet.Config or Azure Artifacts Credential Provider; --version required Configure NuGet.Config credentials or install the Azure Artifacts Credential Provider