Using dotnet nuget package vulnerability scan in Azure DevOps build
Image from Pexels

Using dotnet nuget package vulnerability scan in Azure DevOps build

Listing nuget vulnerabilities and controlling build in Azure DevOps

Since December 2021 when Log4Shell vulnerabilty caused by popular package for logging in Log4j for Java was discovered here is raised certain level of awareness of using OpenSource packages in application.

Because of these cases it is critical that you have indicator whether you are using a dependency with reported vulnerability before you make your application available for wide audience, practically before you do the production release. The ideal time to do this check is when building the application.

If you are developing on .NET platform you are lucky, because since version of .NET 5 the vulnerability scan is available with .NET SDK 5.0.200 via simple CLI command.

Listing the vulnerabilities with CLI command

As contribution to this effort, Microsoft added functionality to .NET CLI to scan and check used NuGet packages against centralized GitHub Advisory Database. It is quite easy to use this command from dotnet CLI and get a report of whether you have directly or indirectly referenced a NuGet package with a detected and reported vulnerability.

All you have to do is to list packages for your solution with --vulnerable parameter.

dotnet list ./MySloution.sln package --vulnerable
    

However, this will only list packages you re directly referencing in your solution, without checking indirect dependencies which are basically NuGet packages which are packages you are directly using are referencing. To list those too and increase security check level you need include --include-transitive parameter.

dotnet list ./MySloution.sln package --vulnerable --include-transitive
    

Nuget Vulnerability Terminal 

This is a great feature, but unfortunately in a form like this, you can only report a detected vulnerability but you cannot act upon it out of the box for example to fail a build. That's why we'll need to parse the output and build our logic on top of it.

Handling the vulnerability scan report

Upon running the vulnerability check, all you get is basically a report listing the vulnerable NuGet packages your application is using directly or indirectly. Unfortunately as of now dotnet CLI does not support different output formats, so we'll have to work with this plain text report output.

Now you can use any script language to handle this output, but I decided to use PowerShell as it is pretty easy to use, you can reference .NET libraries in it and since version 7 it is a cross-platform which means you can run it not only on Windows, but on Mac and Linux too.

To make it more usable, I made solution folder and solution file name as parameters for the script. All that needs to be done is to split the CLI output and split it into lines. Every line that starts with " > " means it is a vulnerability detected by the CLI command. This means all that needs to be checked is whether the line string contains " High ", " Critical " or " Moderate "

$workingFolder = $args[0]
$solution = $args[1]

$processrocessInfo = New-Object System.Diagnostics.ProcessStartInfo
$processrocessInfo.FileName = "dotnet"
$processrocessInfo.RedirectStandardError = $true
$processrocessInfo.RedirectStandardOutput = $true
$processrocessInfo.UseShellExecute = $false
$processrocessInfo.WorkingDirectory = $workingFolder
$packageListCommand = 'list ./' + $solution + ' package --vulnerable --include-transitive'

$output=''

$processrocessInfo.Arguments = $packageListCommand
$process = New-Object System.Diagnostics.Process
$process.StartInfo = $processrocessInfo
$process.Start() | Out-Null
$stdout = $process.StandardOutput.ReadToEnd()
$stderr = $process.StandardError.ReadToEnd()
$process.WaitForExit()
$output = $output + $stdout + [Environment]::NewLine

echo $output

$leadingString = '   > '
$outputLines = $output -split [Environment]::NewLine

foreach ($line in $outputLines) {

    if($line.StartsWith($leadingString)){
        $packageLine =  $line.Substring($leadingString.Length)

        if($packageLine.Contains(" High ") -or $packageLine.Contains(" Critical ") -or $packageLine.Contains(" Moderate ")){
            throw "Vulnerability detected!"
        }
    }
}
    

Once one of the string for vulnerability severity is detected, all we need to do is throw and exception. If you add this script run in your build pipeline, this will mean that the pipeline will fail.

Nuget Vulnerability Build Fail

To use this script in Azure DevOps build pipeline, you just need to make the PowerShell script part of your repository and run it from the build pipeline with a simple PowerShell task.

  - task: PowerShell@2
    displayName: "NuGet packages vulnerabilities scan"
    enabled: true
    continueOnError: false
    inputs:
      filePath: '$(Build.SourcesDirectory)/nuget-vunerability-build-fail.ps1'
      arguments: '''$(Build.SourcesDirectory)'' ''MulitpleDb.Sample.sln'''
      pwsh: true
    

Generating the report for Azure DevOps

You can see from the script above and as well from the console screenshot, you still get the report in plain text format along with the build failure due to throwing custom exception. In moet cases this will be good enough as you stopped the build with vulnerable dependencies to be finished and therefore pushed later to the production via release.

In case you want to have your vulnerability report more readable and vulnerability links click-able directly from the Azure DevOps build result then some additional parsing and output building needs to be done.

Azure DevOps supports markdown (.md) format out of the box, so ideally we would want to have a markdown tables written up and once done, pushed to Azure DevOps build results. We also need to differentiate each column from the plain text output and also detect the project to which this partial vulnerability report is linked to.

So firts we need to:

  • split the output into lines
  • check lines which start with "Project `"
  • check lines which start with " > "
  • split each report line by " " (space) and take only non empty elements

Of course, we need to keep the exception throwing logic in place, but we need to read the whole report and then throw exception, which changes the flow a bit.

$workingFolder = $args[0]
$solution = $args[1]

$processrocessInfo = New-Object System.Diagnostics.ProcessStartInfo
$processrocessInfo.FileName = "dotnet"
$processrocessInfo.RedirectStandardError = $true
$processrocessInfo.RedirectStandardOutput = $true
$processrocessInfo.UseShellExecute = $false
$processrocessInfo.WorkingDirectory = $workingFolder
$packageListCommand = 'list ./' + $solution + ' package --vulnerable --include-transitive'

$output=''
$summary='# Nuget package vulnerability scan summary';

    $processrocessInfo.Arguments = $packageListCommand
    $process = New-Object System.Diagnostics.Process
    $process.StartInfo = $processrocessInfo
    $process.Start() | Out-Null
    $stdout = $process.StandardOutput.ReadToEnd()
    $stderr = $process.StandardError.ReadToEnd()
    $process.WaitForExit()
    $output = $output + $stdout + [Environment]::NewLine


echo $output

$leadingString = '   > '
$outputLines = $output -split [Environment]::NewLine
$vulnerabilityDetected = $false
foreach ($line in $outputLines) {

    if($line.StartsWith('Project ')){
        $projectName = $line.Split('`')[1]
        $summary = $summary + [Environment]::NewLine + [Environment]::NewLine + '## ' + $projectName + [Environment]::NewLine + [Environment]::NewLine
        $summary = $summary + "| Transitive Package | Resolved | Severity | Advisory URL |" + [Environment]::NewLine
        $summary = $summary + "|:--------|:--------:|--------|--------|"
    }

    if($line.StartsWith($leadingString)){
        $packageLine =  $line.Substring($leadingString.Length)

        $columns = $packageLine.Split(" ")
        $row = "| "
        $columnCount = 0
        foreach($column in $columns){
            if($column.Length -gt 1 ){
                $columnCount = $columnCount + 1
                $value = $column.Trim()
                if($columnCount -eq 4){
                    $value = "[" + $value + "](" + $value + ")"
                }
                $row = $row + $value + " |"
            }
        }

        $summary = $summary + [Environment]::NewLine + $row


        if($packageLine.Contains(" High ") -or $packageLine.Contains(" Critical ") -or $packageLine.Contains(" Moderate ")){

            if($vulnerabilityDetected -eq $false){
                $vulnerabilityDetected = $true
            }
        }
    }
}


if($vulnerabilityDetected){
    # Write to file
    $summaryPath = $workingFolder + "/nuget-vulnerabilty-report.md"
    Set-Content -Path $summaryPath -Value $summary

    # Push to Azure DevOps
    $summaryOut = "##vso[task.uploadsummary]" + $summaryPath
    Write-Host $summaryOut

    throw "Vulnerability detected!"
}
    

If we disable the Azure push line Write-Host $summaryOut we can safely run the scrip on our local and check the output markdown result

# Nuget package vulnerability scan summary

## MulitpleDb.Sample

| Transitive Package | Resolved | Severity | Advisory URL |
|:--------|:--------:|--------|--------|
| System.Net.Http |4.3.0 |High |[https://github.com/advisories/GHSA-7jgj-8wvc-jh57](https://github.com/advisories/GHSA-7jgj-8wvc-jh57) |
| System.Text.RegularExpressions |4.3.0 |Moderate |[https://github.com/advisories/GHSA-cmhx-cq75-c4mj](https://github.com/advisories/GHSA-cmhx-cq75-c4mj) |

    

And if we load this output into any markdown viewer like markdownlivepreview.com we will see roughly how this will look like once published to Azure DevOps

Md Preview 

Pushing the report to Azure DevOps 

We have all the components for our build pipeline with vulnerability scan, all we have to do is to incorporate PowerShell script calls to the build pipeline yaml of Azure DevOps build pipeline.

pool:
  vmImage: 'windows-latest'

variables:
  solution: '**/*.sln'
  buildPlatform: 'Any CPU'
  buildConfiguration: 'Release'
  sdkVersion: ''

steps:

- task: UseDotNet@2
  displayName: 'Use .NET 5.0.408 sdk'
  inputs:
    packageType: 'sdk'
    version: '5.0.408'
    installationPath: $(Agent.ToolsDirectory)/dotnet

- task: DotNetCoreCLI@2
  displayName: 'Restore NuGet packages'
  inputs:
    command: restore
    projects: '$(Build.Repository.LocalPath)\MulitpleDb.Sample\MulitpleDb.Sample.csproj'

- task: DotNetCoreCLI@2
  enabled: true
  displayName: "Build project"
  inputs:
    command: 'build'
    projects: '$(Build.Repository.LocalPath)\MulitpleDb.Sample\MulitpleDb.Sample.csproj'
    configuration: $(buildConfiguration)   

- task: PowerShell@2  
  displayName: "NuGet packages vulnerabilities scan"  
  enabled: true  
  continueOnError: false  
  inputs:  
    filePath: '$(Build.SourcesDirectory)/nuget-vunerability-build-fail-report.ps1'  
    arguments: '''$(Build.SourcesDirectory)'' ''MulitpleDb.Sample.sln'''  
    pwsh: true 

- task: DotNetCoreCLI@2
  displayName: 'Publish'
  inputs:
    command: publish
    projects: '$(Build.Repository.LocalPath)\MulitpleDb.Sample\MulitpleDb.Sample.csproj'
    arguments: '--output $(Build.ArtifactStagingDirectory)'
    publishWebProjects: false
    

Azure Devops Build Log

This build will fail at our PowerShell task, but if you go to Extensions tab in the build, you will get the rendered markdown output as part of the build summary

 Azure Devops Build Summary

Disclaimer

Purpose of the code contained in snippets or available for download in this article is solely for learning and demo purposes. Author will not be held responsible for any failure or damages caused due to any other usage.


About the author

DEJAN STOJANOVIC

Dejan is a passionate Software Architect/Developer. He is highly experienced in .NET programming platform including ASP.NET MVC and WebApi. He likes working on new technologies and exciting challenging projects

CONNECT WITH DEJAN  Loginlinkedin Logintwitter Logingoogleplus Logingoogleplus

JavaScript

read more

SQL/T-SQL

read more

Umbraco CMS

read more

PowerShell

read more

Comments for this article