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:

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:

PackageVersionTypeIntroduced BySeverity
Package A5.2.0DirectApplicationHigh
Package B3.1.4TransitivePackage ACritical

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:

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:

PackageAdvisory SeverityExposureInternal Risk
Package AHighInternet-facingCritical
Package BHighInternal toolHigh
Package CModerateBuild-onlyModerate

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:

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:

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:

ProjectPackageSeverityOwnerFix
Orders.ApiPackage AHighOrders Team4.2.4
Billing.ApiPackage BCriticalBilling Team9.1.1
Identity.ApiPackage CModeratePlatform TeamReview

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:

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:

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

  1. Audit direct and transitive dependencies.

  2. Pin the .NET SDK used by CI.

  3. Normalize audit results into a consistent internal model.

  4. Preserve individual advisory information.

  5. Show the dependency chain for transitive vulnerabilities.

  6. Include installed and known fixed versions where available.

  7. Separate advisory severity from organizational risk.

  8. Track runtime, test, and build dependencies.

  9. Assign findings to application or platform owners.

  10. Use policy-based release gates.

  11. Avoid failing builds on every low-risk finding.

  12. Maintain time-bound security exceptions.

  13. Run audits during pull requests and on a regular schedule.

  14. Generate both machine-readable and human-readable reports.

  15. Consider SARIF for security-tool integration.

  16. Track vulnerability trends over time.

  17. Validate package upgrades with automated tests.

  18. Review package sources as part of supply-chain security.

  19. Use centralized package management where appropriate.

  20. 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.