Security Testing in Azure DevOps CI/CD Pipelines
How to integrate security testing into Azure DevOps CI/CD pipelines. Covers SAST, DAST, dependency scanning with OWASP, secret detection, container scanning, and building security gates into your release pipeline.
Security testing in CI/CD means catching vulnerabilities before they reach production — not after a penetration test finds them six months later. Azure DevOps supports multiple security tools through Marketplace extensions and open-source integrations, and each can be added as a pipeline stage with a quality gate.
The DevSecOps pipeline model
PR Validation: Secret scanning, SAST, lint security rules
Build: Dependency vulnerability scan
Integration: DAST (dynamic analysis against staging)
Pre-release: Container scan, licence compliance
Production: Runtime monitoring (not pipeline — separate concern)
Each layer catches different vulnerability classes. Together they give defence in depth before code ships.
Stage 1: Secret scanning (every PR)
Prevent credentials from being committed. Use Gitleaks or detect-secrets:
- stage: SecretScan
displayName: Secret Detection
jobs:
- job: Gitleaks
steps:
- script: |
docker run --rm \
-v $(Build.SourcesDirectory):/repo \
zricethezav/gitleaks:latest \
detect \
--source /repo \
--report-format sarif \
--report-path gitleaks-report.sarif \
--exit-code 1
displayName: Scan for secrets (Gitleaks)
- task: PublishPipelineArtifact@1
inputs:
targetPath: gitleaks-report.sarif
artifact: gitleaks-report
condition: always()Add a .gitleaks.toml to the repo to allowlist known false positives (test credentials in fixture files):
[allowlist]
description = "Test fixture allowlist"
regexes = ['''testuser@example\.com''', '''Test@2025!''']
paths = ['''tests/fixtures/.*''']Stage 2: SAST — Static Application Security Testing
Microsoft Security DevLabs extension provides SAST tools:
- task: MicrosoftSecurityDevOps@1
displayName: SAST scan
inputs:
categories: 'secrets,code'
break: true # Fail pipeline if high severity foundFor JavaScript/TypeScript, add ESLint security plugin as a code-level SAST:
npm install --save-dev eslint-plugin-security eslint-plugin-no-secrets// .eslintrc.json
{
"plugins": ["security", "no-secrets"],
"extends": ["plugin:security/recommended"],
"rules": {
"no-secrets/no-secrets": "error"
}
}- script: npm run lint:security -- --format=junit --output-file=sast-results.xml
displayName: ESLint security rules
- task: PublishTestResults@2
inputs:
testResultsFormat: JUnit
testResultsFiles: sast-results.xml
testRunTitle: SAST — $(Build.BuildNumber)
condition: always()Stage 3: Dependency vulnerability scanning
OWASP Dependency-Check scans all npm/Maven/pip dependencies against the CVE database:
- task: dependency-check-build-task@6
displayName: OWASP Dependency Check
inputs:
projectName: '$(Build.Repository.Name)'
scanPath: '$(Build.SourcesDirectory)'
format: 'JUNIT'
failOnCVSS: '7' # Fail on CVSS score >= 7 (High/Critical)
suppressionPath: 'suppression.xml' # Known accepted vulnerabilities
- task: PublishTestResults@2
inputs:
testResultsFormat: JUnit
testResultsFiles: dependency-check-junit.xml
testRunTitle: Dependency Scan — $(Build.BuildNumber)
condition: always()For npm projects, also run the native npm audit:
- script: |
npm audit --audit-level=high --json > npm-audit.json || true
node -e "
const report = require('./npm-audit.json');
const high = report.metadata.vulnerabilities.high;
const critical = report.metadata.vulnerabilities.critical;
if (high + critical > 0) {
console.error(\`Found \${critical} critical, \${high} high vulnerabilities\`);
process.exit(1);
}
"
displayName: npm audit gateStage 4: DAST — Dynamic Application Security Testing
Run OWASP ZAP against the staging environment after deployment:
- stage: DAST
displayName: Dynamic Security Testing
dependsOn: DeployStaging
jobs:
- job: ZAP
steps:
- script: |
docker run --rm \
-v $(Build.ArtifactStagingDirectory):/zap/wrk \
ghcr.io/zaproxy/zaproxy:stable \
zap-baseline.py \
-t $(STAGING_URL) \
-r zap-report.html \
-x zap-report.xml \
-l WARN \
-I # Don't fail on warnings, only errors
displayName: OWASP ZAP baseline scan
- task: PublishPipelineArtifact@1
inputs:
targetPath: $(Build.ArtifactStagingDirectory)/zap-report.html
artifact: zap-report
condition: always()ZAP baseline scan checks for common OWASP Top 10 issues (missing security headers, XSS, SQL injection patterns) without active exploitation.
Stage 5: Container image scanning
If your application runs in Docker:
- task: trivy-azure-pipelines-task@1
displayName: Trivy container scan
inputs:
version: latest
image: '$(ACR_NAME).azurecr.io/$(IMAGE_NAME):$(Build.BuildId)'
severities: 'CRITICAL,HIGH'
ignoreUnfixed: true
exitCode: '1'Or use Microsoft Defender for Containers if images are pushed to Azure Container Registry.
Security gate for production
Block production deployments when critical security issues exist:
- stage: SecurityGate
displayName: Security Quality Gate
dependsOn: [DAST, DependencyScan]
jobs:
- job: Gate
steps:
- script: |
CRITICAL=$(cat dependency-check-results.json | jq '.dependencies[].vulnerabilities[] | select(.severity == "CRITICAL") | .name' | wc -l)
if [ "$CRITICAL" -gt 0 ]; then
echo "##vso[task.logissue type=error]$CRITICAL critical vulnerabilities — blocking release"
exit 1
fi
displayName: Critical vulnerability gateCommon errors and fixes
Error: Gitleaks flags test fixtures as secret leaks
Fix: Create a .gitleaks.toml with [allowlist] entries for known test values, or exclude the tests/fixtures/ directory entirely.
Error: OWASP Dependency-Check takes 20+ minutes in the pipeline
Fix: Enable the --cacheToLocal option so the CVE database is downloaded once and cached. Use Cache@2 task to persist between pipeline runs.
Error: ZAP scan fails with authentication errors on staging
Fix: Pass authentication headers to ZAP: add --hook script that logs in first, or use ZAP's built-in form-based authentication configuration with zap-full-scan.py -z "-config replacer.full_list(0).description=auth".
Error: npm audit fails on known false positives with no fix available
Fix: Create an .npmrc audit suppression or use audit-ci with a config file: { "high": { "allowlist": ["GHSA-xyz-known-false-positive"] } }.
Stay ahead in AI-driven QA
Get practical tutorials on test automation, AI testing, and quality engineering — straight to your inbox. No spam, unsubscribe any time.
Discussion
Sign in with GitHub to comment · powered by Giscus