Modern .NET applications rarely depend only on code written by the development team. A typical application may use dozens or even hundreds of NuGet packages, including direct dependencies, transitive dependencies, build tools, serializers, database providers, authentication libraries, and framework extensions.
That dependency graph creates a security and maintenance challenge.
A package may contain a known vulnerability, become outdated, introduce a licensing concern, or pull another vulnerable package transitively.
A simple application may look like this:
Application
|
+-- Package A
| |
| +-- Package C
|
+-- Package B
|
+-- Package D
|
+-- Package E
The application may explicitly reference only Package A and Package B, while Package E is introduced indirectly.
This is why manually reviewing the project file is not enough.
A better approach is to automate dependency auditing and produce a report that answers practical questions:
Which packages have known vulnerabilities?
How severe are those vulnerabilities?
Is the affected package direct or transitive?
Which project uses it?
Which dependency introduced it?
Is a fixed version available?
Which applications are affected?
What should the development team upgrade?
This article explains how to build an automated .NET dependency risk-reporting workflow around NuGet auditing and turn package audit results into actionable engineering information.
Introduction
A basic .NET project may contain references such as:
<ItemGroup>
<PackageReference Include="Example.Data.Client" Version="8.0.1" />
<PackageReference Include="Example.Json" Version="13.0.2" />
<PackageReference Include="Example.Logging" Version="7.2.0" />
</ItemGroup>
The project file tells you what the application directly references.
It does not tell the complete dependency risk story.
A package can introduce additional dependencies:
MyApplication
|
+-- WebPackage
|
+-- UtilityPackage
|
+-- VulnerablePackage
The vulnerable package may never appear directly in the application's .csproj file.
This is where dependency auditing becomes important.
What Is NuGet Auditing?
NuGet auditing analyzes package dependencies against known vulnerability information and can report packages affected by known security advisories.
Modern NuGet tooling supports package auditing during restore and can expose vulnerability information as warnings. The exact behavior depends on the .NET SDK, NuGet version, project configuration, and audit settings.
The important architectural idea is:
Project
|
v
Restore
|
v
Dependency Graph
|
v
Vulnerability Audit
|
v
Audit Findings
|
v
Risk Report
The audit is not the final report.
It is the input to a larger dependency-risk workflow.
Why Automated Reporting Matters
A development team may have hundreds of projects:
Repository
|
+-- WebApp
+-- Api
+-- Worker
+-- BackgroundService
+-- AdminPortal
+-- SharedLibraries
+-- Tests
Running a package audit manually for every project is inefficient.
An automated pipeline can produce:
Dependency Risk Report
----------------------
Projects Scanned: 37
Packages Scanned: 612
Vulnerable Packages: 8
Critical: 1
High: 3
Moderate: 3
Low: 1
The report can then be used by developers, security teams, and release engineering.
Direct vs Transitive Dependencies
This distinction is essential.
Direct Dependency
A package explicitly referenced by the project.
<PackageReference
Include="Example.Package"
Version="5.2.0" />
Transitive Dependency
A package brought in by another dependency.
Application
|
v
Package A
|
v
Package B
The application references Package A directly, while Package B is transitive.
A risk report should identify both.
For example:
| Package | Version | Type | Introduced By | Severity |
|---|---|---|---|---|
| Package A | 5.2.0 | Direct | Application | High |
| Package B | 3.1.4 | Transitive | Package A | Critical |
Without the Introduced By information, developers may not know how to remediate the issue.
Build a Dependency Inventory
Before generating risk reports, create a normalized package inventory.
A useful model is:
public sealed record PackageDependency(
string Project,
string PackageId,
string Version,
bool IsDirect,
string? ParentPackage);
For example:
var dependency = new PackageDependency(
Project: "Orders.Api",
PackageId: "Example.Data.Client",
Version: "8.0.1",
IsDirect: true,
ParentPackage: null);
A transitive package could be represented as:
var dependency = new PackageDependency(
Project: "Orders.Api",
PackageId: "Example.Security.Core",
Version: "4.2.1",
IsDirect: false,
ParentPackage: "Example.Data.Client");
This normalized representation makes reporting easier.
Discover the Dependency Graph
The first step is to collect dependency information from every project.
For a project, the dependency graph conceptually looks like:
Project
|
+-- Direct A
| |
| +-- Transitive C
|
+-- Direct B
|
+-- Transitive D
|
+-- Transitive E
Useful sources include:
Project files
project.assets.jsondotnet list packageNuGet restore information
Central package management files
Lock files where applicable
The exact source should be selected based on the team's SDK and repository configuration.
dotnet list package
The .NET CLI can expose project package information.
A typical workflow is:
dotnet list package
For vulnerable packages, the CLI also supports vulnerability-related reporting options in supported SDK/tooling versions.
For example:
dotnet list package --vulnerable
The exact command behavior can vary with the installed SDK, so CI environments should pin and document the SDK version used for auditing.
Why CI Should Use a Fixed SDK
Dependency audit output can change when the SDK or NuGet tooling changes.
A repository should therefore make the audit environment reproducible.
For example:
global.json
|
v
Pinned .NET SDK
|
v
NuGet Restore
|
v
Audit
This avoids a situation where developers and CI systems produce different results because they are using different SDK versions.
Central Package Management
Large repositories often centralize package versions.
For example:
<ItemGroup>
<PackageVersion
Include="Example.Json"
Version="13.0.2" />
<PackageVersion
Include="Example.Logging"
Version="7.2.0" />
</ItemGroup>
This makes dependency management easier because multiple projects can share the same version.
It also creates an opportunity for centralized risk reporting.
Instead of treating every project independently:
37 Projects
612 References
the report can identify:
Unique Packages
----------------
Example.Json
Example.Logging
Example.Data.Client
This makes remediation more efficient.
Generate a Normalized Risk Model
Raw audit output is useful for machines but often difficult for humans.
Create an internal model:
public sealed record DependencyRisk(
string Project,
string PackageId,
string InstalledVersion,
string Severity,
string Advisory,
string? FixedVersion,
bool IsTransitive);
Then the reporting pipeline becomes:
Audit Output
|
v
Parser
|
v
Normalized Findings
|
v
Risk Classification
|
v
Report Generator
Severity Classification
A useful report should group findings by severity.
For example:
Critical
High
Moderate
Low
Unknown
Do not invent severity values if the underlying advisory does not provide them.
If the source provides an official severity classification, preserve it.
If multiple vulnerability records affect the same package, the report should retain the individual findings rather than collapsing them blindly.
Risk Is More Than Severity
A High vulnerability in an unused command-line tool may have a different operational impact from a High vulnerability in a public web application's runtime dependency.
Therefore, consider additional dimensions:
Risk =
Severity
+
Exposure
+
Reachability
+
Runtime Usage
+
Exploitability
+
Remediation Availability
This does not mean replacing the advisory severity.
Instead, keep advisory severity and organizational risk as separate fields.
For example:
| Package | Advisory Severity | Exposure | Internal Risk |
|---|---|---|---|
| Package A | High | Internet-facing | Critical |
| Package B | High | Internal tool | High |
| Package C | Moderate | Build-only | Moderate |
This produces a more useful engineering report.
Directness Should Affect Prioritization
A vulnerable direct dependency is usually easier to remediate:
Project
|
+-- Vulnerable Package
A transitive dependency may require:
Project
|
+-- Package A
|
+-- Vulnerable Package
The remediation may involve upgrading Package A, replacing it, or explicitly overriding the transitive version when compatibility permits.
Therefore, include:
Direct
Transitive
in the report.
Identify the Dependency Chain
For transitive vulnerabilities, show the path.
For example:
Orders.Api
|
+-- Example.Data.Client 8.0.1
|
+-- Example.Security.Core 4.2.1
|
+-- Vulnerable Package 2.1.0
This immediately tells the developer where the vulnerable package came from.
A report that only says:
Vulnerable Package 2.1.0
is much less actionable.
Find a Remediation Version
Where vulnerability metadata provides a fixed or patched version, include it.
For example:
Installed: 2.1.0
Fixed: 2.1.4
Then the report can say:
Upgrade to 2.1.4 or a compatible later version.
Do not automatically recommend a version that has not been established as safe or compatible.
The remediation engine should distinguish:
Known Fixed Version
Compatible Upgrade
Unknown
Handle Multiple Advisories
One package can have multiple vulnerabilities.
For example:
Package A 4.1.0
CVE-X -> High
CVE-Y -> Moderate
CVE-Z -> Low
The report should not hide the individual advisories.
Instead:
Package A
|
+-- Advisory X
+-- Advisory Y
+-- Advisory Z
A package-level summary can then report the highest severity.
Build a Machine-Readable Report
JSON is useful for CI/CD systems.
For example:
{
"generatedAt": "2026-08-18T10:00:00Z",
"projectsScanned": 37,
"findings": [
{
"project": "Orders.Api",
"packageId": "Example.Security.Core",
"version": "4.2.1",
"severity": "High",
"isTransitive": true,
"fixedVersion": "4.2.4"
}
]
}
Machine-readable output can feed:
Dashboards
Security systems
Pull-request checks
Ticketing systems
Release gates
Build a Human-Readable Report
Developers also need a concise summary.
For example:
# Dependency Risk Report
Projects scanned: 37
Unique packages: 412
Vulnerable packages: 8
Critical: 1
High: 3
Moderate: 3
Low: 1
Top remediation:
1. Example.Security.Core -> 4.2.4
2. Example.Http.Client -> 9.1.2
3. Example.Json -> 13.0.5
Then provide project-level details.
Generate Markdown Reports
Markdown works well in pull requests and CI artifacts.
var markdown = new StringBuilder();
markdown.AppendLine("# Dependency Risk Report");
markdown.AppendLine();
markdown.AppendLine(
$"Projects scanned: {summary.ProjectCount}");
markdown.AppendLine(
$"Vulnerable packages: {summary.VulnerablePackageCount}");
A later stage can publish the generated Markdown as a pipeline artifact or pull-request comment.
Generate SARIF Where Appropriate
For security tooling integration, SARIF can be useful because it provides a standardized format for representing static-analysis and security findings.
A dependency audit pipeline can transform normalized findings into SARIF rather than inventing a completely custom security format.
The advantage is interoperability with tools that understand SARIF.
Create a CI Pipeline
A practical pipeline might look like:
Pull Request
|
v
Restore
|
v
Dependency Audit
|
v
Parse Results
|
v
Risk Classification
|
+------> JSON
|
+------> Markdown
|
+------> SARIF
|
v
Policy Gate
|
+--> Pass
|
+--> Fail
The build should fail only when a finding violates an explicitly defined policy.
Define Security Gates
For example:
Critical vulnerabilities
-> Block release
High vulnerabilities
-> Block production deployment
Moderate vulnerabilities
-> Warning
Low vulnerabilities
-> Track
These are example policies, not universal rules.
Organizations should define thresholds based on application exposure, regulatory requirements, exploitability, and remediation processes.
Avoid Making Every Warning a Build Failure
A common mistake is:
Any vulnerability
|
v
Build fails
This can quickly create alert fatigue.
A better approach is policy-based gating:
Finding
|
v
Risk Evaluation
|
+--> Block
+--> Warn
+--> Track
The policy can consider:
Severity
Runtime vs development dependency
Direct vs transitive
Internet exposure
Fixed version availability
Exception status
Development vs Production Dependencies
Not every package has the same runtime exposure.
For example:
Application
|
+-- Runtime Package
|
+-- Test Package
|
+-- Build Tool
A vulnerability in a runtime package may require faster remediation than one used exclusively during testing.
However, build tooling should not be ignored because compromised development dependencies can still affect the software supply chain.
The report should therefore classify dependency scope rather than simply ignoring development packages.
Track Exceptions
Sometimes a package cannot immediately be upgraded.
The report should support a controlled exception process.
For example:
Package:
Example.Package
Severity:
High
Status:
Accepted Risk
Reason:
Upgrade blocked by incompatible vendor integration.
Owner:
Platform Team
Expiration:
2026-09-30
Exceptions should expire.
An indefinite exception becomes permanent technical debt.
Add Ownership
A risk report becomes more useful when it identifies the responsible team.
For example:
| Project | Package | Severity | Owner | Fix |
|---|---|---|---|---|
| Orders.Api | Package A | High | Orders Team | 4.2.4 |
| Billing.Api | Package B | Critical | Billing Team | 9.1.1 |
| Identity.Api | Package C | Moderate | Platform Team | Review |
Ownership can come from repository metadata, project configuration, or a maintained service catalog.
Trend Dependency Risk Over Time
A single report shows current state.
A trend shows whether the organization is improving.
Track:
Week 1
Critical: 4
High: 12
Week 2
Critical: 2
High: 10
Week 3
Critical: 1
High: 7
Useful metrics include:
Open vulnerabilities
New vulnerabilities
Resolved vulnerabilities
Mean time to remediation
Vulnerabilities by team
Vulnerabilities by application
Vulnerabilities by severity
Detect New Vulnerabilities
Dependency risk is not static.
A package that was safe yesterday can become associated with a newly disclosed vulnerability today.
Therefore, dependency auditing should run regularly rather than only when dependencies change.
A useful strategy is:
Pull Request
-> Fast audit
Main Branch
-> Full audit
Scheduled Job
-> Re-audit dependency graph
This catches newly disclosed vulnerabilities in existing dependency versions.
Keep the Audit Reproducible
Record:
SDK Version
Repository Commit
Audit Timestamp
Package Sources
Project
Target Framework
Dependency Graph
This makes it possible to explain why a finding appeared.
Secure the Package Sources
Dependency auditing is part of software supply-chain security.
The package restore process should use trusted package sources and appropriate repository controls.
Avoid silently allowing arbitrary package feeds in production pipelines.
A dependency report is only useful if the dependency graph itself comes from a trusted build environment.
Avoid Blind Auto-Upgrades
Automated vulnerability reports should not automatically upgrade every package.
An upgrade can introduce:
Breaking changes
API changes
Behavioral changes
New transitive dependencies
Performance regressions
A safer workflow is:
Vulnerability Found
|
v
Identify Fixed Version
|
v
Check Compatibility
|
v
Upgrade
|
v
Run Tests
|
v
Security Verification
Automation can create an upgrade proposal, but production adoption should still follow the repository's normal validation process.
Handle Centralized Dependency Updates
If a repository uses central package management, remediation can often be applied in one location.
For example:
Directory.Packages.props
|
+-- Package A -> 4.2.4
+-- Package B -> 8.1.2
The resulting report should show how many projects are affected by the change.
This helps prioritize upgrades that remove risk across many applications.
Dependency Risk Report Architecture
A complete implementation can look like this:
Repository
|
+----------+----------+
| |
v v
Project Files Central Versions
| |
+----------+----------+
|
v
Restore Graph
|
v
NuGet Audit
|
v
Finding Parser
|
v
Risk Normalization
|
+-------------+-------------+
| | |
v v v
JSON Markdown SARIF
| | |
+-------------+-------------+
|
v
Policy Engine
|
+------+------+
| |
v v
Pass Fail
This architecture separates dependency discovery from reporting and policy decisions.
That separation makes the system easier to maintain.
Example Policy Engine
A simple policy object might look like:
public sealed record DependencyPolicy(
bool BlockCritical,
bool BlockHigh,
int MaximumDaysForHighRisk);
Evaluation:
public static bool ShouldBlock(
DependencyRisk finding,
DependencyPolicy policy)
{
if (finding.Severity == "Critical" &&
policy.BlockCritical)
{
return true;
}
if (finding.Severity == "High" &&
policy.BlockHigh)
{
return true;
}
return false;
}
A production implementation should use a structured severity type rather than comparing raw strings.
Common Mistakes
Scanning Only Direct Dependencies
Transitive packages can introduce significant risk.
Treating Every Finding as Equally Important
Risk depends on more than package name and severity.
Ignoring Build Dependencies
Build tooling is part of the software supply chain.
Automatically Upgrading Everything
Security remediation still requires compatibility validation.
Creating Reports Without Ownership
A finding without an owner is difficult to remediate.
Not Tracking Exceptions
Temporary exceptions can become permanent.
Running Audits Only During Releases
New vulnerabilities can be disclosed between releases.
Losing the Dependency Graph
A package name without its parent dependency provides limited remediation guidance.
Failing Builds on Every Warning
This creates alert fatigue and encourages teams to ignore security output.
Not Pinning the Audit Environment
Different SDK or NuGet versions can produce inconsistent behavior.
Advantages
Automated Visibility
Teams get a consistent view of dependency risk across repositories.
Faster Remediation
Reports can identify affected projects, packages, and potential fixed versions.
CI/CD Integration
Security checks can become part of the normal development workflow.
Better Prioritization
Severity, directness, ownership, and exposure can be combined into a useful risk model.
Historical Tracking
Reports can measure whether dependency risk is increasing or decreasing.
Supply-Chain Awareness
The approach covers both direct and transitive dependencies.
Disadvantages
Tooling Complexity
A mature reporting system requires parsing, normalization, policy management, and reporting.
Audit Noise
Large repositories can generate many findings.
Remediation Can Be Difficult
A vulnerable transitive dependency may require upgrading or replacing its parent package.
Vulnerability Data Changes
The same package can move from no known issues to a vulnerability finding when new advisories are published.
Compatibility Constraints
The newest secure version may not always be immediately compatible with the application.
Best Practices
Audit direct and transitive dependencies.
Pin the .NET SDK used by CI.
Normalize audit results into a consistent internal model.
Preserve individual advisory information.
Show the dependency chain for transitive vulnerabilities.
Include installed and known fixed versions where available.
Separate advisory severity from organizational risk.
Track runtime, test, and build dependencies.
Assign findings to application or platform owners.
Use policy-based release gates.
Avoid failing builds on every low-risk finding.
Maintain time-bound security exceptions.
Run audits during pull requests and on a regular schedule.
Generate both machine-readable and human-readable reports.
Consider SARIF for security-tool integration.
Track vulnerability trends over time.
Validate package upgrades with automated tests.
Review package sources as part of supply-chain security.
Use centralized package management where appropriate.
Revisit security policies as application exposure and organizational requirements change.
Frequently Asked Questions
Does NuGet auditing detect every possible dependency risk?
No. Vulnerability auditing focuses on known vulnerability information. It does not automatically identify every licensing, maintenance, malicious-package, configuration, or architectural risk.
Does auditing include transitive dependencies?
Supported NuGet auditing workflows can identify vulnerabilities in transitive dependencies. The exact output and command behavior depend on the installed SDK and NuGet tooling.
Should every vulnerability fail the build?
No. Organizations should define explicit security gates based on severity, exposure, exploitability, dependency scope, and business risk.
How should I handle a vulnerable transitive dependency?
First identify which direct dependency introduced it. Then determine whether upgrading the parent package resolves the issue. If an appropriate fixed version is compatible, upgrade and test it.
Should development dependencies be included?
Yes. Build and development dependencies are part of the software supply chain, although their runtime exposure may differ from production dependencies.
How often should dependency audits run?
Run them during normal development and periodically outside release cycles. This helps identify newly disclosed vulnerabilities affecting existing dependency versions.
Should vulnerability reports be stored?
Yes. Historical reports are valuable for demonstrating remediation progress, identifying recurring dependency problems, and measuring security posture over time.
Can dependency auditing replace software composition analysis?
Not necessarily. NuGet auditing is one component of a broader software supply-chain security strategy. Organizations may still require additional tooling for licensing, malicious-package detection, provenance, SBOM analysis, and broader ecosystem coverage.
Conclusion
Dependency security becomes increasingly difficult as .NET applications grow and their dependency graphs become deeper.
A project file may show only a small set of direct references, while the restored application can contain a much larger transitive dependency graph.
Automated NuGet auditing provides the foundation for identifying known vulnerable packages, but the real value comes from turning those findings into an actionable engineering workflow:
Dependency Graph
|
v
NuGet Audit
|
v
Normalized Findings
|
v
Risk Classification
|
v
Ownership + Remediation
|
v
Policy Gate
|
v
Continuous Monitoring
The goal is not simply to produce a list of vulnerable packages.
A useful dependency risk report should tell developers what is vulnerable, where it is used, whether it is direct or transitive, how serious the finding is, which dependency introduced it, whether a known fix exists, who owns the affected application, and whether the finding should block a release.
When this workflow becomes part of CI/CD, dependency security changes from an occasional manual activity into a continuous engineering control.

Join the conversation! Your thoughts help the community grow.