Rewiring

Free security scans with Azure Pipelines

If Github Advanced Security is a bit too much for your wallet, here's a way to get most of the way for free (as long as you're already paying for Azure Devops...)

Github Advanced Security for Azure Devops offers a lot of security features (secret scanning, static analysis, vulnerability detection...) for an eye-watering price of 49$/per active committer/month. That adds up to a lot, but luckily you can get similar (but maybe poorer quality) functionality for free.

Combining package audits with Semgrep yields a simple pipeline for inspecting vulnerabilities in your applications. We're using the standard set of semgrep rules, which you can unlock more of by making an account, and of course there's also the cloud service stuff available for a fee. If all this stuff is new to you, I suggest fixing the findings from the free tier and moving on from there.

Let's see the templates we're working with. I have simplified the parameters here, like the dependsOn and jobName I normally include - add them if you need them.

Package audits

With these tools we aim to get notifications if our projects contain any vulnerable dependencies. A simple way to do this is to subscribe to pipeline failure notifications in ADO and then making sure the jobs fail if vulnerabilities are detected.

For dotnet package audit, see my earlier post

npm audit

Running npm audit fails if any vulnerabilities are detected, so it's simple to plug in:

# /public/node/jobs/npm-audit.yml
parameters:
  - name: workingDir
    type: string
    default: './'

  - name: nodeVersion
    type: string
    default: '>=20'

jobs:
  - job: NPM_AUDIT
    displayName: Fail if vulnerable npm packages found in ${{parameters.workingDir}}
    steps:       
      - task: NodeTool@0
        displayName: Ensure Node.js version (${{ parameters.nodeVersion }})
        inputs:
          versionSpec: ${{ parameters.nodeVersion }}

      - task: Npm@1
        displayName: 'NPM: ci'
        inputs:
          command: custom
          workingDir: ${{ parameters.workingDir }}
          customCommand: ci

      - task: Npm@1
        displayName: 'Error if vulnerabilities are found'
        inputs:
          command: custom
          workingDir: ${{ parameters.workingDir }}
          customCommand: audit

Static scanning with Semgrep

Here's a complete job template for doing a Semgrep scan on the repository root.

# /public/shared/jobs/semgrep.yml
jobs:
  - job: SEMGREP
    pool:
      vmImage: ubuntu-latest
    displayName: Run semgrep scan (OSS)
    variables:
      PIP_CACHE_DIR: $(Pipeline.Workspace)/.pip
    steps:
      - task: Cache@2
        inputs:
          key: 'pip | "$(Agent.OS)" | semgrep'
          restoreKeys: |
            pip | "$(Agent.OS)"
          path: $(pip_cache_dir)
        displayName: Cache pip
      - script: |
          python -m pip install --upgrade pip
          pip install semgrep
        displayName: install semgrep
      - script: |
          semgrep scan --config=${{ parameters.configDirectory }} --output $(System.ArtifactsDirectory)/scan_results.sarif --sarif
        displayName: Run semgrep
      - publish: $(System.ArtifactsDirectory)/scan_results.sarif
        artifact: CodeAnalysisLogs

Things of note:

Viewing the results

Once the above job is completed, the results can be observed from the Scans-tab: Screenshot 2025-03-27 085549

Running semgrep on a pull request

To perform diff-sensitive scanning, see the docs. The main differences:

env:
   SEMGREP_PR_ID: $(System.PullRequest.PullRequestNumber)
   SEMGREP_BASELINE_REF: origin/${{parameters.targetBranch}}

Putting it all together

Once all the job templates are complete, we can combine them into a single job template that runs all scans in parallel.

# /public/shared/jobs/security-scan.yml
parameters:
  - name: solutionForPackageAudit
    displayName: Path to the dotnet .sln file to scan nuget vulnerabilities for. Leave empty to skip nuget audit.
    type: string
    default: ''

  - name: nodeWorkingDir
    displayName: Directory containing the package.json. Leave empty to skip npm audit. If the file is in the repo root, use './'
    type: string
    default: ''

jobs:
  - ${{if parameters.solutionForPackageAudit }}:
      - template: /public/dotnet/jobs/nuget-audit.yml
        parameters:
          solutionPath: ${{parameters.solutionForPackageAudit}}

  - ${{if parameters.nodeWorkingDir}}:
      - template: /public/node/jobs/npm-audit.yml
        parameters:
          workingDir: ${{parameters.nodeWorkingDir}}

  - template: /public/shared/jobs/semgrep.yml

Using the template is easy:

trigger: none

schedules:
  - cron: '0 2 * * 0'
    displayName: Run security scans on Sunday
    branches:
      include:
        - main

resources:
  repositories:
    - repository: templates
      type: git
      name: automation/templates
      ref: refs/heads/release/v1

jobs:
  - template: public/shared/jobs/security-scan.yml@templates
    parameters:
      solutionForPackageAudit: backend/SolutionName.sln
      nodeWorkingDir: frontend

With this setup it's pretty easy to get started with static security scans. For dotnet projects I also recommend Roslynator and SonarAnalyzers for build-time analysis of security and other goodies. I'll write a post on those later.

If anyone knows of a battle-proven semgrep ruleset for dotnet, let me know!

See my other Azure posts

Thoughts, comments? Send me an email!

#azure #dotnet #npm #security #semgrep #tech