Skip to content

IKRC Insights

Why a .NET 10 Build Can Fail on a Package You Never Added

Retargeting to net10.0 makes NuGet audit transitive packages during restore, so with warnings treated as errors, a newly published advisory can stop a release build that has not changed.

Take a hypothetical order-entry application on .NET 8 whose projects set TreatWarningsAsErrors, so any warning in the build is reported as an error. The upgrade pull request changes TargetFramework from net8.0 to net10.0, the build passes, and the release ships. Some days later, orders placed with a discount code start showing the wrong total, and a small fix is ready. The release pipeline stops at restore with NU1903, a package with a known high-severity vulnerability. The package is named in no project file, and nobody changed a package version between the two builds.

What the net10.0 retarget changes in restore

NuGet's audit was introduced in .NET 8. During restore it checks the packages in the graph against vulnerability data from the configured audit sources, and for nuget.org that data comes from the GitHub Advisory Database. Microsoft's restore reference says that in .NET 8 and .NET 9, only direct package references are audited by default.

The breaking-change note for .NET 10 states the new rule: "When projects target .NET 10 or higher, then NuGetAuditMode defaults to all if it hasn't been explicitly set." Transitive packages, the ones pulled in by the packages a project references, now raise warnings too, and in a project that treats warnings as errors Microsoft notes that this can cause restore failures. For a project that targets .NET 9 or lower, the default "remains direct".

With the .NET 10 SDK, what decides the default is the project's target framework. A team that installs that SDK on its build agents ahead of the retarget, as the .NET 10 upgrade planning article describes, still gets a direct-only audit for its net8.0 projects unless someone has set NuGetAuditMode. The edit to TargetFramework is what widens the check to the whole graph. A library that multi-targets net8.0 and net10.0 gets the wider check on both targets, because NuGet's auditing documentation says that "if any one target framework selects all, then audit will use this value for all target frameworks." One exception in the history: a .NET 9 preview briefly made all the default for every project, and the .NET 9.0.101 SDK reverted it.

The result of the wider check also depends on the date. The same NuGet page says a clean audit means nothing was found "at the present moment of time you checked", because "the advisory database can be updated at any time." NuGetAuditLevel defaults to low, so restore reports every severity, from NU1901 for low through NU1902 and NU1903 to NU1904 for critical. Under TreatWarningsAsErrors, and with none of those codes exempted, each one fails the restore. In the hypothetical above, a commit that restored cleanly on Monday fails on Thursday with no change to the repository, which is why the policy for these four codes has to be in place by the time the retarget merges.

Which direct reference brought the package in?

The restore audit reports the affected package, the severity and a link to the advisory. For a transitive package, NuGet's auditing page says "you'll need to find the transitive package path", and dotnet nuget why does that: given the project file and the flagged package name, it prints the dependency graph that leads to the package. Microsoft's fix order starts closest to the project. If the project references package A, A depends on B and B on the flagged package C, first try a newer A, then a newer B, and only then reference C directly at a fixed version. A direct reference added this way is a pin that outlives its reason unless someone removes it, and the auditing page says to remove it "when a new package version update becomes available". Repositories on Central Package Management can use its transitive pinning for the same purpose.

Listing every transitive package for .NET 10 compatibility is a separate check, covered in what .NET 8 and .NET 9 end of support means.

Four decisions the advisory policy has to record

NuGet's documentation recommends configuring the audit at the repository level, for example in a Directory.Build.props file at the root of the repository. The first two decisions below are settings in that file, the third is wiring in the CI configuration, and the fourth is a record kept beside each exception:

  • Which severities fail which build. A repository that keeps TreatWarningsAsErrors can list NU1901 through NU1904 in WarningsNotAsErrors "to prevent vulnerabilities discovered in the future from breaking your build". A repository without it can add only NU1903 and NU1904 to WarningsAsErrors, so high and critical advisories fail and low and moderate ones stay warnings.
  • Whether a project may set NuGetAuditMode to direct. The breaking-change note documents that switch, and with it the project is warned only about packages it references directly.
  • Which pipeline fails on an advisory. The auditing page shows a dedicated audit pipeline: a property such as AuditPipeline turns the audit codes, NU1900 through NU1905, into errors when restore runs with -p:AuditPipeline=true, and a second condition lists the same codes in WarningsNotAsErrors for every other build.
  • How a suppression is written. A NuGetAuditSuppress item takes the advisory URL from the warning message, and that URL is the one value the documented item carries. Microsoft says a suppression would "completely suppress the advisories, even for packages that share the advisory", and calls it "a last resort". We would record the rest in a comment beside each item: who accepted the advisory, why it does not apply, and the date of the next review, with a ticket assigned to that person for that date.

Our default would set NuGetAuditMode to all explicitly and use Microsoft's conditional pair, so release and hotfix builds report advisories as warnings and the audit pipeline fails on them. That pipeline would run on a timer as well as on every merge, and a failed run would go to the developer who maintains the build.

The hotfix branch is where a missing policy shows

Back on the release branch of the hypothetical order-entry application, with the discount-code fix waiting, the developer on call can suppress the advisory, exempt the audit codes from TreatWarningsAsErrors, or update the package if a fixed version exists. The first two are policy decisions made under time pressure by whoever happens to be on call.

Updating looks like the quick route. In the .NET 10 SDK, dotnet package update --vulnerable moves each flagged package to "the lowest version that is higher than the currently referenced version, that does not have any known vulnerabilities." The command runs its own restore to find the warnings, and its reference page says that with TreatWarningsAsErrors on, "NuGet's restore warnings can cause restore to fail, preventing the update from completing." Microsoft's workaround puts a condition on the property, TreatWarningsAsErrors Condition="'$(CustomCondition)' == ''", and runs CustomCondition=true dotnet package update --vulnerable on Linux and Mac shells. When the update succeeds, the small hotfix also carries a package change, and that change needs its own test run before the release goes to production.

How IKRC would set this up during a .NET 10 upgrade

We would merge the audit policy as its own pull request while the projects still target net8.0. Setting NuGetAuditMode to all there brings the transitive findings forward, the exemption keeps them as warnings in ordinary builds, and the audit pipeline lists them for the developer who maintains the build to work through before the retarget. The wiring is checked by three results: an ordinary restore of a project that currently has an advisory prints the warning and succeeds, the same restore with -p:AuditPipeline=true fails, and the failed run reaches that developer. The retarget pull request then changes TargetFramework and nothing about auditing. Both pull requests fall under our .NET development and security services.

Related Reading

Planning a .NET 10 upgrade and a separate workflow improvement covers the SDK's own support warnings and roll-forward. What .NET 8 and .NET 9 end of support means covers the deadline and package compatibility.

Contact IKRC

Your next software project.

Connection Lost

Attempting to reconnect to the server...