.NET applications lean heavily on NuGet, and a typical ASP.NET Core service resolves a dependency graph that runs well past the handful of packages listed in its project file. The transitive layer — packages pulled in by the packages you actually chose — is where most surprises live, and NuGet's restore process quietly wires all of it into your build. The good news for .NET teams is that you do not need to install a third-party scanner to get started: the SDK ships one.
The built-in scanner: dotnet list package --vulnerable
Since .NET 5, the SDK can query the GitHub Advisory Database (through the NuGet audit sources) and report known vulnerabilities in your restored packages. The command is part of dotnet list package:
dotnet restore
dotnet list package --vulnerable
By default this only reports vulnerabilities in your direct dependencies. That is a trap, because most real risk hides one or more levels down. Always include transitive packages:
dotnet list package --vulnerable --include-transitive
Now you get the full picture: each vulnerable package, the resolved version, the advisory severity, and a link. You can restrict it to a project or solution and combine it with related audits for deprecated and outdated packages:
dotnet list package --deprecated
dotnet list package --outdated
NuGet also supports audit on restore, which surfaces vulnerabilities automatically every time you build. Enable it in your project or Directory.Build.props:
<PropertyGroup>
<NuGetAudit>true</NuGetAudit>
<NuGetAuditMode>all</NuGetAuditMode>
<NuGetAuditLevel>moderate</NuGetAuditLevel>
</PropertyGroup>
NuGetAuditMode set to all audits transitive packages too, and NuGetAuditLevel sets the severity floor for warnings — you can escalate these to build-breaking errors by treating the relevant NuGet audit warnings as errors in CI.
A caveat about project format
This tooling relies on the modern PackageReference format and the assets file produced by dotnet restore. Legacy projects still using packages.config will not benefit from dotnet list package --vulnerable. Migrating to PackageReference is worthwhile for security reasons alone, not just tidiness.
Central Package Management makes audits easier
If you run more than a couple of projects, Central Package Management (CPM) is a meaningful upgrade to your audit story. By moving version numbers out of individual project files and into a single Directory.Packages.props, you get one place to see and bump every package version across the solution — which means a vulnerable package is pinned and patched once rather than hunted down in a dozen .csproj files. Combined with audit-on-restore, CPM turns "which projects use the vulnerable version?" from an archaeology exercise into a single grep. It also removes the classic drift where two projects in the same solution silently resolve different versions of the same dependency, one patched and one not — the kind of inconsistency that lets a fixed CVE quietly reappear in the next build.
Where the built-in scanner stops
- Presence, not reachability. It reports that a vulnerable assembly is in your dependency graph. It cannot tell you whether your code calls the vulnerable API, so a flaw in a rarely-used serialization path is ranked identically to one in your authentication middleware.
- Advisory-source bound. It reflects what the configured NuGet audit sources know. Newly published malicious packages and typosquats have no advisory yet and pass silently.
- No policy or memory. Each invocation is a snapshot. There is no SLA tracking, no cross-repository dashboard, and no durable, auditable record of which findings you accepted and why.
- Manual remediation. It points at a fixed version; updating the
PackageReference, resolving transitive conflicts, and validating the build is manual work.
Going further
The built-in scanner is a great habit to establish — enable audit-on-restore everywhere and fail CI on high-severity findings. To close the remaining gaps, layer a continuous platform. Safeguard's software composition analysis engine scans the same restored NuGet graph and adds method-level reachability, so the vulnerabilities that touch code your service actually executes are prioritized above the ones that merely exist in the graph.
For the judgment calls, the Griffin AI analysis engine explains how a given NuGet CVE is reachable in your application, what conditions an attacker would need, and whether the recommended package upgrade is a safe bump or a breaking one — the analysis a senior engineer would otherwise do manually for every finding. When the upgrade is safe, the autonomous auto-fix workflow opens a tested pull request that updates the offending PackageReference and any transitive pins.
If you are currently evaluating a commercial scanner such as Mend for your .NET estate, the Safeguard vs Mend comparison details how reachability and autonomous remediation change the workflow.
NuGet audit quick reference
| Task | Command / setting |
|---|---|
| Scan direct deps | dotnet list package --vulnerable |
| Scan transitive deps | dotnet list package --vulnerable --include-transitive |
| Find deprecated packages | dotnet list package --deprecated |
| Audit on every restore | <NuGetAudit>true</NuGetAudit> |
| Include transitive in audit | <NuGetAuditMode>all</NuGetAuditMode> |
| Set severity floor | <NuGetAuditLevel>moderate</NuGetAuditLevel> |
Recommended routine: migrate any lingering packages.config projects to PackageReference; enable audit-on-restore with NuGetAuditMode=all; run dotnet list package --vulnerable --include-transitive in CI and fail on high severity; then add continuous, reachability-aware scanning so your team fixes what is exploitable rather than triaging the entire graph by hand.
The .NET SDK gives you a genuinely useful auditor for free. Pair it with reachability, policy, and autonomous fixes to turn per-build warnings into a managed remediation pipeline.
See your .NET dependency graph ranked by real exploitability — start free or read the documentation.