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:
--sarif
flag in the command turns the output to a format the SARIF SAST Scans Tab extension understands. See CLI reference for more toggles- the results are published to an artifact called
CodeAnalysisLogs
- again for the extension
Viewing the results
Once the above job is completed, the results can be observed from the Scans-tab:
Running semgrep on a pull request
To perform diff-sensitive scanning, see the docs. The main differences:
- add a parameter
targetBranch
to mark the target of the PR - add
persistCredentials: true
andfetchdepth: 100
(or something) to thecheckout
step - run
git fetch origin ${{parameters.targetBranch}}:origin/${{parameters.targetBranch}}
before running the scan - Option: add
--error
flag tosemgrep scan
so the pipeline fails if something is found. This way you won't get the sarif scan output, but the PR won't pass with any findings. - add environment variables to the scan step:
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!
Thoughts, comments? Send me an email!