Skip to main content
Back to blog

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.

InnovateBits4 min read
Share

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 found

For 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 gate

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

Free newsletter

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