.NET 10 NuGet Audit can make a CI restore fail because a package with a known vulnerability exists somewhere in the resolved dependency graph, even when your project never references that package directly. The right response is not to disable auditing or suppress every warning. First prove what was restored, find which top-level dependency brought the package in, then choose the smallest remediation that preserves a clear security policy.

For projects targeting net10.0 or later, NuGetAuditMode now defaults to all, which audits both direct and transitive packages. If your build treats warnings as errors, a newly reported advisory can fail restore. Microsoft’s .NET 10 compatibility note confirms that this is intentional behavior, not a random CI regression.

Build a .NET 10 NuGet Audit Triage Loop

Use the same sequence every time a NU1901 through NU1904 warning appears:

  1. Restore the exact dependency graph used by CI.
  2. Save a machine-readable vulnerability report.
  3. Trace the vulnerable package back to its owner.
  4. Decide whether the package is actually resolved, pruned, or stale in an old artifact.
  5. Update the owning package, promote a transitive package only when necessary, or use a narrow documented suppression.
  6. Regenerate and review the lock file, then rerun the normal CI policy.

That sequence separates real supply-chain exposure from noisy dependency maintenance work without lowering your security bar.

A NuGet audit warning means a known-vulnerable package version is present in the resolved graph. It does not prove that your application reaches the vulnerable code path. Equally, “we do not call that API” does not make the warning a false positive. Treat reachability as a separate security assessment after you have identified the exact package and advisory.

Start With the CI Dependency Graph

Restore explicitly in CI so auditing happens at a known, visible stage:

dotnet restore MySolution.sln --locked-mode

Use --locked-mode only when your application repositories commit lock files. It prevents restore from silently changing the resolved graph. Microsoft recommends lock files for application projects at the start of a dependency chain, and documents locked mode as a way to make CI restores repeatable. NuGet lock-file guidance explains both the use case and the pruning-related lock-file behavior.

After restore, produce a report for the affected project:

dotnet package list \
  --project src/MyApp/MyApp.csproj \
  --no-restore \
  --include-transitive \
  --vulnerable \
  --format json \
  > artifacts/nuget-vulnerabilities.json

dotnet package list supports both --include-transitive and --vulnerable, and .NET 10 uses the noun-first command form. Its JSON output is useful in CI because it gives your build log and security review a stable artifact instead of relying on copied console output. The command reference includes this reporting pattern.

Do not update packages before saving this snapshot. You need a before-and-after record that answers:

  • Which project and target framework produced the warning?
  • Which package version was resolved?
  • Which advisory URL and severity were reported?
  • Is the package direct or transitive?
  • Did the remediation remove, upgrade, or merely suppress the advisory?

Trace the Dependency Owner Before Changing Versions

A transitive package is not necessarily the package you should update. Find the top-level package that introduced it:

dotnet nuget why \
  src/MyApp/MyApp.csproj \
  Vulnerable.Package \
  --framework net10.0

dotnet nuget why shows the dependency graph for a package and supports framework-specific output. This matters in multi-targeted projects, where a package may be relevant to one target framework but not another. Microsoft’s command documentation describes the graph and framework options.

For example, the output may reveal a path like this:

MyApp
└── Contoso.Messaging 5.2.0
    └── Contoso.Transport 3.4.1
        └── Vulnerable.Package 1.8.0

Your first remediation candidate is normally Contoso.Messaging or Contoso.Transport, not an arbitrary direct reference to Vulnerable.Package.

This is the key distinction:

FindingFirst action
Vulnerable package is directUpgrade or replace that direct package.
Vulnerable package is transitiveUpgrade the top-level owner that brings it in.
Owner cannot yet upgradePromote the transitive package deliberately to a fixed version, then test compatibility.
Package is absent from the current resolved reportInvestigate stale lock files, old CI artifacts, or a different target framework before changing policy.
Advisory does not apply after reviewUse a narrow advisory-level suppression with a documented reason and review date.

Fix the Owner First

Suppose Contoso.Transport has a release that removes the vulnerable dependency. Update that package on a branch, review its release notes, and test the application:

dotnet package update Contoso.Transport \
  --project src/MyApp/MyApp.csproj

When you do not specify a version, dotnet package update seeks the highest version available from configured sources. That can be useful, but it also means you must review compatibility changes rather than treating the command as an automatic production fix.

For direct vulnerable references, the .NET 10 command can target known vulnerabilities:

dotnet package update \
  --project src/MyApp/MyApp.csproj \
  --vulnerable

With --vulnerable, the command attempts to move a vulnerable referenced package to the lowest higher version that has no known vulnerability. Microsoft’s documentation also warns that the command performs an implicit restore. A repository that treats restore warnings as errors can therefore block the update operation before it has a chance to fix anything.

Do not solve that by weakening the normal CI restore. Run the upgrade in a controlled branch or local maintenance workflow, make the version change reviewable, and then rerun the regular locked restore with the real CI policy intact.

Promote a Transitive Package Only When Necessary

If the owning package cannot yet update, you can explicitly control the vulnerable transitive package version:

<ItemGroup>
  <PackageReference
    Include="Vulnerable.Package"
    Version="2.1.4" />
</ItemGroup>

This is called promotion: the project takes explicit ownership of a dependency that was previously only transitive.

Promotion is appropriate only when all of these are true:

  • The fixed version is compatible with the owning package.
  • You have tested the application, not only restore.
  • You have a reason to believe the owner will eventually remove the need for the override.
  • The new direct reference is documented in the pull request or dependency-maintenance record.

Do not promote every warning automatically. Each extra direct reference becomes another package your team owns, updates, and eventually needs to remove.

If your repository uses Central Package Management, make the version decision in the central package configuration instead of adding conflicting local versions. The important principle is the same: make dependency ownership explicit and reviewable.

Do Not Confuse Pruning With an Audit Fix

.NET 10 also enables package pruning by default for projects that target .NET 10 or later. Pruning removes unnecessary packages from the dependency graph, including certain framework-provided assemblies that would otherwise appear as package dependencies. NuGet’s PackageReference documentation explains that pruning affects both the resolved graph and lock-file contents.

A pruning warning such as NU1510 is not a vulnerability warning. It tells you that a direct package reference can be removed when pruning applies across the project’s runtime targets.

For example, this kind of reference may be unnecessary for a .NET 10-only application:

<ItemGroup>
  <PackageReference Include="System.Text.Json" Version="9.0.4" />
</ItemGroup>

Do not remove it blindly in a multi-targeted project. A package can be unnecessary for net10.0 but still required for net48 or another target. NuGet only raises NU1510 when pruning applies to all relevant runtime targets. The NU1510 guidance shows this distinction.

Pruning can reduce noise and simplify the graph, but it is not a substitute for fixing a resolved vulnerable package. Always confirm the post-change graph with:

dotnet package list \
  --project src/MyApp/MyApp.csproj \
  --no-restore \
  --include-transitive \
  --vulnerable

If the vulnerable package remains in that report, it still needs a remediation decision.

Set a Severity Policy Instead of Turning Audit Off

NuGet uses these warning codes for vulnerability severity:

WarningSeverity
NU1901Low
NU1902Moderate
NU1903High
NU1904Critical

The mappings are documented in NuGet’s warning reference.

A practical policy is to report all advisories but fail CI for high and critical findings:

<PropertyGroup>
  <NuGetAuditLevel>low</NuGetAuditLevel>
  <WarningsAsErrors>
    $(WarningsAsErrors);NU1903;NU1904
  </WarningsAsErrors>
</PropertyGroup>

This keeps lower-severity findings visible without making every advisory an emergency deployment blocker. Your organization may choose a stricter threshold, but make it an explicit policy decision rather than an accidental side effect of upgrading the SDK.

Avoid these broad “fixes”:

<NuGetAudit>false</NuGetAudit>
<NuGetAuditMode>direct</NuGetAuditMode>

The first disables security auditing. The second reverts to the older direct-only default and hides transitive findings. Both settings can be justified temporarily in exceptional circumstances, but neither should be the reflex response to a failing .NET 10 restore.

Suppress an Advisory, Not a Package or Warning Family

Sometimes an advisory needs a temporary exception. NuGet supports suppression by advisory URL:

<ItemGroup>
  <NuGetAuditSuppress
    Include="https://github.com/advisories/GHSA-example" />
</ItemGroup>

Use this only after recording:

  • The advisory URL.
  • The affected project and package version.
  • Why the normal upgrade path is blocked.
  • The owner of the exception.
  • A review or expiry date.

A specific suppression is auditable. Suppressing all NU1901 through NU1904 warnings is not. Microsoft documents NuGetAuditSuppress specifically for advisory-level suppression and recommends tracing a transitive dependency to its owner before considering promotion or suppression. The .NET 10 migration guidance covers both approaches.

Preserve Evidence in CI

A useful CI pipeline keeps restore, auditing, and build separate:

steps:
  - script: dotnet restore MySolution.sln --locked-mode
    displayName: Restore and audit NuGet packages

  - script: >
      dotnet package list
      --project src/MyApp/MyApp.csproj
      --no-restore
      --include-transitive
      --vulnerable
      --format json
      > artifacts/nuget-vulnerabilities.json
    displayName: Save NuGet vulnerability report

  - script: dotnet build MySolution.sln --configuration Release --no-restore
    displayName: Build

Upload the JSON report as a CI artifact when your platform supports it. A failed restore tells you that policy was triggered; the report, dependency path, pull request, and lock-file diff explain what changed and why.

When package pruning changes the graph, expect a lock-file diff. That is normal only when it is intentional and reviewed. Do not regenerate lock files silently in a dependency-update job and then assume the result is safe because CI passed.

Final Remediation Checklist

Before merging a NuGet audit fix, verify that:

  • The vulnerability was reproduced from the current locked restore.
  • The affected package and advisory are recorded.
  • dotnet nuget why identified the dependency owner.
  • The preferred fix updated the owner package.
  • Any promoted transitive package has a documented reason.
  • NU1510 pruning cleanup was reviewed separately from vulnerability remediation.
  • The lock-file diff is expected.
  • The normal CI restore policy still applies.
  • The post-change vulnerability report no longer includes the advisory, or a narrow documented suppression remains.
  • Build and relevant tests pass against the updated graph.

.NET 10 makes transitive vulnerability auditing more visible because transitive dependencies can still expose an application. The durable solution is not to make CI quieter. It is to make the dependency graph explainable, remediation choices deliberate, and the resulting security policy easy to audit.

References

Enjoy This Blog?

Buy Me a Coffee Donate via PayPal

Discover more from Dot Net Coder

Subscribe to get the latest posts sent to your email.

Write A Comment

Ads Blocker Image Powered by Code Help Pro

Ads Blocker Detected!!!

We have detected that you are using extensions to block ads. Please support us by disabling these ads blocker.

Powered By
Best Wordpress Adblock Detecting Plugin | CHP Adblock