Safeguard
Security Guides

.NET Dependency Vulnerability Scanning: A Practical Guide

How to scan .NET dependencies for known vulnerabilities using dotnet list package, NuGet audit, and reachability-aware SCA, and how to wire it into CI so nothing ships with a known critical.

Marcus Chen
AppSec Engineer
5 min read

A .NET service you wrote six months ago and haven't touched can become vulnerable overnight — not because your code changed, but because a CVE was published against a NuGet package deep in its dependency tree. Dependency vulnerability scanning is how you find out before an attacker does. The good news is that .NET now ships capable scanning out of the box; the gap most teams have is turning it from a command you occasionally run into a gate that blocks risky releases. This guide covers the built-in tooling, its limits, and how to build a scanning workflow that actually holds.

Start with the built-in tools

The .NET SDK can enumerate vulnerable and deprecated packages without any third-party tooling. The core command:

dotnet list package --vulnerable --include-transitive

This resolves your project's full dependency graph and checks each package against the GitHub Advisory Database, printing the package, the resolved version, severity, and the advisory URL. The --include-transitive flag is essential — most real-world vulnerabilities live in dependencies-of-dependencies, not the packages you declared directly. Two companions round it out:

dotnet list package --deprecated      # maintainer-flagged, often unmaintained
dotnet list package --outdated        # newer versions available

Deprecated and abandoned packages matter because an unmaintained dependency won't get a patch when a vulnerability is found — the fix path becomes "replace it," which takes planning.

Turn on NuGet audit and fail the build

Running a command manually catches nothing on the day you forget. Since .NET 8, NuGet Audit runs during dotnet restore and emits warnings (NU1901NU1904) for known-vulnerable packages; .NET 9 audits transitive dependencies by default. Configure it to be strict and blocking:

<PropertyGroup>
  <NuGetAudit>true</NuGetAudit>
  <NuGetAuditMode>all</NuGetAuditMode>       <!-- direct + transitive -->
  <NuGetAuditLevel>moderate</NuGetAuditLevel> <!-- floor severity to warn on -->
  <WarningsAsErrors>NU1901;NU1902;NU1903;NU1904</WarningsAsErrors>
</PropertyGroup>

Now a restore in CI fails the moment a moderate-or-worse advisory matches your tree. This is the single most effective change for most teams: it moves scanning from "someone's responsibility" to "the build's responsibility."

Understand what the built-in tools don't do

The SDK tooling is a strong floor, not a ceiling. Its limits are worth naming so you know what to layer on:

  • No reachability. It tells you a vulnerable package is present, not whether your code ever calls the vulnerable function. Most teams find the majority of flagged CVEs are in code paths they never execute, so undifferentiated alerts create fatigue.
  • Advisory-database lag. Coverage depends on the advisory feed; newly disclosed issues and ecosystem-specific advisories can arrive late or incomplete.
  • No fix orchestration. It won't open a pull request, test the bump, or tell you if the upgrade is breaking.
  • Point-in-time. It reflects the moment you ran it; continuous monitoring against new disclosures is a separate capability.

This is where dedicated software composition analysis earns its place — adding reachability analysis, continuous monitoring, and remediation on top of the raw advisory match.

Build the scanning workflow into CI

A durable workflow has four stages. Wire them so each is automatic:

StageToolGate
On restoreNuGet auditFail on moderate+ advisories
On PRdotnet list package --vulnerable + SCABlock merge on new critical/high
NightlyFull SCA scan of default branchTicket new findings
On releaseSBOM generation + policy gateBlock ship on unremediated critical

Here's a minimal CI step that fails when any vulnerable package is present:

dotnet restore --locked-mode
if dotnet list package --vulnerable --include-transitive | grep -q '>='; then
  echo "Vulnerable packages found"; exit 1
fi

Run the same scan locally through the Safeguard CLI so developers catch issues before they push, and keep the CI gate as the backstop. Consistency between local and CI is what keeps scanning from becoming a merge-time surprise.

Prioritize and remediate without drowning

Finding vulnerabilities is easy; deciding what to fix first is the hard part. Rank by a combination of severity, reachability (does your code call it?), exploit availability, and whether a fix exists. A critical CVE in a method you never invoke is a lower priority than a high-severity one directly on your request path. When a fixed version exists, the fastest safe path is an automated upgrade PR that also runs your test suite — which is exactly what Safeguard's auto-fix engine produces, so remediation is a review-and-merge rather than a research project.

A scanning checklist

  • dotnet list package --vulnerable --include-transitive runs in CI
  • NuGet audit on, transitive, warnings-as-errors at your severity floor
  • SCA layered on for reachability + continuous monitoring
  • Nightly scan of the default branch, not just PRs
  • SBOM + policy gate at release
  • Findings ranked by severity and reachability, not raw count

How Safeguard Helps

Safeguard takes .NET dependency scanning past the "list of CVEs" stage. It continuously monitors your NuGet dependency graph — direct and transitive — against fresh advisory data, and its reachability analysis tells you which flagged vulnerabilities actually touch code your application runs, so a team of three isn't triaging three hundred equal-looking alerts. Findings arrive prioritized by exploitability, and when a fix exists the auto-fix workflow opens a tested upgrade pull request. It runs from the CLI in any CI system and enforces policy gates that block a release with unremediated critical vulnerabilities. Teams comparing scanning approaches often look at the Safeguard vs Snyk page, and can see what's included at each tier on the pricing page.

Get started free at app.safeguard.sh/register and read the CI integration docs at docs.safeguard.sh.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.