Modern .NET applications rarely depend only on code written by the development team. A typical project may pull in dozens or even hundreds of direct and transitive NuGet packages.
That dependency model makes software development faster, but it also creates a security responsibility.
A vulnerable package can enter a production application through a dependency that nobody explicitly added to the project. If vulnerability checks happen only during periodic security reviews, a risky dependency may already have reached a release branch.
A better approach is to make dependency security part of the build process itself.
In this article, we will build a practical vulnerability gate for a .NET CI pipeline using the .NET CLI, NuGet package auditing, and CI failure conditions. The goal is not simply to generate a vulnerability report. The goal is to make known dependency vulnerabilities capable of stopping an unsafe build.
Why NuGet Vulnerability Gates Matter
Consider a project with this dependency structure:
OrderApi
├── Package A
│ └── Package C
├── Package B
│ └── Package D
└── Package E
└── Package C
The application may directly reference only A, B, and E.
However, C and D are still part of the application's dependency graph.
This creates a common security problem:
Developer adds package
|
v
Transitive dependency introduced
|
v
Vulnerability disclosed
|
v
Build continues normally
|
v
Application is deployed
A vulnerability gate changes the flow:
Dependency restore
|
v
Vulnerability audit
|
v
Policy evaluation
|
+----+----+
| |
Pass Fail
| |
v v
Build Stop
The important difference is that security becomes an enforceable engineering control rather than an informational report.
Understanding NuGet Package Auditing
The .NET SDK and NuGet tooling provide package vulnerability auditing capabilities.
A basic audit can be performed from a project directory with:
dotnet restore
Depending on the SDK and project configuration, vulnerability information can be reported during restore.
You can also explicitly inspect project dependencies with:
dotnet list package --vulnerable
For projects using newer .NET SDK capabilities, the package auditing experience can be integrated more directly into the restore/build workflow.
The exact command and available options depend on the SDK version used by the repository, so the CI environment should always use a known and controlled SDK version.
Start With a Deterministic SDK
Before creating a security gate, make the build environment predictable.
A repository can specify its expected .NET SDK using global.json:
{
"sdk": {
"version": "9.0.000",
"rollForward": "latestFeature"
}
}
The version should match an SDK actually supported by the project.
The purpose of this file is not security by itself. It helps ensure that developers and CI agents are using compatible tooling so that restore, build, and audit behavior remains consistent.
Do not copy the example version blindly into production. Use the SDK version appropriate for your application.
Building the First Vulnerability Gate
A simple CI strategy is:
Restore dependencies.
Run the vulnerability audit.
Fail the job if the defined security policy is violated.
Continue to compilation and tests only when the dependency check passes.
A generic shell workflow might look like this:
dotnet restore
dotnet list package --vulnerable
if [ $? -ne 0 ]; then
echo "Dependency vulnerability check failed."
exit 1
fi
dotnet build --no-restore
dotnet test --no-build
However, production pipelines should avoid relying solely on the exit code of a reporting command if the command does not provide the precise failure semantics required by the organization's policy.
The security gate should explicitly define what constitutes a failure.
Define a Vulnerability Policy
Not every security finding necessarily deserves the same response.
A practical policy might be:
| Severity | CI Policy | Typical Action |
|---|---|---|
| Critical | Block | Upgrade or replace dependency |
| High | Block | Remediate before release |
| Moderate | Review or block | Evaluate exposure |
| Low | Report | Track and review |
| Informational | Report | No automatic failure |
The exact thresholds should be determined by the application's risk profile and organizational security requirements.
For an internet-facing service handling sensitive workloads, the policy may be stricter than for an internal development utility.
The key is to make the policy explicit.
Avoid the "Ignore Everything" Trap
A common reaction to dependency warnings is to suppress them.
For example, a team may decide that a vulnerable transitive package is not currently exploitable and add an exception.
Exceptions can be legitimate, but they should have an owner and expiration date.
A useful exception record should contain:
Package:
Vulnerable version:
Advisory:
Reason for temporary acceptance:
Affected application:
Risk assessment:
Owner:
Created:
Expiration:
Remediation plan:
The problem is not having exceptions.
The problem is having permanent exceptions that nobody reviews.
Handling Transitive Dependencies
One of the most important features of dependency auditing is visibility into transitive packages.
You can inspect the dependency graph with:
dotnet list package --include-transitive
This can help identify where a vulnerable package entered the application.
For example:
MyApi
└── Authentication.Library 5.x
└── Utility.Library 2.x
└── Vulnerable.Library 1.4
The application may not have a direct reference to Vulnerable.Library.
That does not mean the package is irrelevant.
The correct remediation may involve upgrading the direct dependency that brings it into the graph.
Example CI Pipeline
A GitHub Actions-style workflow can separate restore, audit, build, and test stages:
name: .NET Security Gate
on:
pull_request:
push:
branches:
- main
jobs:
security:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '9.0.x'
- name: Restore
run: dotnet restore
- name: Audit dependencies
run: dotnet list package --vulnerable
- name: Build
run: dotnet build --no-restore --configuration Release
- name: Test
run: dotnet test --no-build --configuration Release
For an actual production pipeline, the audit step should be configured to enforce the organization's defined severity policy rather than merely printing findings.
The pipeline should also pin or centrally control the tooling versions used by security checks where practical.
Pull Request Security Gates
Running dependency audits only on the main branch is too late.
A better workflow runs the gate during pull requests.
The flow becomes:
Developer changes dependency
|
v
Pull Request
|
v
Dependency restore
|
v
Security audit
|
+----+----+
| |
Pass Fail
| |
v v
Merge Fix dependency
This provides feedback before the dependency change reaches production branches.
It also makes security part of normal developer workflow instead of creating a separate security process.
Security Gate vs Dependency Update Automation
These solve different problems.
| Capability | Vulnerability Gate | Dependency Update |
|---|---|---|
| Detects known vulnerabilities | Yes | Usually |
| Blocks unsafe builds | Yes | No |
| Updates packages | No | Yes |
| Enforces policy | Yes | Usually not |
| Works as release control | Yes | No |
| Removes vulnerable package automatically | No | Potentially |
A mature pipeline can use both.
Automated updates can propose dependency upgrades, while the vulnerability gate ensures that unacceptable vulnerabilities do not pass into protected branches.
Production Considerations
Cache Carefully
CI systems often cache NuGet packages to speed up builds.
Caching can improve performance, but it should not turn the dependency audit into a stale artifact.
The restore and audit process should still operate against the dependency metadata and vulnerability information expected by the security process.
Protect the Main Branch
A vulnerability gate becomes much more useful when the protected branch requires the security job to pass.
Otherwise, developers may see a failed security check but still merge the change.
Keep Build and Security Results Separate
A useful pipeline should distinguish:
Compilation failure
Test failure
Dependency vulnerability
Configuration failure
Infrastructure failure
This makes failures easier to diagnose.
Monitor the Audit Source
Dependency auditing depends on vulnerability intelligence.
A pipeline should document where vulnerability information comes from and how often that information is refreshed.
If the advisory data source is unavailable, the organization should have an explicit policy for whether builds fail closed, fail open, or enter a separate warning state.
Common Mistakes
Auditing Only Direct Packages
This misses vulnerabilities introduced through transitive dependencies.
Running Security Checks Only Before Releases
By that point, vulnerable dependencies may already exist in multiple branches.
Treating Every Vulnerability as Automatically Exploitable
A vulnerability report identifies a known issue. It does not automatically establish exploitability in every application.
Risk assessment still matters.
Blocking Builds Without an Exception Process
A strict gate without a documented exception mechanism can lead developers to disable or bypass the control.
Using Uncontrolled SDK Versions
Different SDK versions can produce different restore and tooling behavior.
Standardize the build environment.
Troubleshooting a Failing Dependency Gate
When the pipeline fails, start with the package name and affected version.
Then inspect the dependency graph:
dotnet list package --include-transitive
Determine whether the vulnerable package is:
A direct dependency
A transitive dependency
A development-only dependency
A runtime dependency
A test dependency
Next, determine whether an upgrade is available.
If the package is transitive, identify which direct dependency introduces it.
Finally, verify the application impact before creating an exception.
Do not solve the problem by simply disabling the audit.
Frequently Asked Questions
Does a NuGet vulnerability warning mean the application is compromised?
No. A vulnerability finding indicates that a package version has a known security issue. It does not prove that the application has been compromised or that the vulnerable code path is exploitable in the application's context.
Should vulnerability checks run on every build?
For CI pipelines, running them on pull requests and protected branches provides strong coverage. Local development checks can also be useful, but the authoritative policy should live in CI.
Should low-severity vulnerabilities block production builds?
Not necessarily. Severity thresholds should reflect the application's risk profile and organizational security policy.
What about vulnerabilities in transitive dependencies?
They should be evaluated just like direct dependencies. The remediation may require upgrading the direct package that introduces the vulnerable transitive dependency.
Can a vulnerability gate replace software composition analysis?
No. A vulnerability gate is one component of a broader software supply-chain security strategy. Organizations may also need dependency inventory, license analysis, provenance controls, package integrity controls, secret scanning, static analysis, and runtime security controls.
Best Practices Checklist
Define severity-based CI policies.
Audit direct and transitive dependencies.
Run security checks on pull requests.
Protect important branches with required checks.
Keep the .NET SDK version controlled.
Maintain a documented vulnerability exception process.
Give exceptions owners and expiration dates.
Separate security failures from compilation and test failures.
Monitor the vulnerability data source.
Review the policy periodically as the application and threat landscape change.
Conclusion
NuGet vulnerability scanning becomes much more valuable when it is connected to an enforceable CI policy.
The objective is not to produce another security report that developers have to remember to review. The objective is to make dependency security part of the software delivery path.
A well-designed gate checks both direct and transitive dependencies, applies clearly defined severity rules, runs before code reaches protected branches, and provides a controlled exception process when remediation cannot happen immediately.
For .NET teams, this creates a practical security boundary around one of the most important parts of the modern application stack: the dependency graph itself.

Join the conversation! Your thoughts help the community grow.