DevOps 4 min read

Security Testing in Azure DevOps CI/CD

How to integrate security testing into Azure DevOps CI/CD pipelines. Covers SAST, DAST, dependency scanning with OWASP, secret detection, container.

I
InnovateBits
InnovateBits

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:

YAML
1- stage: SecretScan 2 displayName: Secret Detection 3 jobs: 4 - job: Gitleaks 5 steps: 6 - script: | 7 docker run --rm \ 8 -v $(Build.SourcesDirectory):/repo \ 9 zricethezav/gitleaks:latest \ 10 detect \ 11 --source /repo \ 12 --report-format sarif \ 13 --report-path gitleaks-report.sarif \ 14 --exit-code 1 15 displayName: Scan for secrets (Gitleaks) 16 17 - task: PublishPipelineArtifact@1 18 inputs: 19 targetPath: gitleaks-report.sarif 20 artifact: gitleaks-report 21 condition: always()

Add a .gitleaks.toml to the repo to allowlist known false positives (test credentials in fixture files):

TOML
1[allowlist] 2 description = "Test fixture allowlist" 3 regexes = ['''testuser@example\.com''', '''Test@2025!'''] 4 paths = ['''tests/fixtures/.*''']

Stage 2: SAST — Static Application Security Testing

Microsoft Security DevLabs extension provides SAST tools:

YAML
1- task: MicrosoftSecurityDevOps@1 2 displayName: SAST scan 3 inputs: 4 categories: 'secrets,code' 5 break: true # Fail pipeline if high severity found

For JavaScript/TypeScript, add ESLint security plugin as a code-level SAST:

BASH
1npm install --save-dev eslint-plugin-security eslint-plugin-no-secrets
JSON
1// .eslintrc.json 2{ 3 "plugins": ["security", "no-secrets"], 4 "extends": ["plugin:security/recommended"], 5 "rules": { 6 "no-secrets/no-secrets": "error" 7 } 8}
YAML
1- script: npm run lint:security -- --format=junit --output-file=sast-results.xml 2 displayName: ESLint security rules 3 4- task: PublishTestResults@2 5 inputs: 6 testResultsFormat: JUnit 7 testResultsFiles: sast-results.xml 8 testRunTitle: SAST — $(Build.BuildNumber) 9 condition: always()

Stage 3: Dependency vulnerability scanning

OWASP Dependency-Check scans all npm/Maven/pip dependencies against the CVE database:

YAML
1- task: dependency-check-build-task@6 2 displayName: OWASP Dependency Check 3 inputs: 4 projectName: '$(Build.Repository.Name)' 5 scanPath: '$(Build.SourcesDirectory)' 6 format: 'JUNIT' 7 failOnCVSS: '7' # Fail on CVSS score >= 7 (High/Critical) 8 suppressionPath: 'suppression.xml' # Known accepted vulnerabilities 9 10- task: PublishTestResults@2 11 inputs: 12 testResultsFormat: JUnit 13 testResultsFiles: dependency-check-junit.xml 14 testRunTitle: Dependency Scan — $(Build.BuildNumber) 15 condition: always()

For npm projects, also run the native npm audit:

YAML
1- script: | 2 npm audit --audit-level=high --json > npm-audit.json || true 3 node -e " 4 const report = require('./npm-audit.json'); 5 const high = report.metadata.vulnerabilities.high; 6 const critical = report.metadata.vulnerabilities.critical; 7 if (high + critical > 0) { 8 console.error(\`Found \${critical} critical, \${high} high vulnerabilities\`); 9 process.exit(1); 10 } 11 " 12 displayName: npm audit gate

Stage 4: DAST — Dynamic Application Security Testing

Run OWASP ZAP against the staging environment after deployment:

YAML
1- stage: DAST 2 displayName: Dynamic Security Testing 3 dependsOn: DeployStaging 4 jobs: 5 - job: ZAP 6 steps: 7 - script: | 8 docker run --rm \ 9 -v $(Build.ArtifactStagingDirectory):/zap/wrk \ 10 ghcr.io/zaproxy/zaproxy:stable \ 11 zap-baseline.py \ 12 -t $(STAGING_URL) \ 13 -r zap-report.html \ 14 -x zap-report.xml \ 15 -l WARN \ 16 -I # Don't fail on warnings, only errors 17 displayName: OWASP ZAP baseline scan 18 19 - task: PublishPipelineArtifact@1 20 inputs: 21 targetPath: $(Build.ArtifactStagingDirectory)/zap-report.html 22 artifact: zap-report 23 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:

YAML
1- task: trivy-azure-pipelines-task@1 2 displayName: Trivy container scan 3 inputs: 4 version: latest 5 image: '$(ACR_NAME).azurecr.io/$(IMAGE_NAME):$(Build.BuildId)' 6 severities: 'CRITICAL,HIGH' 7 ignoreUnfixed: true 8 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:

YAML
1- stage: SecurityGate 2 displayName: Security Quality Gate 3 dependsOn: [DAST, DependencyScan] 4 jobs: 5 - job: Gate 6 steps: 7 - script: | 8 CRITICAL=$(cat dependency-check-results.json | jq '.dependencies[].vulnerabilities[] | select(.severity == "CRITICAL") | .name' | wc -l) 9 if [ "$CRITICAL" -gt 0 ]; then 10 echo "##vso[task.logissue type=error]$CRITICAL critical vulnerabilities — blocking release" 11 exit 1 12 fi 13 displayName: Critical vulnerability gate

Common 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"] } }.

Tags
#security-testing#azure-devops#sast#dast#owasp#dependency-scanning#devsecops

Share this article

Follow for more

Follow me on social media for more developer tips, tricks, and tutorials. Let's connect and build something great together!