Safeguard's software composition analysis (SCA, GA) covers .NET and C# projects that use NuGet. Its CLI support matrix lists *.csproj, *.vbproj and *.fsproj project files with packages.lock.json, Paket's paket.dependencies and paket.lock, and *.deps.json files from built .NET apps. Findings are ranked with reachability analysis (GA for C# and .NET), EPSS and CISA KEV, and Griffin can open a fix pull request, with autonomous remediation switched off until you turn it on.
The rest of this post covers what a .NET team has to get right before any scanner can be trusted, the Safeguard flow step by step from its docs, and where Safeguard stops.
Key takeaways
- SCA is GA for NuGet. The docs list C#, Visual Basic and F# project files,
packages.lock.json, Paket files and*.deps.jsonas supported inputs. packages.configandDirectory.Packages.propsare not named in Safeguard's support matrix. If your repository relies on either, run a test scan and check the inventory before you rely on the results.- Reachability analysis is GA for C# and .NET, both static and dynamic, according to Safeguard's reachability docs, which describe it as "NuGet-aware".
- Prioritisation (GA) combines reachability, EPSS, CISA KEV, runtime context and business context into a 0 to 100 priority score.
- AI Remediate and autonomous remediation are GA but ship switched off, with every category set to Manual. Fix pull requests for NuGet are listed with "Full" support in the AI Remediate docs.
- License compliance is GA. Safeguard reads licence data from package metadata and licence files and applies an allow, deny and review policy.
- Policies and gates are GA and run in the platform.
safeguard ci scan --wait --fail-on <severity>or--fail-on-gate <gate>is how a pipeline fails; plainsafeguard scannever fails a build on findings. - The Safeguard CLI, local runner and IDE extensions are GA. SAST is early access and is not part of this post's flow.
Why .NET dependency risk is easy to miss
A modern ASP.NET Core service rarely has more than a few dozen PackageReference entries in its project file. The restore graph behind those entries is much larger. Every package brings its own dependencies, which bring theirs, and NuGet resolves the whole set at restore time. The project file you read in code review is a short summary of what actually ships.
Three NuGet behaviours make that gap wider than many teams expect.
NuGet picks the lowest applicable version. Microsoft's documentation on package references explains that a version such as 3.6.0 means any version at or above 3.6.0, "with preference for the lowest version". That rule keeps builds predictable, but it also means a transitive dependency can sit on an old, vulnerable minimum for years. A newer, fixed release exists on nuget.org, and nothing in your graph asks for it.
Transitive packages are not pinned unless you ask. Without a lock file, two restores of the same project on different days can resolve different transitive versions if a package publishes a new release inside an open range or a floating version. Microsoft documents an opt-in lock file, packages.lock.json, enabled with the MSBuild property RestorePackagesWithLockFile. Once a project has that file at its root, NuGet always uses it during restore. Locked mode, dotnet restore --locked-mode or RestoreLockedMode, then fails the restore if the lock file and the project disagree.
Two project styles still coexist. SDK-style projects use PackageReference. Older .NET Framework projects often still use packages.config, a flat list kept at the project root. Microsoft's own audit table shows that NuGet audit features arrived at different times for the two styles, and that several MSBuild properties, including NuGetAuditMode, NoWarn and TreatWarningsAsErrors, do not apply to packages.config projects. A solution that mixes both styles needs a scanner that reads both, or a team that knows which projects it is not seeing.
Central package management adds one more layer. With ManagePackageVersionsCentrally set to true, versions move out of each project file and into a Directory.Packages.props file at the repository root. Microsoft notes that only one such file is evaluated per project by default, the closest one up the directory tree. Transitive pinning, enabled with CentralPackageTransitivePinningEnabled, lets that central file override a transitive version without adding a direct reference to every project. That is a useful fix tool. It also means the version that ships may not appear in the project file at all.
The result is a familiar pattern. The build passes. dotnet list package --vulnerable shows nothing at the top level because nobody added --include-transitive. A vulnerable copy of a serialiser or an encoder sits four levels down, pulled in by a logging library, and ships to production.
What the standards ask of .NET teams
None of the major frameworks name NuGet. All of them expect the same outcome: know every third-party component you ship, check it for known vulnerabilities throughout its life, and respond on a risk basis.
| Source | What it asks for | Where NuGet makes it hard |
|---|---|---|
| NIST SP 800-218 (SSDF) PW.4.4 | Verify that acquired commercial, open source and other third-party components meet your requirements throughout their life cycles | Transitive versions change without a lock file, so "verified" at one restore may not hold at the next |
| NIST SP 800-218 PS.3.2 | Collect, safeguard, maintain and share provenance data for every component of each release, for example in an SBOM | packages.config, PackageReference and central management store versions in different places |
| NIST SP 800-218 RV.1.1 and RV.2.2 | Gather information on vulnerabilities in the software and its third-party components, then plan and implement risk responses | Restore warnings are per project and per developer machine, not a fleet-wide record |
| OWASP Top 10:2025 A03 Software Supply Chain Failures | Monitor sources such as CVE, NVD and OSV for the components you use, using SCA or SBOM tools | NuGet audit reads advisory data at restore time, but it does not track history or ownership |
| CISA BOD 26-04 (federal agencies) | Remediate on timelines set by asset exposure, KEV status, exploit automation and technical impact | Requires knowing which deployed apps carry a given CVE, across many repositories |
A note on federal timelines. CISA issued BOD 26-04, "Prioritizing Security Updates Based on Risk", on 10 June 2026. It revokes BOD 22-01, the 2021 directive that created the KEV catalogue, and replaces its fixed deadlines with remediation timelines in its Table 1, driven by four factors: whether the asset is publicly exposed, whether the CVE is on KEV, whether exploitation can be automated, and whether the attacker gains partial or total control. If your internal policy still quotes BOD 22-01 deadlines, treat them as history and read Table 1 instead. BOD 26-04 binds federal civilian agencies, but many contractors and regulated firms copy its logic.
OWASP's 2025 list puts software supply chain failures third and says the category has "the highest average incidence rate" in its contributed data. Its listed weaknesses include CWE-1104, use of unmaintained third-party components, and CWE-1395, dependency on a vulnerable third-party component. Both are exactly what SCA is meant to catch in a NuGet graph.
EPSS and KEV feed the risk-based part. FIRST describes EPSS as a model that estimates the probability a published CVE will be exploited in the next 30 days, published daily for every CVE. CISA's KEV catalogue lists vulnerabilities seen exploited in the wild. Neither replaces CVSS. Together they tell you which of your many NuGet findings deserve this week's attention.
What NuGet already gives you, and where it stops
It is worth being fair to the built-in tooling first, because a good .NET pipeline uses it alongside any SCA platform.
NuGet audit runs during dotnet restore on modern SDKs. It raises NU1901 to NU1904 warnings for low, moderate, high and critical severity packages. Microsoft's docs show how to treat only NU1903 and NU1904 as errors, so that high and critical findings fail the build while lower ones stay as warnings. NuGetAuditSuppress lets you exclude a specific advisory by URL. Audit sources in NuGet.Config let a team that blocks the public package feed still fetch vulnerability data.
The audit mode default matters. Microsoft's page says NuGetAuditMode defaults to all for projects that target net10.0 or higher, and to direct otherwise. A large .NET 8 estate therefore audits only top-level packages unless someone changes the setting. We cover that choice in more depth in what NuGet audit mode direct versus all means and in dotnet list package --vulnerable versus NuGet audit.
Here is where the built-in tools stop:
- They answer "is this project vulnerable right now" for one restore on one machine. They do not answer "which of our 300 repositories carry this version" or "what changed since last week".
- They do not rank findings by whether your code calls the vulnerable function.
- They do not track exceptions, approvals, expiry dates or owners.
- They do not open a pull request with the fix and a test run attached.
- They do not produce an SBOM in SPDX or CycloneDX for customers or auditors.
That is the gap an SCA platform fills.
Which .NET files Safeguard reads
The single most important question for any .NET team is which files the scanner actually parses. Safeguard's docs answer it in two places.
The SCA page lists NuGet among its supported ecosystems and states that "manifests and lockfiles are both honored so resolution matches what actually installs". The CLI support matrix is more specific. For .NET and C#, it lists:
| Package manager | Manifest files | Lock files | Status in the docs |
|---|---|---|---|
| NuGet | *.csproj, *.vbproj, *.fsproj | packages.lock.json | Full support |
| Paket | paket.dependencies | paket.lock | Full support |
| .NET Core | *.deps.json | none | Full support |
The deep dependency scanning page adds that, for NuGet, Safeguard "resolves packages.lock.json and .csproj".
The same support matrix also lists Windows binaries (.exe, .msi, .dll) as supported binary file types, and Azure Container Registry among supported registries. That matters for .NET because a lot of .NET risk ships inside published folders and container images rather than in source.
What the docs do not list
Two common .NET files are missing from that table:
packages.config, used by many older .NET Framework projects.Directory.Packages.props, the central package management file.
Safeguard's docs do not say these files are unsupported. They simply do not list them. Until they do, plan as if they are not covered, and check. A simple way is to scan a repository that uses them and compare Safeguard's component list against dotnet list package --include-transitive output for the same commit. If the counts line up, you have your answer for that repository. If they do not, the gap is usually in exactly these files.
There is a practical workaround for both. For central package management, turning on RestorePackagesWithLockFile makes NuGet write a packages.lock.json per project that records the fully resolved graph, wherever the versions were declared. For a built application, the *.deps.json file that the .NET SDK writes next to your output describes the runtime dependency set, and that file is on Safeguard's list. For packages.config projects, the long-term fix is migration to PackageReference.
Why the lock file matters so much
Without packages.lock.json, any scanner has to resolve the graph itself from the project file and version ranges. That is an estimate of what NuGet would pick, made at scan time, which may differ from what your build picked last Tuesday. With the lock file, the scanner reads exactly what the build restored. For SCA you want the second.
Safeguard's CLI offers a flag, --resolve-transitive, that resolves transitive dependencies through an external package index when no lock file is present. It is a reasonable fallback for a repository you do not control. For your own code, committing a lock file is better: it is deterministic, it does not depend on a network lookup, and it makes the SBOM reproducible. We walk through the NuGet side of this in NuGet lock files and locked mode for repeatable restores.
How transitive depth is resolved
Safeguard's deep dependency scanning (GA) walks every edge of the resolved graph up to 100 levels. At each node, the docs say, it resolves the package URL and version against its vulnerability knowledge graph, computes licence and provenance data, feeds the node into reachability analysis, and records the full path from your project root to that package.
For NuGet the path is the useful part. When a finding sits five levels down, you need to know which direct reference pulled it in, because that is the one you will upgrade, override or replace. Safeguard's UI shows the chain when you click a finding, along with whether that chain is reachable, whether another path to the same package exists, and what the safe version would change elsewhere in the graph.
Shared subgraphs are resolved once. A package used by 40 of your dependencies is analysed one time, not 40. The docs give typical scan times for the dependency walk itself: under a second for small projects, a few seconds for medium ones, and under a minute for very large monorepos. Full SBOM generation from a repository takes longer, from under a minute for small projects to 5 to 15 minutes for projects with 500 or more dependencies, according to the generate SBOM page.
The CLI version of a deep resolve, as the deep dependency docs show it, is:
safeguard scan --dir . --resolve-transitive --transitive-depth 100
The CLI usage page notes that safeguard <command> --help is authoritative if it ever disagrees with the docs, so check safeguard scan --help on your installed version before you script around a flag.
Step by step: scanning a .NET repository with Safeguard
The flow below follows Safeguard's docs. Where the docs do not show a screen or command for something, this post describes the outcome rather than inventing one.
Step 1: make the dependency graph deterministic
Before scanning, give the scanner a lock file to read. In each project, or once in a Directory.Build.props file that every project imports:
<PropertyGroup>
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
</PropertyGroup>
Run dotnet restore, commit the resulting packages.lock.json files, and switch CI restores to locked mode:
dotnet restore --locked-mode
This step is not a Safeguard feature. It is the NuGet setting that makes any SCA result match what your build actually ships.
Step 2: install the Safeguard CLI and sign in
The CLI (GA) is a single static binary with no .NET, Node, Python or Java runtime to install. On Windows, from PowerShell:
irm https://cli.safeguard.sh/install.ps1 | iex
On macOS or Linux:
curl -fsSL https://cli.safeguard.sh/install | bash
Then open the interactive terminal with safeguard, type /login to sign in through the browser, and type /scan . in the folder that holds your solution. Sign-in uses an OAuth 2.0 device-code flow. On a machine with no browser, safeguard login --no-browser prints a URL and code to enter elsewhere.
A scan from the interactive terminal while signed in uploads the SBOM and findings to the platform. Your source code does not leave the machine.
Step 3: or scan from the command line and keep the files
For a local, file-only run:
safeguard scan --dir .
safeguard scan builds an SBOM, matches it against vulnerability databases and writes JSON results into a folder per scan under ./output, or wherever --output points. The usage docs name vuln.json, license.json and collection.json among the files written. This mode is useful for a first look at a repository, for air-gapped machines, and for keeping scan output as a build artifact.
The same command takes other targets, one at a time. For .NET teams the relevant ones are a container image in a private Azure registry, as the docs show:
safeguard scan --image myregistry.azurecr.io/myapp:v1 \
--registry-username "$REGISTRY_USER" \
--registry-password "$REGISTRY_PASSWORD"
and an SBOM you already have, for example one produced by your own build:
safeguard scan --sbom sbom.json
Step 4: connect the repository for continuous scanning
For ongoing coverage, connect the repository in the web app rather than relying on local scans. In the app, open Integrations from the sidebar. Under Source Code (SCM) the docs list GitHub, GitLab, Bitbucket and a generic Git card. Click Connect, choose public or private, supply credentials for a private repository, select the repositories, and set the branch or tag, project name and version for each.
The generate SBOM page lists the generation options you can set: project name, version, branch or tag, Include Dev Dependencies, and Deep Analysis, which turns on Griffin's deeper analysis. For .NET, leave dev dependencies off for a production view and turn them on for a separate build-chain view. Test-only packages such as test frameworks and mocking libraries still matter to the build machine, but they rarely ship.
Safeguard rescans connected projects daily. You can add triggers for On Push, Scheduled and On Release (new tags). A new CVE published against a package you already ship is matched against your existing SBOMs, so you do not wait for the next commit to find out.
If your Git server is self-hosted or firewalled, the local runner (GA) runs the platform's scan jobs on your own machine and uploads only SBOMs and findings. We compare those routes in repo connection, CLI or local runner.
Step 5: read the findings for a NuGet package
Open Projects, pick the project, and go to its vulnerabilities. The vulnerabilities docs list the fields shown for each finding: CVE ID, CVSS score, EPSS score, whether it is exploited in the wild, the affected component and version, the fixed version, a description, references and a reachability verdict. The SCA page adds the full transitive path from the project root and the safe upgrade target.
Here is an illustrative example of how a transitive NuGet finding reads in that view. The package names in the path are made up; the CVE and version ranges are real and taken from the public OSV record for GHSA-ghhp-997w-qr28.
Contoso.Api (project root)
└─ Contoso.Logging.Extensions 3.2.0
└─ Example.Templating 1.8.4
└─ System.Text.Encodings.Web 4.5.0 <- CVE-2021-26701
Affected ranges: 4.0.0 to before 4.5.1, 4.6.0 to before 4.7.2, 5.0.0 to before 5.0.1
Fixed versions: 4.5.1, 4.7.2, 5.0.1
Three things in that example are typical of .NET findings. The vulnerable package is a framework package that nobody added on purpose. It arrived through two libraries that you did add. And it has more than one fixed version, one per release line, so the right fix depends on which line the rest of the graph can accept.
Step 6: let prioritisation sort the queue
Safeguard's prioritisation engine (GA) combines four signals into a single 0 to 100 priority score: reachability, exploitability (EPSS and KEV), runtime context (environment, exposure, data class), and business context declared in policy. The reachability docs show the default bands. Reachable, KEV-listed and in production lands at 90 to 100. Reachable with EPSS above 0.5 in production lands at 75 to 89. Unreachable findings of any severity fall to 0 to 30. The weights are configurable per tenant under Settings → Prioritization.
In the vulnerabilities table the default sort is Priority, highest first. The docs list filters you can type, including:
reachable:true
kev:true
epss:>0.3
env:production reachable:true kev:true
The last one is the P0 backlog. You can also ask Griffin in plain English, for example "Show my P0 findings in production that are reachable". We go deeper on the scoring model in prioritise vulnerabilities with reachability, EPSS and KEV.
Step 7: open a fix pull request
From a finding, View Fix shows the upgrade options and any breaking change warnings. AI Remediate goes further. The docs describe five stages: Griffin analyses the vulnerability and your code, works out the safest upgrade path, assesses breaking changes, creates a pull request, and runs your tests to validate it. Among its dependency abilities, the docs list upgrading vulnerable packages, handling transitive dependency conflicts and updating lock files.
You can run it three ways, per the AI Remediate page:
- Single vulnerability. Open the finding, click AI Remediate, review, then Create Pull Request.
- Bulk. Select several findings, click AI Remediate Selected. Griffin batches compatible fixes into one pull request per repository.
- Page level. Click AI Remediate at the top of the Vulnerabilities page to plan fixes for everything fixable.
The supported ecosystems table on that page lists .NET with NuGet at "Full" support. Each pull request carries a title with the CVE reference, a description of the vulnerability, a summary of the change, breaking change warnings, test results if configured, and advisory links. Breaking change risk is graded Safe, Minor, Major or Critical.
Settings live under Settings → AI Remediate: Auto-Create PRs, Require Approval, Test Integration, Breaking Change Policy and Branch Naming.
The fully autonomous version, where Safeguard opens pull requests on its own without someone clicking, is part of autonomous remediation (GA). It ships switched off. Every category, including Vulnerabilities and Dependencies, starts at Manual, and nothing runs until an administrator turns on the master toggle. Each category then gets a strategy. Safest keeps to backward-compatible fixes. Balanced allows minor versions and patches. Aggressive allows major versions. Smart lets the model choose per item. For a .NET estate, Safest or Balanced is the usual starting point, because a major version bump of an ASP.NET Core or Entity Framework package is rarely a drop-in change. The full matrix is in autonomous remediation in Safeguard.
Step 8: check licences
License compliance (GA) runs on the same graph. Safeguard reads licence data from package metadata, matches SPDX identifiers, matches full licence texts in LICENSE, COPYING and NOTICE files, and reads source headers. When two signals disagree, it raises a licence discrepancy finding.
For NuGet, the package metadata is the license element in the .nuspec, which takes either an SPDX expression or a file in the package. Microsoft's nuspec reference marks the older licenseUrl field as deprecated in favour of license. Packages that still carry only a URL are a common source of "unknown" results in any tool, and Safeguard classifies unknown or unidentified licences as needing review.
Policies are written in YAML with allow, deny and require_review lists, and can be scoped by asset label, so a redistributed SDK can follow stricter rules than an internal service. Safeguard can generate a NOTICE or THIRD-PARTY-NOTICES file and a licence text folder for releases, though the docs say there is no CLI command for that yet; it is done in the app. safeguard scan writes the licences it found into license.json. For more on the obligations themselves, see open source licence obligations, OpenChain and attribution.
Step 9: gate the pipeline
Policies and gates (GA) are evaluated in the platform, not by the CLI. The CLI docs are explicit: safeguard scan exits 0 whenever the scan ran, whatever it found; 1 when the command itself failed; and 2 when results were written but the vulnerability scan did not run, so a script never mistakes that for a clean pass. There is no severity threshold flag on safeguard scan.
To fail a build, use safeguard ci scan. Many .NET teams run Azure Pipelines, so here is the pipeline from Safeguard's Azure Pipelines guide, unchanged:
trigger:
branches:
include: [main]
pr:
branches:
include: [main]
pool:
vmImage: ubuntu-latest
steps:
- checkout: self
- bash: |
set -eu
export SAFEGUARD_INSTALL_DIR="$(Agent.TempDirectory)/safeguard-bin"
curl -fsSL https://cli.safeguard.sh/install | bash
"$SAFEGUARD_INSTALL_DIR/safeguard" ci scan --wait --fail-on critical
displayName: Safeguard scan
env:
SAFEGUARD_API_KEY: $(SAFEGUARD_API_KEY)
Setup takes three steps. Create a key under Settings → Developer with the scopes scans:trigger and scans:read, optionally locked to one project. In Azure DevOps, add it as a pipeline variable named SAFEGUARD_API_KEY with "Keep this value secret" on. Then commit the file. Secret variables reach a script only through env, as shown. For Azure Repos, the pr block is ignored, so add a Build validation branch policy on the target branch instead.
The options that matter most:
| Change | How, per the docs |
|---|---|
| Report without failing | Drop --fail-on critical |
| Another threshold | --fail-on high, medium or low |
| Fail on a named policy gate | --fail-on-gate <gate> with --wait |
| Scan on the agent, results only leave it | Add --local |
| Pin the CLI version | export SAFEGUARD_VERSION=<version> before the install line |
The guide uses an Ubuntu agent. The CI docs say every system runs the CLI in a plain shell step on a Linux or macOS runner with bash and curl, so a Windows-only build agent needs a separate Linux job for the scan step. That is usually fine for SCA, because the scan reads the lock files and project files rather than building the solution.
--fail-on covers dependency vulnerabilities. By default safeguard ci scan runs on Safeguard against the connected repository and also runs SAST (early access); the CI docs say SAST findings are shown in Safeguard but are not part of --fail-on yet. With --local, the scan covers SBOM and dependency vulnerabilities only.
Gate actions are Block, Warn and Notify. Block fails the gate. Warn passes with a warning naming the policy. Notify passes and sends a notification. See plain English security policies to release gates for how to write the policies behind a gate.
Step 10: handle exceptions properly
Not every finding gets fixed. The vulnerabilities page offers Mark False Positive, with a justification and approval if your policy requires it, and Accept Risk, with an expiry date and a business justification. Gate exceptions follow a similar request, approve and audit flow. Licence exceptions use the same workflow, with a named approver and auto re-evaluation at expiry.
Contrast that with NuGetAuditSuppress, which is a line in a project file with no owner and no end date. Both have a place. The platform record is the one an auditor can follow.
Reachability for C# and .NET, in detail
Reachability is where SCA either earns trust or loses it, so it is worth being precise about what the docs say.
Safeguard's reachability page lists support per language. For C# and .NET, static reachability is GA, dynamic reachability is GA, and the note reads "NuGet-aware". For comparison, the same table marks Ruby and PHP static reachability as Preview and Swift and Objective-C as Preview for both. .NET is in the GA group.
The docs describe three kinds of reachability:
- Static reachability builds a call graph from your source, lock file and compiled code, and checks whether a path leads to the vulnerable function.
- Dynamic reachability uses a runtime collector to record which modules and functions are actually loaded and called in your workloads, and marks never-executed vulnerable paths as cold. That needs the collector deployed where your app runs; static analysis does not.
- Configuration reachability reads framework configuration to see whether code is wired in at all.
Each finding then gets one of four verdicts: Reachable, Conditionally reachable, Unreachable or Unknown. Unknown is treated as potentially reachable.
Where reachability is weakest in .NET
The docs are honest that reachability is weakest with reflection, dynamic dispatch and code paths that only run under specific configuration. .NET has plenty of all three:
- Dependency injection. ASP.NET Core resolves services from a container at runtime. A call graph sees the interface, and has to infer which implementation runs.
- Reflection and attributes. Serialisers, model binders, MVC controllers and many plugin systems discover types by reflection.
- Source generators and dynamic code. Generated code is real code, but it may not be visible in the source tree a scanner reads.
- Data-driven vulnerabilities. A parser bug can be triggered by malicious input even when static analysis cannot draw a neat path.
Safeguard marks these cases Unknown and does not suppress them. That is the right default, and it means a .NET codebase with heavy reflection will show more Unknown verdicts than a small console tool. Treat Unknown as "still in the queue", and lean on KEV, EPSS and runtime context for those findings.
A worked example: one transitive CVE across a .NET estate
Suppose an advisory lands for a package your teams never reference directly, and a security lead asks the three usual questions. Which apps ship it? Which ones matter? How fast can we fix them?
Which apps ship it. Because Safeguard rescans connected projects daily and matches new CVEs against existing SBOMs, the affected projects appear in the vulnerabilities view without anyone running a scan. Filter by component name to list every project that carries the vulnerable version.
Which ones matter. Sort by Priority. An internet-facing production API where the vulnerable encoder is reachable, and the CVE is on KEV, rises to the top. An internal batch job where the call path is Unreachable drops into the backlog. A desktop tool with Unknown reachability sits in between, and someone looks at it by hand.
How fast can we fix them. Select the high-priority findings and run bulk AI Remediate. Griffin proposes one pull request per repository. Where the vulnerable package is transitive, the usual NuGet fixes are to upgrade the direct parent to a release that depends on the fixed version, to add a direct PackageReference at the fixed version, or, under central package management, to pin it centrally with transitive pinning. Microsoft notes that transitive pinning does not allow downgrades. Review each pull request's breaking change grade, let CI run, and merge. We cover the trade-offs between those NuGet fix options in fixing a vulnerable transitive NuGet package.
Decision table: which route for which .NET setup
| Your setup | Recommended route | What to check first |
|---|---|---|
SDK-style projects with packages.lock.json committed | Connect the repository; add safeguard ci scan --wait --fail-on high in CI | Nothing special; this is the best case |
| SDK-style projects, no lock file | Turn on RestorePackagesWithLockFile, commit, then scan | Compare component counts before and after |
Central package management (Directory.Packages.props) | Generate lock files per project and scan those | The props file is not named in the support matrix |
Legacy .NET Framework with packages.config | Scan the built output's *.deps.json where present, or migrate to PackageReference | packages.config is not named in the support matrix |
| Paket | Scan as normal; the docs list paket.dependencies and paket.lock | Confirm results on one repository first |
| Containerised ASP.NET Core on Azure Container Registry | safeguard scan --image against the registry, plus source scanning | Base image findings and app findings need different owners |
| Self-hosted or firewalled Azure DevOps Server | safeguard ci scan --local, or the local runner | Only results leave the agent |
| Windows-only build agents | Run the scan step in a separate Linux job | The CI docs require a Linux or macOS runner with bash and curl |
| Air-gapped build network | safeguard scan --dir . with offline databases loaded by safeguard update --import | Database freshness is now your job |
Requirement versus what Safeguard does versus what you still own
| Requirement | What Safeguard does (label) | What you still own |
|---|---|---|
| Inventory every NuGet component, direct and transitive (SSDF PS.3.2) | SBOM generation and deep dependency scanning from project and lock files (GA) | Committing lock files; covering file types the docs do not list |
| Check components for known vulnerabilities throughout their life (SSDF PW.4.4) | Daily rescans of connected projects against NVD, GHSA, OSV, CISA KEV and vendor bulletins (GA) | Connecting every repository that ships |
| Prioritise on risk (BOD 26-04 Table 1 logic) | Priority score from reachability, EPSS, KEV, runtime and business context (GA) | Accurate asset exposure and environment labels |
| Respond to vulnerabilities (SSDF RV.2.2) | AI Remediate fix pull requests; autonomous remediation (GA, ships off) | Reviewing breaking changes, running tests, merging |
| Stop new vulnerable versions shipping | Policies and gates with Block, Warn and Notify; --fail-on and --fail-on-gate in CI (GA) | Choosing thresholds and owning exceptions |
| Licence obligations | Licence detection, classification, policy and attribution files (GA) | Legal judgement on unusual licences |
What Safeguard does not do
Honest limits matter more in SCA than in most categories, because a missed file type looks exactly like a clean result.
- It does not list every NuGet file type. The support matrix names
*.csproj,*.vbproj,*.fsproj,packages.lock.json, Paket files and*.deps.json. It does not namepackages.configorDirectory.Packages.props. Do not assume coverage for those until you have tested it on your own repositories. - It does not replace NuGet audit at restore time. Safeguard scans in CI, on a schedule and from the CLI. NuGet audit warns a developer the moment they restore on their own machine. Keep both.
- It does not fail a build from
safeguard scan. That command always exits 0 when the scan ran. Gating happens throughsafeguard ci scanwith--fail-onor--fail-on-gate, or by parsing the output files yourself. - It does not run the CI step on Windows-only agents. The CI docs require a Linux or macOS runner with bash and curl. The CLI itself runs on Windows for local scans.
- Reachability cannot see everything. Reflection, dependency injection, dynamic dispatch and data-driven bugs produce Unknown verdicts. Unknown is not suppressed, so you still review those findings.
- Dynamic reachability needs a runtime collector. Without it, you get static and configuration reachability only.
- It does not merge fix pull requests for you by default. Autonomous remediation ships off, every category Manual. Even when it is on, major version upgrades of framework packages need human review.
- It does not patch the .NET runtime on your servers. A vulnerable shared framework installed on a host, rather than shipped in your app or image, is fixed by updating the runtime through your operating system or Microsoft's channels.
- It does not query findings by priority from the CLI. The reachability docs say there is no CLI command for this yet, and the
/v1REST endpoints described there are not yet available. Use the app or the Griffin search. - It does not decide your licence policy. It classifies and enforces what you write; legal still decides what is acceptable.
Common mistakes
Scanning without a lock file. The scanner then estimates the graph. Commit packages.lock.json and restore in locked mode so the SBOM matches the build.
Assuming a mixed solution is fully covered. A solution with SDK-style projects and a few old packages.config projects can look complete in a dashboard while the legacy projects are missing. Count projects in the SBOM against projects in the .sln.
Leaving NuGet audit on direct for .NET 8 and 9 projects. The default for anything below net10.0 is direct. Set NuGetAuditMode to all if you want developers to see transitive warnings locally.
Treating safeguard scan exit code 0 as a pass. It means the scan ran. Use safeguard ci scan --wait --fail-on <severity> or parse vuln.json.
Fixing a transitive package by editing the wrong file. Under central package management the version lives in Directory.Packages.props. A fix that adds a version to a project file may be overridden or rejected. Read every fix pull request's diff, especially in repositories that use central management, because the AI Remediate docs do not say which file it edits in that layout.
Turning on autonomous remediation with Aggressive for everything. Major version upgrades of ASP.NET Core, Entity Framework or serialisation packages often change behaviour. Start with Safest or Balanced, and require approval.
Ignoring Unknown reachability. In reflection-heavy .NET code, Unknown is common. It means "could not decide", not "safe".
Forgetting the published output and the image. Source scans miss packages added at publish time, self-contained runtime files and base image packages. Scan the container image or the built output too.
Mapping the API key wrongly in Azure Pipelines. Secret variables reach scripts only through env. An AUTHENTICATION_REQUIRED error usually means the mapping is missing.
FAQ
Does Safeguard support NuGet and C# for SCA?
Yes. SCA is GA, and NuGet is on Safeguard's list of supported ecosystems. The CLI support matrix lists *.csproj, *.vbproj and *.fsproj manifests with packages.lock.json, Paket's paket.dependencies and paket.lock, and *.deps.json files.
Does Safeguard read packages.config?
The docs do not list packages.config. Test on a representative repository before relying on it, and consider migrating those projects to PackageReference or scanning their built output.
Does Safeguard support NuGet central package management?
Directory.Packages.props is not named in the support matrix. Turn on RestorePackagesWithLockFile so each project writes a packages.lock.json with the fully resolved graph, and Safeguard will read that.
Is reachability analysis available for .NET?
Yes. Safeguard's reachability docs list C# and .NET as GA for both static and dynamic reachability, described as NuGet-aware. Dynamic reachability needs a runtime collector in your workloads.
How does Safeguard decide which NuGet vulnerability to fix first?
It computes a 0 to 100 priority score from reachability, EPSS, CISA KEV status, public exploit availability, environment, exposure, data sensitivity and business criticality. Weights are configurable per tenant under Settings → Prioritization.
Can Safeguard open a pull request to fix a vulnerable NuGet package?
Yes. AI Remediate lists .NET and NuGet with full support. It can upgrade vulnerable packages, handle transitive conflicts and update lock files, and you review the pull request before merging. Fully autonomous remediation is GA but ships switched off.
How do I fail an Azure DevOps build on vulnerable NuGet packages with Safeguard?
Use safeguard ci scan --wait --fail-on critical (or high, medium, low) in a bash step, with SAFEGUARD_API_KEY mapped through env. For Azure Repos, add a Build validation branch policy because the pr trigger is ignored.
Do I still need dotnet list package --vulnerable or NuGet audit?
Yes. They give developers immediate feedback at restore time. Safeguard adds the fleet-wide view, prioritisation, history, exceptions, fix pull requests and SBOM output.
Does my source code leave my machine when I scan?
Not with the CLI or local runner. A scan from the interactive terminal uploads the SBOM and findings, never the source. With safeguard ci scan --local, only results leave the runner.
Can Safeguard scan .NET container images and binaries?
The CLI support matrix lists container registries including Azure Container Registry, and Windows binary types including .exe, .msi and .dll. Use safeguard scan --image for images.
Fixing NuGet findings when the fix is slow to arrive
Most NuGet findings have a fixed release on nuget.org, and the steps above get it into your build. The harder cases are when the fix sits behind a parent package that has not updated, or when no fixed version exists yet, and that is what the next section covers.
Fix it without waiting for upstream
Whatever the vulnerability, most of the risk sits in two gaps. The first is the time between a fix being published and that fix actually running in your production builds. The second, and worse, is the time before any fix exists at all, while a maintainer works out a patch and a release schedule. Waiting through both gaps is a habit, not a requirement. Safeguard gives teams three ways to close them.
1. Autonomous remediation: pull requests, not tickets
When a fixed version exists, Safeguard's Griffin models open a tested, policy gated pull request that moves the affected dependency to the safe version. You can run it on one repository or in bulk across every project that carries the vulnerable version. Remediation categories include dependencies, licenses, security posture and findings, and each one is set separately. Reachability analysis ranks the vulnerable code paths your application actually calls first, so the first pull requests you merge are the ones that matter most. Autonomous remediation ships switched off, with every category set to Manual, so your team decides how much runs on its own and what still needs a human approval.
2. Gold Open Source: the safe version in seconds, free
gold.safeguard.sh is free and needs no login. Search any CVE or package to see affected and fixed versions, CVSS, EPSS, CISA KEV status and a recommended safe version, or use the "Am I affected?" checker. The same data is available through a free read API, README status badges and RSS threat feeds.
3. Gold Registry: start from components that are already fixed
Gold Registry provides hardened, drop in replacements for upstream packages and container images. Every Gold artifact is held continuously to zero exploitable critical or high CVEs and a completed license check, with signed build provenance (signed with Safeguard's key and verifiable with cosign, SLSA Build Level 3 as the design target) and a public evidence page you can hand to an auditor. An artifact loses its Gold badge if it regresses. Teams can point their own package directory at the registry so hardened equivalents resolve without changing how developers install anything.
When upstream has no fix yet, or will not ship one, Safeguard first works to get the fix accepted upstream. If the issue is severe or upstream will not move, Safeguard builds a fork, fixes the vulnerability there and publishes a hardened Gold build, so you are not stuck on a maintainer's release schedule. You can also request a hardened, compatibility tested build of a specific package. Ask which packages it covers before you plan around it.
| Your situation | What to use |
|---|---|
| A fixed version exists | Autonomous remediation opens the upgrade pull request; Gold Open Source confirms the safe version |
| You need to know today whether you are affected | Gold Open Source lookup and "Am I affected?" checker, free and without login |
| You want vulnerable versions to stop shipping | Safeguard policies and gates in CI |
| Upstream has no fix yet, or will not ship one | A custom Gold request; fork and patch |
| You want to start clean on new projects | Pull hardened components and images from Gold Registry |
One honest limit: when the vulnerable code lives in a vendor appliance, a hosted service or an operating system runtime you do not build yourself, the fix still has to come from that vendor or distribution. The steps above apply to the open source dependencies and container images you build and ship, which is where most teams carry the largest and least tracked share of their exposure.
See how Safeguard would help
The first demo is a real Safeguard CLI session. You install the CLI, run safeguard to open the interactive terminal and ask it to "fix the lodash CVE in this repo". It reads the finding and advisory, asks before any command that changes something, edits package.json to lodash 4.17.21, runs npm install and npm test with approval, prints a summary and pushes nothing, then shows /permissions, /model and /scan . The demo uses an npm project, but the same loop applies to a vulnerable NuGet package in a C# repository: the assistant reads the finding, proposes the version change, and asks before running anything that changes your tree.
Click through it at your own pace, or open it full screen.
The second demo sets up the local runner on your own machine, approves it with its device code, and then scans from the terminal. This is the route for .NET teams on a self-hosted or firewalled Azure DevOps Server, because the runner uploads SBOMs and findings and never your source.
Click through it at your own pace, or open it full screen.
The third demo connects Claude to Safeguard with OAuth and lets you choose which Safeguard tools Claude may use. Add Safeguard from Claude's connector directory, where it is listed as a community connector, rather than configuring a custom server. Once connected, you can ask Claude which of your .NET projects carry a given NuGet package and version, and it answers from your own Safeguard data.
Click through it at your own pace, or open it full screen.
To try it on your own solution, download the CLI from safeguard.sh/download and read the setup guides at docs.safeguard.sh/docs.
Quick check
Your .NET 8 solution uses central package management and no lock files. What should you change before trusting a Safeguard SCA scan?
Turn on RestorePackagesWithLockFile so each project writes a packages.lock.json with the fully resolved graph, commit those files, and restore in locked mode. Safeguard's support matrix lists packages.lock.json but does not name Directory.Packages.props, so the lock files are what make the scan match the build.
A pipeline runs safeguard scan --dir . and exits 0. Does that mean no vulnerable NuGet packages were found?
No. safeguard scan exits 0 whenever the scan ran, whatever it found. To fail a build on findings, use safeguard ci scan --wait --fail-on <severity> or --fail-on-gate <gate>, or parse the vuln.json it writes.
Sources
- Safeguard Docs: Software Composition Analysis (SCA)
- Safeguard Docs: CLI support matrix
- Safeguard Docs: Deep Dependency Scanning
- Safeguard Docs: Vulnerability Prioritization (Reachability Analysis)
- Safeguard Docs: ESSCM Vulnerabilities
- Safeguard Docs: AI Remediate
- Safeguard Docs: Generate SBOM
- Safeguard Docs: License Compliance
- Safeguard Docs: Safeguard CLI
- Safeguard Docs: CLI Usage
- Safeguard Docs: CI/CD Integration
- Safeguard Docs: Azure Pipelines
- Safeguard Docs: Policies and Gates
- Safeguard Docs: Continuous Scanning
- Microsoft Learn: PackageReference in project files (lock files and locked mode)
- Microsoft Learn: Auditing package dependencies for security vulnerabilities
- Microsoft Learn: Central Package Management
- Microsoft Learn: packages.config reference
- Microsoft Learn: .nuspec reference
- NIST SP 800-218, Secure Software Development Framework
- CISA BOD 26-04: Prioritizing Security Updates Based on Risk
- CISA Known Exploited Vulnerabilities Catalog
- FIRST: Exploit Prediction Scoring System (EPSS)
- OWASP Top 10:2025 A03 Software Supply Chain Failures
- CycloneDX specification overview
- SPDX specifications
- GitHub Advisory Database: NuGet ecosystem
- OSV: GHSA-ghhp-997w-qr28 (CVE-2021-26701)
Last reviewed: 25 October 2026. Product capabilities change; check docs.safeguard.sh for the current state.