Just show me the code
As always, if you don’t care about the post I have uploaded the source code on my Github.

Almost any dotnet application contains several NuGet dependencies, and those dependencies may have their own dependencies, and so on and so forth. This means that your application ends up depending on dozens of external dependencies.

What if any of those dependencies you’re using are vulnerable? That could pose a risk of a supply chain attack.
What is software supply chain attack? It is an upstream vulnerability in one of your dependencies that can be fatal, making your app vulnerable to a potential compromise.

The term supply chain is used to refer to everything that goes into your software and where it comes from. It is the dependencies that your software supply chain depends on. A dependency can be code, binaries, or other components, and where they come from, such as a repository or package manager.

If you don’t live under a rock, then you’ve likely heard about last year Log4j vulnerability, this was a software supply chain vulnerability in which having the popular Java logging framework installed on your app could compromise your business.

There are quite a few well-known tools that will allow you to scan your app for vulnerable dependencies, also if your project is hosted on GitHub you can leverage GitHub Security to find vulnerable dependencies and Dependabot will fix them by opening up a pull request against your codebase.

Right now, if you are not checking anywhere on your deployment pipeline if your application has any known NuGet security vulnerability, you should know that the dotnet CLI is capable of doing that analysis without the need of any kind of external tool.

How to use the .NET CLI to check if your app has any vulnerable NuGet dependency

You can list any known vulnerabilities in your dependencies within your projects using the dotnet list package --vulnerable command.

This command gets the security information from the centralized GitHub Advisory Database. This database provides two main listings of vulnerabilities:

  • A CVE is Common Vulnerabilities and Exposures. This is a list of publicly disclosed computer security flaws.
  • A GHSA is a GitHub Security Advisory. GitHub is a CVE Numbering Authority (CNA) and is authorized to assign CVE identification numbers.

To use this feature you’ll need to have installed the .NET SDK version 5.0.200 (or higher).

The dotnet list package --vulnerable command only works with projects that are using the PackageReference format, which means that you won’t be able to use it if your project still uses the packages.config format.

Let me show you an example.
I have built a .NET5 N-Layer application that has a few NuGet vulnerabilities, and here’s how the output of the dotnet list package --vulnerable command looks like:

The following sources were used:
   https://api.nuget.org/v3/index.json

The given project `VulnerableApp.Library.Contracts` has no vulnerable packages given the current sources.
The given project `VulnerableApp.Library.Impl` has no vulnerable packages given the current sources.
The given project `VulnerableApp.Repository.Contracts` has no vulnerable packages given the current sources.
The given project `VulnerableApp.Repository.Impl` has no vulnerable packages given the current sources.
Project `VulnerableApp.Core.Extensions` has the following vulnerable packages
   [netstandard2.0]: 
   Top-level Package      Requested   Resolved   Severity   Advisory URL                                     
   > Newtonsoft.Json      12.0.3      12.0.3     High       https://github.com/advisories/GHSA-5crp-9r3c-p9vr

Project `VulnerableApp.WebApi` has the following vulnerable packages
   [net5.0]: 
   Top-level Package                                    Requested   Resolved   Severity   Advisory URL                                     
   > Microsoft.AspNetCore.Authentication.JwtBearer      5.0.6       5.0.6      Moderate   https://github.com/advisories/GHSA-q7cg-43mg-qp69
   > Newtonsoft.Json                                    12.0.3      12.0.3     High       https://github.com/advisories/GHSA-5crp-9r3c-p9vr

The given project `VulnerableApp.WebApi.IntegrationTest` has no vulnerable packages given the current sources.
The given project `VulnerableApp.Library.Impl.UnitTest` has no vulnerable packages given the current sources.

As you can see this command will tell you if there are any packages that contains a vulnerability, the severity of the vulnerability, and a link with more information about the vulnerability.

The “Requested” and “Resolved” columns might lead to confusion, so let me explain what they mean:

  • The “Requested” column will include the version that the developer has indicated in the package reference within the project file, which means it can be a range.
  • The “Resolved” column will include the version that the project is currently using and will always be a single value.

The dotnet list package --vulnerable command ONLY checks direct dependencies, which means that it will only scan the NuGet packages that are directly installed on your app (top-level packages).
If you are interested in seeing vulnerabilities within your dependencies as well, you’ll need to use the --include-transitive parameter, like this dotnet list package --vulnerable --include-transitive.

Let’s execute the dotnet list package --vulnerable --include-transitive command on the same app.

The following sources were used:
   https://api.nuget.org/v3/index.json

Project `VulnerableApp.Library.Contracts` has the following vulnerable packages
   [netstandard2.0]: 
   Transitive Package      Resolved   Severity   Advisory URL                                     
   > Newtonsoft.Json       12.0.3     High       https://github.com/advisories/GHSA-5crp-9r3c-p9vr

Project `VulnerableApp.Library.Impl` has the following vulnerable packages
   [netstandard2.0]: 
   Transitive Package      Resolved   Severity   Advisory URL                                     
   > Newtonsoft.Json       12.0.3     High       https://github.com/advisories/GHSA-5crp-9r3c-p9vr

The given project `VulnerableApp.Repository.Contracts` has no vulnerable packages given the current sources.
The given project `VulnerableApp.Repository.Impl` has no vulnerable packages given the current sources.
Project `VulnerableApp.Core.Extensions` has the following vulnerable packages
   [netstandard2.0]: 
   Top-level Package      Requested   Resolved   Severity   Advisory URL                                     
   > Newtonsoft.Json      12.0.3      12.0.3     High       https://github.com/advisories/GHSA-5crp-9r3c-p9vr

Project `VulnerableApp.WebApi` has the following vulnerable packages
   [net5.0]: 
   Top-level Package                                    Requested   Resolved   Severity   Advisory URL                                     
   > Microsoft.AspNetCore.Authentication.JwtBearer      5.0.6       5.0.6      Moderate   https://github.com/advisories/GHSA-q7cg-43mg-qp69
   > Newtonsoft.Json                                    12.0.3      12.0.3     High       https://github.com/advisories/GHSA-5crp-9r3c-p9vr

Project `VulnerableApp.WebApi.IntegrationTest` has the following vulnerable packages
   [net5.0]: 
   Transitive Package                                   Resolved   Severity   Advisory URL                                     
   > Microsoft.AspNetCore.Authentication.JwtBearer      5.0.6      Moderate   https://github.com/advisories/GHSA-q7cg-43mg-qp69
   > Newtonsoft.Json                                    12.0.3     High       https://github.com/advisories/GHSA-5crp-9r3c-p9vr
   > System.Net.Http                                    4.3.0      High       https://github.com/advisories/GHSA-7jgj-8wvc-jh57
   > System.Text.RegularExpressions                     4.3.0      Moderate   https://github.com/advisories/GHSA-cmhx-cq75-c4mj

Project `VulnerableApp.Library.Impl.UnitTest` has the following vulnerable packages
   [net5.0]: 
   Transitive Package                    Resolved   Severity   Advisory URL                                     
   > Newtonsoft.Json                     12.0.3     High       https://github.com/advisories/GHSA-5crp-9r3c-p9vr
   > System.Net.Http                     4.3.0      High       https://github.com/advisories/GHSA-7jgj-8wvc-jh57
   > System.Text.RegularExpressions      4.3.0      Moderate   https://github.com/advisories/GHSA-cmhx-cq75-c4mj

As you can see the list of vulnerable dependencies has grown quite a bit from the previous execution.

To put it simply, remember to use ALWAYS the --use-transitive parameter when running the dotnet list package --vulnerable command, because the NuGet packages you have installed on your app have their own dependencies and those dependencies can be have their own dependencies, and so on and so forth. And you want to check if there is a vulnerability on the entire chain of dependencies not only on your top-level packages.

How to integrate the .NET CLI vulnerability scan feature with your CI/CD pipelines

The dotnet list package --vulnerable --use-transitive command only LISTS vulnerabilities found on your dependencies, if a vulnerability is found within your application the dotnet CLI won’t thrown any kind of error, which means that there is not a native way to stop the execution of a pipeline if a vulnerability is found.

But that’s no excuse at all, because you can check for any known NuGet vulnerability and break the pipeline execution with just a couple of lines of bash script.

dotnet list package --vulnerable --include-transitive 2>&1 | tee build.log
grep -q -i "critical\|high\|moderate\|low" build.log; [ $? -eq 0 ] && echo "Security Vulnerabilities found on the log output" && exit 1
  • The first line runs the dotnet list package --vulnerable --include-transitive command and stores the result on a file named “build.log”
  • The second one searches the “build.log” file for the “critical”, “high”, “moderate” or “low” keywords. If there is a match, it breaks the pipeline execution by returning a general error.

As you can see it’s a pretty simple script, now let me show you how an Azure DevOps Pipeline and a GitHub Action will look like.

Azure Pipeline

The pipeline is a run of the mill .NET pipeline (dotnet restore, dotnet build, dotnet test and dotnet publish), the only thing that’s different is that there is an extra step where the dotnet list package --vulnerable --include-transitive command is being executed and evaluated.

It is also worth mentioning that the dotnet list package --vulnerable --include-transitive command needs to run after the dotnet restore command.

trigger:
- master

pool:
  vmImage: ubuntu-latest

variables:
  buildConfiguration: 'Release'
 
steps:
- task: DotNetCoreCLI@2
  inputs:
    command: 'restore'
    projects: '**/*.csproj'
  displayName: 'Restore Nuget Packages'

- task: Bash@3
  displayName: Check NuGet vulnerabilities
  inputs:
    targetType: 'inline'
    script: |
      dotnet list package --vulnerable --include-transitive 2>&1 | tee build.log
      echo "Analyze dotnet list package command log output..."
      grep -q -i "critical\|high\|moderate\|low" build.log; [ $? -eq 0 ] && echo "Security Vulnerabilities found on the log output" && exit 1      
    
- task: DotNetCoreCLI@2
  inputs:
    command: 'build'
    projects: '**/*.csproj'
    arguments: '--no-restore'
  displayName: 'Build projects'
 

- task: DotNetCoreCLI@2
  inputs:
    command: 'test'
    projects: '**/*Test.csproj'
    arguments: '--no-restore --no-build'
  displayName: 'Run Tests'
 
- task: DotNetCoreCLI@2
  inputs:
    command: 'publish'
    publishWebProjects: false
    projects: '**/VulnerableApp.WebApi.csproj'
    arguments: '--configuration $(buildConfiguration) --no-restore'
    modifyOutputPath: false
  displayName: 'Publish Api'

GitHub Action

The same thing happens here, the pipeline is a run of the mill .NET pipeline with an extra step where the dotnet list package --vulnerable --include-transitive command is being executed and evaluated.

name: dotnet build pipeline checking nuget vulnerabilities

on:
  push:
    branches: [ "main" ]
  pull_request:
    branches: [ "main" ]
    
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v3
    - name: Setup .NET
      uses: actions/setup-dotnet@v2
      with:
        dotnet-version: 6.0.x
    - name: Restore dependencies
      run: dotnet restore
    - name: Checking NuGet vulnerabilites
      run: |
        dotnet list package --vulnerable --include-transitive 2>&1 | tee build.log
        echo "Analyze dotnet list package command log output..."
        grep -q -i "critical\|high\|moderate\|low" build.log; [ $? -eq 0 ] && echo "Security Vulnerabilities found on the log output" && exit 1        
    - name: Build
      run: dotnet build --no-restore
    - name: Test
      run: dotnet test --no-build --verbosity normal

How to check if your app has any outdated or deprecated dependencies

To ensure a secure supply chain of dependencies, you will want to ensure that all of your dependencies are regularly updated to the latest stable version as they will often include the latest functionality and security patches to known vulnerabilities.

You can also use the dotnet CLI to list any known deprecated or outdated dependencies you may have inside your project or solution.

The commands you’ll want to use are the following ones:

  • dotnet list package --outdated
  • dotnet list package --deprecated

Check outdated dependencies

The dotnet list package --outdated lists packages that have been outdated.

Here’s how the output of the dotnet list package --outdated command looks like when executed on the same application I have used in the previous section.

The following sources were used:
   https://api.nuget.org/v3/index.json

The given project `VulnerableApp.Library.Contracts` has no updates given the current sources.
Project `VulnerableApp.Library.Impl` has the following updates to its packages
   [netstandard2.0]: 
   Top-level Package                                            Requested   Resolved   Latest
   > AutoMapper                                                 10.1.1      10.1.1     11.0.1
   > Microsoft.Extensions.DependencyInjection.Abstractions      5.0.0       5.0.0      6.0.0 
   > Microsoft.Extensions.Logging                               5.0.0       5.0.0      6.0.0 
   > Microsoft.Extensions.Options.ConfigurationExtensions       5.0.0       5.0.0      6.0.0 

The given project `VulnerableApp.Repository.Contracts` has no updates given the current sources.
Project `VulnerableApp.Repository.Impl` has the following updates to its packages
   [netstandard2.0]: 
   Top-level Package                                            Requested   Resolved   Latest
   > Microsoft.Extensions.DependencyInjection.Abstractions      5.0.0       5.0.0      6.0.0 
   > Microsoft.Extensions.Options.ConfigurationExtensions       5.0.0       5.0.0      6.0.0 

Project `VulnerableApp.Core.Extensions` has the following updates to its packages
   [netstandard2.0]: 
   Top-level Package      Requested   Resolved   Latest
   > Newtonsoft.Json      12.0.3      12.0.3     13.0.1

Project `VulnerableApp.WebApi` has the following updates to its packages
   [net5.0]: 
   Top-level Package                                          Requested   Resolved   Latest
   > AutoMapper.Extensions.Microsoft.DependencyInjection      8.1.1       8.1.1      11.0.0
   > Microsoft.AspNetCore.Authentication.JwtBearer            5.0.6       5.0.6      6.0.7 
   > Microsoft.AspNetCore.Authentication.OpenIdConnect        5.0.6       5.0.6      6.0.7 
   > Microsoft.AspNetCore.Mvc.NewtonsoftJson                  5.0.6       5.0.6      6.0.7 
   > Microsoft.Identity.Web                                   1.11.0      1.11.0     1.25.1
   > Newtonsoft.Json                                          12.0.3      12.0.3     13.0.1
   > Serilog.AspNetCore                                       4.1.0       4.1.0      6.0.1 
   > Serilog.Sinks.Console                                    3.1.1       3.1.1      4.0.1 
   > Serilog.Sinks.File                                       4.1.0       4.1.0      5.0.0 
   > Swashbuckle.AspNetCore                                   6.1.4       6.1.4      6.4.0 
   > Swashbuckle.AspNetCore.Annotations                       6.1.4       6.1.4      6.4.0 
   > Swashbuckle.AspNetCore.Newtonsoft                        6.1.4       6.1.4      6.4.0 

Project `VulnerableApp.WebApi.IntegrationTest` has the following updates to its packages
   [net5.0]: 
   Top-level Package                       Requested   Resolved   Latest
   > coverlet.collector                    1.3.0       1.3.0      3.1.2 
   > Microsoft.AspNetCore.Mvc.Testing      5.0.6       5.0.6      6.0.7 
   > Microsoft.NET.Test.Sdk                16.7.1      16.7.1     17.2.0
   > xunit                                 2.4.1       2.4.1      2.4.2 
   > xunit.runner.visualstudio             2.4.3       2.4.3      2.4.5 

Project `VulnerableApp.Library.Impl.UnitTest` has the following updates to its packages
   [net5.0]: 
   Top-level Package                Requested   Resolved   Latest
   > coverlet.collector             1.3.0       1.3.0      3.1.2 
   > Microsoft.NET.Test.Sdk         16.7.1      16.7.1     17.2.0
   > Moq                            4.16.1      4.16.1     4.18.2
   > xunit                          2.4.1       2.4.1      2.4.2 
   > xunit.runner.visualstudio      2.4.3       2.4.3      2.4.5 

Having an outdated package is not as problematic as having a vulnerable one, but nonetheless it is good to know about them.
In this case I’m evaluating a .NET5 app that was built a couple years ago, so it is completely normal that some of the dependencies are outdated.

The dotnet list package --outdated command only checks for outdated references on the top-level packages, if you want to check for outdated dependencies in the entire chain of dependencies you can use the --use-transitive parameter.

Check deprecated dependencies

The dotnet list package --deprecated lists packages that have been deprecated.

Here’s how the output of the dotnet list package --deprecated command looks like when executed on the same application I have used in the previous section.

The following sources were used:
   https://api.nuget.org/v3/index.json

The given project `VulnerableApp.Library.Contracts` has no deprecated packages given the current sources.
Project `VulnerableApp.Library.Impl` has the following deprecated packages
   [netstandard2.0]: 
   Top-level Package                                            Requested   Resolved   Reason(s)      Alternative
   > Microsoft.Extensions.DependencyInjection.Abstractions      5.0.0       5.0.0      Other,Legacy              
   > Microsoft.Extensions.Logging                               5.0.0       5.0.0      Other,Legacy              
   > Microsoft.Extensions.Options.ConfigurationExtensions       5.0.0       5.0.0      Other,Legacy              

The given project `VulnerableApp.Repository.Contracts` has no deprecated packages given the current sources.
Project `VulnerableApp.Repository.Impl` has the following deprecated packages
   [netstandard2.0]: 
   Top-level Package                                            Requested   Resolved   Reason(s)      Alternative
   > Microsoft.Extensions.DependencyInjection.Abstractions      5.0.0       5.0.0      Other,Legacy              
   > Microsoft.Extensions.Options.ConfigurationExtensions       5.0.0       5.0.0      Other,Legacy              

The given project `VulnerableApp.Core.Extensions` has no deprecated packages given the current sources.
The given project `VulnerableApp.WebApi` has no deprecated packages given the current sources.
The given project `VulnerableApp.WebApi.IntegrationTest` has no deprecated packages given the current sources.
The given project `VulnerableApp.Library.Impl.UnitTest` has no deprecated packages given the current sources.

.NET5 reached the “End of Support” status a couple of months ago, that’s the reason why the scan is telling me that those 5.0.0 dependencies are deprecated.

The dotnet list package --deprecated only checks for deprecated references on the top-level packages, if you want to check for outdated dependencies in the entire chain of dependencies you can use the --use-transitive parameter.