Compare two restore output properties. NuGet in MSBuild 17.13 and .NET SDK 9.0.200 added RestoreProjectCount, RestoreSkippedCount and RestoreProjectsAuditedCount. A small target in Directory.Solution.targets can fail the build when the audited count does not match the project count, so a silent audit gap cannot pass.
The problem with silence
NuGet Audit prints warnings when it finds something. When it prints nothing, you cannot tell whether packages were clean or the audit never ran. Reasons it might not run include audit turned off with NuGetAudit set to false, an audit source that returned no data, a project skipped because restore was up to date, or an older SDK on the agent.
The three counts
| Property | Meaning |
|---|---|
| RestoreProjectCount | Projects in the restore |
| RestoreSkippedCount | Projects restore skipped as already up to date |
| RestoreProjectsAuditedCount | Projects that were audited |
These are output properties of the restore task. Microsoft notes they are not available with static graph restore.
The target
Microsoft's example places the target in a file named Directory.Solution.targets next to the solution file. The solution level file matters because NuGet's restore runs at the solution level, whereas Directory.Build.props is imported by projects.
<Project>
<Target Name="AssertRestoreTaskOutputProperties"
AfterTargets="Restore"
Condition="'$(CI)' == 'true'">
<Error
Condition="'$(RestoreProjectsAuditedCount)' != '$(RestoreProjectCount)'"
Text="Restore did not audit every project. Expected: $(RestoreProjectCount) Found: $(RestoreProjectsAuditedCount)" />
</Target>
</Project>
If your cache lets restore skip unchanged projects, you may want a condition that adds the skipped count to the audited count before comparing. Decide whether the check should run everywhere or only in CI, and make sure your CI sets the variable you test.
Putting it into a pipeline
- Pin the SDK with a global.json so the properties exist on every agent.
- Add the targets file and test it locally by turning NuGetAudit off for one project and watching the build fail.
- Run restore in an audit pipeline that treats NU1900 to NU1905 as errors, as Microsoft's separate pipeline example shows, using a property like AuditPipeline set on the command line.
- Keep ordinary builds warning only, so new advisories do not block unrelated fixes.
- Alert the owning team when the audit pipeline fails, and show a badge for the last result.
Why a separate audit pipeline helps
Microsoft describes keeping audit warnings as non blocking in local builds while failing a dedicated pipeline. Developers still see the warning in their editor, and a new advisory does not stop a bug fix at an inconvenient time.
What this does not prove
It proves the audit ran against the projects in that restore. It does not prove the advisory data was current or complete, or that a vulnerable function is reachable. Pair it with a scan that reports reachability.
Try it yourself
Click through it at your own pace, or open it full screen. Look at what makes a check fail and how that shows up for the author.
Safeguard offers dependency scanning, reachability analysis and policy gates for pull requests, and a free scan shows findings on a repository.
Quick check
Does a restore with zero warnings prove my solution is clean?
No. It could also mean audit was disabled or skipped. The audited count check closes that gap.
Why not put the check in Directory.Build.props?
Restore runs at the solution level, but Directory.Build.props is imported by projects, so the solution targets file is the right place.
Sources
Last reviewed: 19 October 2026.