Security

Security Scanning Tools for Azure Pipelines

Comprehensive guide to integrating security scanning tools into Azure Pipelines, covering dependency scanning, container image scanning, secret detection, infrastructure-as-code scanning, and building automated security gates.

Security Scanning Tools for Azure Pipelines

Overview

Security scanning in your CI/CD pipeline is the difference between catching vulnerabilities before they ship and reading about them in a breach notification. Azure Pipelines supports a wide range of security scanning tools — from dependency analysis to container image scanning to secret detection. I have integrated these tools across dozens of projects, and the key is not just running scans but making them actionable gates that block bad code without drowning developers in noise.

Prerequisites

  • Azure DevOps project with Azure Pipelines configured
  • Node.js 16 or later for build agents
  • Docker installed on build agents (for container scanning)
  • Azure subscription (for Microsoft Defender for DevOps integration)
  • Familiarity with YAML pipeline syntax
  • NPM, NuGet, or other package manager for dependency scanning

Dependency Scanning

Dependency vulnerabilities are the most common attack vector in modern applications. Every npm install pulls in hundreds of transitive dependencies, any one of which could contain a known vulnerability.

npm audit Integration

# azure-pipelines.yml - npm audit as a pipeline gate
stages:
  - stage: SecurityScan
    displayName: 'Security Scanning'
    jobs:
      - job: DependencyScan
        displayName: 'Dependency Vulnerability Scan'
        pool:
          vmImage: 'ubuntu-latest'
        steps:
          - task: NodeTool@0
            inputs:
              versionSpec: '20.x'

          - script: npm ci
            displayName: 'Install dependencies'

          - script: |
              npm audit --json > audit-results.json
              EXIT_CODE=$?
              echo "##vso[task.setvariable variable=auditExitCode]$EXIT_CODE"
              exit 0
            displayName: 'Run npm audit'

          - script: |
              node -e "
              var fs = require('fs');
              var audit = JSON.parse(fs.readFileSync('audit-results.json', 'utf8'));
              var vulns = audit.vulnerabilities || {};
              var critical = 0;
              var high = 0;
              var moderate = 0;

              Object.keys(vulns).forEach(function(name) {
                var severity = vulns[name].severity;
                if (severity === 'critical') critical++;
                else if (severity === 'high') high++;
                else if (severity === 'moderate') moderate++;
              });

              console.log('Vulnerability Summary:');
              console.log('  Critical: ' + critical);
              console.log('  High: ' + high);
              console.log('  Moderate: ' + moderate);

              if (critical > 0 || high > 0) {
                console.log('##vso[task.logissue type=error]Found ' + critical + ' critical and ' + high + ' high vulnerabilities');
                console.log('##vso[task.complete result=Failed;]Security gate failed');
              } else {
                console.log('##vso[task.logissue type=warning]Found ' + moderate + ' moderate vulnerabilities');
              }
              "
            displayName: 'Evaluate audit results'

          - task: PublishBuildArtifacts@1
            inputs:
              pathToPublish: 'audit-results.json'
              artifactName: 'security-reports'
            condition: always()

Snyk Integration

Snyk provides deeper analysis than npm audit, including fix recommendations and license compliance.

# Snyk scanning in Azure Pipelines
steps:
  - script: |
      npm install -g snyk
      snyk auth $SNYK_TOKEN
    displayName: 'Install and authenticate Snyk'
    env:
      SNYK_TOKEN: $(SNYK_TOKEN)

  - script: |
      snyk test --json --severity-threshold=high > snyk-results.json || true
      snyk monitor --project-name="$(Build.Repository.Name)"
    displayName: 'Run Snyk analysis'
    env:
      SNYK_TOKEN: $(SNYK_TOKEN)

  - script: |
      node -e "
      var fs = require('fs');
      var results = JSON.parse(fs.readFileSync('snyk-results.json', 'utf8'));

      if (results.vulnerabilities) {
        var byType = {};
        results.vulnerabilities.forEach(function(v) {
          byType[v.severity] = (byType[v.severity] || 0) + 1;
        });

        console.log('Snyk Results:');
        console.log('  Total: ' + results.vulnerabilities.length);
        Object.keys(byType).forEach(function(sev) {
          console.log('  ' + sev + ': ' + byType[sev]);
        });

        var critical = byType.critical || 0;
        var high = byType.high || 0;
        if (critical + high > 0) {
          console.log('##vso[task.complete result=Failed;]Blocking vulnerabilities found');
        }
      }
      "
    displayName: 'Process Snyk results'

OWASP Dependency-Check

For language-agnostic dependency scanning:

steps:
  - task: dependency-check-build-task@6
    inputs:
      projectName: '$(Build.Repository.Name)'
      scanPath: '$(Build.SourcesDirectory)'
      format: 'HTML,JSON'
      failOnCVSS: '7'  # Fail on CVSS score >= 7
      suppressionPath: '$(Build.SourcesDirectory)/owasp-suppressions.xml'

  - task: PublishBuildArtifacts@1
    inputs:
      pathToPublish: '$(Common.TestResultsDirectory)/dependency-check'
      artifactName: 'dependency-check-report'
    condition: always()

Suppression file for known false positives:

<!-- owasp-suppressions.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<suppressions xmlns="https://jeremylong.github.io/DependencyCheck/dependency-suppression.1.3.xsd">
  <suppress>
    <notes>False positive - this CVE applies to server-side usage only</notes>
    <packageUrl regex="true">^pkg:npm/some-package@.*$</packageUrl>
    <cve>CVE-2024-12345</cve>
  </suppress>
</suppressions>

Container Image Scanning

Container images contain their own dependency trees — OS packages, runtime libraries, and application dependencies. Scanning the final image catches vulnerabilities that dependency-only scans miss.

Trivy Integration

Trivy is the fastest and most comprehensive open-source container scanner.

# Container scanning with Trivy
stages:
  - stage: Build
    jobs:
      - job: BuildAndScan
        pool:
          vmImage: 'ubuntu-latest'
        steps:
          - task: Docker@2
            displayName: 'Build container image'
            inputs:
              command: 'build'
              Dockerfile: 'Dockerfile'
              tags: '$(Build.BuildId)'
              arguments: '-t myapp:$(Build.BuildId)'

          - script: |
              curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin
            displayName: 'Install Trivy'

          - script: |
              trivy image \
                --format json \
                --output trivy-results.json \
                --severity CRITICAL,HIGH \
                --exit-code 0 \
                myapp:$(Build.BuildId)
            displayName: 'Scan container image'

          - script: |
              node -e "
              var fs = require('fs');
              var results = JSON.parse(fs.readFileSync('trivy-results.json', 'utf8'));

              var totalCritical = 0;
              var totalHigh = 0;
              var details = [];

              (results.Results || []).forEach(function(result) {
                (result.Vulnerabilities || []).forEach(function(v) {
                  if (v.Severity === 'CRITICAL') {
                    totalCritical++;
                    details.push('CRITICAL: ' + v.VulnerabilityID + ' in ' + v.PkgName + ' (' + v.InstalledVersion + ' -> ' + (v.FixedVersion || 'no fix') + ')');
                  }
                  if (v.Severity === 'HIGH') totalHigh++;
                });
              });

              console.log('Container Scan Results:');
              console.log('  Critical: ' + totalCritical);
              console.log('  High: ' + totalHigh);

              details.forEach(function(d) { console.log('  ' + d); });

              if (totalCritical > 0) {
                console.log('##vso[task.complete result=Failed;]Critical container vulnerabilities found');
              }
              "
            displayName: 'Evaluate container scan'

          - task: PublishBuildArtifacts@1
            inputs:
              pathToPublish: 'trivy-results.json'
              artifactName: 'container-scan'
            condition: always()

Microsoft Defender for Containers

For teams using Azure Container Registry:

steps:
  - task: AzureCLI@2
    displayName: 'Push and scan with Defender'
    inputs:
      azureSubscription: 'Azure-Production'
      scriptType: 'bash'
      scriptLocation: 'inlineScript'
      inlineScript: |
        # Push image to ACR (Defender scans automatically on push)
        az acr login --name myregistry
        docker tag myapp:$(Build.BuildId) myregistry.azurecr.io/myapp:$(Build.BuildId)
        docker push myregistry.azurecr.io/myapp:$(Build.BuildId)

        # Wait for scan to complete and check results
        sleep 30
        SCAN_STATUS=$(az acr repository show \
          --name myregistry \
          --image myapp:$(Build.BuildId) \
          --query "changeableAttributes.quarantineState" -o tsv)

        echo "Scan status: $SCAN_STATUS"

  - task: AzureCLI@2
    displayName: 'Check Defender scan results'
    inputs:
      azureSubscription: 'Azure-Production'
      scriptType: 'bash'
      scriptLocation: 'inlineScript'
      inlineScript: |
        # Query Security Center for scan findings
        az security sub-assessment list \
          --assessed-resource-id "/subscriptions/{sub-id}/resourceGroups/{rg}/providers/Microsoft.ContainerRegistry/registries/myregistry" \
          --assessment-name "dbd0cb49-b563-45e7-9724-889e799fa648" \
          --query "[?contains(resourceDetails.id, 'myapp')].{severity:status.severity, description:displayName}" \
          -o table

Secret Detection

Secrets committed to source control are the most preventable and most damaging security issue I see. Pipeline-integrated secret detection catches them before they reach the repository.

Gitleaks Integration

# Secret detection with Gitleaks
steps:
  - script: |
      wget -q https://github.com/gitleaks/gitleaks/releases/download/v8.18.0/gitleaks_8.18.0_linux_x64.tar.gz
      tar -xzf gitleaks_8.18.0_linux_x64.tar.gz
      chmod +x gitleaks
    displayName: 'Install Gitleaks'

  - script: |
      ./gitleaks detect \
        --source=$(Build.SourcesDirectory) \
        --report-format=json \
        --report-path=gitleaks-report.json \
        --verbose \
        --exit-code 1
    displayName: 'Scan for secrets'
    continueOnError: true

  - script: |
      node -e "
      var fs = require('fs');

      if (!fs.existsSync('gitleaks-report.json')) {
        console.log('No secrets detected - clean scan');
        process.exit(0);
      }

      var findings = JSON.parse(fs.readFileSync('gitleaks-report.json', 'utf8'));

      if (findings.length === 0) {
        console.log('No secrets detected - clean scan');
        process.exit(0);
      }

      console.log('SECRET LEAK DETECTED - ' + findings.length + ' finding(s):');
      findings.forEach(function(f) {
        console.log('  Rule: ' + f.RuleID);
        console.log('  File: ' + f.File + ':' + f.StartLine);
        console.log('  Match: ' + f.Match.substring(0, 20) + '...');
        console.log('  Commit: ' + f.Commit);
        console.log('');
      });

      console.log('##vso[task.logissue type=error]Secrets detected in source code');
      console.log('##vso[task.complete result=Failed;]Secret detection gate failed');
      "
    displayName: 'Process secret scan results'

  - task: PublishBuildArtifacts@1
    inputs:
      pathToPublish: 'gitleaks-report.json'
      artifactName: 'secret-scan'
    condition: always()

Custom rules for organization-specific patterns:

# .gitleaks.toml - Custom secret detection rules
title = "Custom Gitleaks Configuration"

[extend]
useDefault = true

[[rules]]
id = "azure-devops-pat"
description = "Azure DevOps Personal Access Token"
regex = '''[a-z2-7]{52}'''
keywords = ["pat", "token", "devops"]

[[rules]]
id = "internal-api-key"
description = "Internal API Key Format"
regex = '''MYORG-[A-Z0-9]{32}'''
keywords = ["api_key", "apikey"]

[[rules]]
id = "connection-string"
description = "Database Connection String"
regex = '''(Server|Data Source)=.+;(User Id|uid)=.+;(Password|pwd)=.+'''
keywords = ["connection", "server"]

[allowlist]
paths = [
  '''\.gitleaks\.toml''',
  '''test/fixtures/''',
  '''__mocks__/'''
]

Microsoft Security DevOps (MSDO) Extension

The MSDO extension bundles multiple scanning tools into a single task:

steps:
  - task: MicrosoftSecurityDevOps@1
    displayName: 'Run Microsoft Security DevOps'
    inputs:
      categories: 'secrets,code,artifacts,IaC'
      tools: 'credscan,eslint,trivy,terrascan'
      break: true  # Fail the pipeline on findings

  - task: PublishBuildArtifacts@1
    inputs:
      pathToPublish: '$(Build.ArtifactStagingDirectory)/.gdn/'
      artifactName: 'msdo-results'
    condition: always()

Infrastructure-as-Code Scanning

Terraform, ARM templates, and Bicep files can contain misconfigurations that create security vulnerabilities in deployed infrastructure.

Checkov for IaC Scanning

# Checkov scans Terraform, ARM, Bicep, Kubernetes manifests
steps:
  - script: |
      pip install checkov
    displayName: 'Install Checkov'

  - script: |
      checkov \
        --directory $(Build.SourcesDirectory)/infrastructure \
        --output json \
        --output-file checkov-results.json \
        --framework terraform \
        --check CKV_AZURE_1,CKV_AZURE_2,CKV_AZURE_3,CKV_AZURE_4,CKV_AZURE_35 \
        --soft-fail
    displayName: 'Scan infrastructure code'

  - script: |
      node -e "
      var fs = require('fs');
      var results = JSON.parse(fs.readFileSync('checkov-results.json', 'utf8'));

      var passed = 0;
      var failed = 0;
      var skipped = 0;
      var failures = [];

      (results.results || []).forEach(function(framework) {
        (framework.passed_checks || []).forEach(function() { passed++; });
        (framework.failed_checks || []).forEach(function(check) {
          failed++;
          failures.push({
            id: check.check_id,
            name: check.check_result.name || check.check_id,
            file: check.file_path,
            resource: check.resource
          });
        });
        (framework.skipped_checks || []).forEach(function() { skipped++; });
      });

      console.log('IaC Scan Results:');
      console.log('  Passed: ' + passed);
      console.log('  Failed: ' + failed);
      console.log('  Skipped: ' + skipped);

      if (failures.length > 0) {
        console.log('\\nFailed Checks:');
        failures.forEach(function(f) {
          console.log('  ' + f.id + ': ' + f.file + ' (' + f.resource + ')');
        });
      }

      if (failed > 0) {
        console.log('##vso[task.logissue type=warning]' + failed + ' IaC security checks failed');
      }
      "
    displayName: 'Process IaC scan results'

Terrascan for Terraform

steps:
  - script: |
      curl -L "https://github.com/tenable/terrascan/releases/latest/download/terrascan_$(uname -s)_x86_64.tar.gz" | tar -xz
      chmod +x terrascan
    displayName: 'Install Terrascan'

  - script: |
      ./terrascan scan \
        --iac-type terraform \
        --iac-dir $(Build.SourcesDirectory)/infrastructure \
        --output json \
        --severity high,critical \
        > terrascan-results.json || true
    displayName: 'Run Terrascan'

  - script: |
      node -e "
      var fs = require('fs');
      var results = JSON.parse(fs.readFileSync('terrascan-results.json', 'utf8'));
      var violations = results.results && results.results.violations || [];

      console.log('Terrascan Results: ' + violations.length + ' violations');
      violations.forEach(function(v) {
        console.log('  [' + v.severity + '] ' + v.rule_name + ': ' + v.resource_name);
        console.log('    ' + v.description);
      });

      var critical = violations.filter(function(v) { return v.severity === 'HIGH'; }).length;
      if (critical > 0) {
        console.log('##vso[task.complete result=Failed;]High severity IaC violations found');
      }
      "
    displayName: 'Process Terrascan results'

Building a Unified Security Gate

Individual scans are useful, but a unified security gate that aggregates results from all scanners provides a single pass/fail decision.

var fs = require("fs");
var path = require("path");

// ============================================================
// Unified Security Gate
// Aggregates results from all security scanners and provides
// a single pass/fail decision for the pipeline
// ============================================================

var THRESHOLDS = {
  dependencies: { critical: 0, high: 3 },
  container: { critical: 0, high: 5 },
  secrets: { any: 0 },
  iac: { high: 0 }
};

function loadResults(filePath) {
  try {
    return JSON.parse(fs.readFileSync(filePath, "utf8"));
  } catch (e) {
    console.log("Warning: Could not load " + filePath + ": " + e.message);
    return null;
  }
}

function evaluateDependencyScan(resultsFile) {
  var results = loadResults(resultsFile);
  if (!results) return { pass: true, skipped: true };

  var critical = 0;
  var high = 0;
  var vulns = results.vulnerabilities || {};

  Object.keys(vulns).forEach(function(name) {
    var severity = vulns[name].severity;
    if (severity === "critical") critical++;
    else if (severity === "high") high++;
  });

  return {
    pass: critical <= THRESHOLDS.dependencies.critical && high <= THRESHOLDS.dependencies.high,
    critical: critical,
    high: high,
    total: Object.keys(vulns).length
  };
}

function evaluateContainerScan(resultsFile) {
  var results = loadResults(resultsFile);
  if (!results) return { pass: true, skipped: true };

  var critical = 0;
  var high = 0;

  (results.Results || []).forEach(function(result) {
    (result.Vulnerabilities || []).forEach(function(v) {
      if (v.Severity === "CRITICAL") critical++;
      else if (v.Severity === "HIGH") high++;
    });
  });

  return {
    pass: critical <= THRESHOLDS.container.critical && high <= THRESHOLDS.container.high,
    critical: critical,
    high: high
  };
}

function evaluateSecretScan(resultsFile) {
  var results = loadResults(resultsFile);
  if (!results) return { pass: true, skipped: true };

  var count = Array.isArray(results) ? results.length : 0;
  return {
    pass: count <= THRESHOLDS.secrets.any,
    count: count
  };
}

function evaluateIacScan(resultsFile) {
  var results = loadResults(resultsFile);
  if (!results) return { pass: true, skipped: true };

  var high = 0;
  (results.results || []).forEach(function(framework) {
    (framework.failed_checks || []).forEach(function(check) {
      if (check.check_result && check.check_result.severity === "HIGH") high++;
    });
  });

  return {
    pass: high <= THRESHOLDS.iac.high,
    high: high
  };
}

// Run all evaluations
var depResult = evaluateDependencyScan("audit-results.json");
var containerResult = evaluateContainerScan("trivy-results.json");
var secretResult = evaluateSecretScan("gitleaks-report.json");
var iacResult = evaluateIacScan("checkov-results.json");

// Print report
console.log("===========================================");
console.log("  UNIFIED SECURITY GATE REPORT");
console.log("===========================================");
console.log("");

var gates = [
  { name: "Dependency Scan", result: depResult },
  { name: "Container Scan", result: containerResult },
  { name: "Secret Detection", result: secretResult },
  { name: "IaC Scan", result: iacResult }
];

var allPassed = true;

gates.forEach(function(gate) {
  var status = gate.result.skipped ? "SKIP" : (gate.result.pass ? "PASS" : "FAIL");
  var icon = gate.result.pass || gate.result.skipped ? "[OK]" : "[XX]";
  console.log("  " + icon + " " + gate.name + ": " + status);

  if (gate.result.critical !== undefined) {
    console.log("      Critical: " + gate.result.critical + ", High: " + gate.result.high);
  }
  if (gate.result.count !== undefined) {
    console.log("      Findings: " + gate.result.count);
  }

  if (!gate.result.pass && !gate.result.skipped) allPassed = false;
});

console.log("");
console.log("  OVERALL: " + (allPassed ? "PASSED" : "FAILED"));
console.log("===========================================");

if (!allPassed) {
  console.log("##vso[task.complete result=Failed;]Security gate failed");
  process.exit(1);
}

Output:

===========================================
  UNIFIED SECURITY GATE REPORT
===========================================

  [OK] Dependency Scan: PASS
      Critical: 0, High: 2
  [XX] Container Scan: FAIL
      Critical: 1, High: 3
  [OK] Secret Detection: PASS
      Findings: 0
  [OK] IaC Scan: PASS
      High: 0

  OVERALL: FAILED
===========================================

Complete Working Example: Full Security Pipeline

A complete pipeline integrating all security scanning stages:

# azure-pipelines.yml - Full security scanning pipeline
trigger:
  branches:
    include:
      - main
      - develop
      - release/*

pr:
  branches:
    include:
      - main

variables:
  imageTag: '$(Build.BuildId)'
  imageName: 'myapp'

stages:
  # Stage 1: Source code security scanning
  - stage: SourceSecurity
    displayName: 'Source Code Security'
    jobs:
      - job: DependencyScan
        displayName: 'Dependency Scan'
        pool:
          vmImage: 'ubuntu-latest'
        steps:
          - task: NodeTool@0
            inputs:
              versionSpec: '20.x'
          - script: npm ci
            displayName: 'Install dependencies'
          - script: |
              npm audit --json > $(Build.ArtifactStagingDirectory)/audit-results.json || true
            displayName: 'npm audit'
          - task: PublishPipelineArtifact@1
            inputs:
              targetPath: '$(Build.ArtifactStagingDirectory)/audit-results.json'
              artifact: 'dep-scan'

      - job: SecretScan
        displayName: 'Secret Detection'
        pool:
          vmImage: 'ubuntu-latest'
        steps:
          - script: |
              wget -q https://github.com/gitleaks/gitleaks/releases/download/v8.18.0/gitleaks_8.18.0_linux_x64.tar.gz
              tar -xzf gitleaks_8.18.0_linux_x64.tar.gz
            displayName: 'Install Gitleaks'
          - script: |
              ./gitleaks detect \
                --source=$(Build.SourcesDirectory) \
                --report-format=json \
                --report-path=$(Build.ArtifactStagingDirectory)/gitleaks-report.json \
                --exit-code 0
            displayName: 'Scan for secrets'
          - task: PublishPipelineArtifact@1
            inputs:
              targetPath: '$(Build.ArtifactStagingDirectory)/gitleaks-report.json'
              artifact: 'secret-scan'

      - job: IacScan
        displayName: 'Infrastructure Code Scan'
        pool:
          vmImage: 'ubuntu-latest'
        condition: |
          and(
            succeeded(),
            or(
              eq(variables['Build.Reason'], 'PullRequest'),
              eq(variables['Build.SourceBranch'], 'refs/heads/main')
            )
          )
        steps:
          - script: pip install checkov
            displayName: 'Install Checkov'
          - script: |
              checkov \
                --directory $(Build.SourcesDirectory)/infrastructure \
                --output json \
                --output-file $(Build.ArtifactStagingDirectory)/checkov-results.json \
                --soft-fail || true
            displayName: 'Run Checkov'
          - task: PublishPipelineArtifact@1
            inputs:
              targetPath: '$(Build.ArtifactStagingDirectory)/checkov-results.json'
              artifact: 'iac-scan'

  # Stage 2: Build and container scanning
  - stage: BuildAndScan
    displayName: 'Build & Container Scan'
    dependsOn: SourceSecurity
    jobs:
      - job: BuildImage
        pool:
          vmImage: 'ubuntu-latest'
        steps:
          - task: Docker@2
            displayName: 'Build image'
            inputs:
              command: build
              Dockerfile: Dockerfile
              tags: $(imageTag)
              arguments: '-t $(imageName):$(imageTag)'

          - script: |
              curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin
              trivy image \
                --format json \
                --output $(Build.ArtifactStagingDirectory)/trivy-results.json \
                --severity CRITICAL,HIGH \
                $(imageName):$(imageTag)
            displayName: 'Scan container with Trivy'

          - task: PublishPipelineArtifact@1
            inputs:
              targetPath: '$(Build.ArtifactStagingDirectory)/trivy-results.json'
              artifact: 'container-scan'

  # Stage 3: Security gate evaluation
  - stage: SecurityGate
    displayName: 'Security Gate'
    dependsOn:
      - SourceSecurity
      - BuildAndScan
    jobs:
      - job: EvaluateGate
        pool:
          vmImage: 'ubuntu-latest'
        steps:
          - download: current
            patterns: '**/*.json'

          - task: NodeTool@0
            inputs:
              versionSpec: '20.x'

          - script: |
              node $(Build.SourcesDirectory)/scripts/security-gate.js \
                --dep-scan=$(Pipeline.Workspace)/dep-scan/audit-results.json \
                --secret-scan=$(Pipeline.Workspace)/secret-scan/gitleaks-report.json \
                --container-scan=$(Pipeline.Workspace)/container-scan/trivy-results.json \
                --iac-scan=$(Pipeline.Workspace)/iac-scan/checkov-results.json
            displayName: 'Evaluate security gate'

  # Stage 4: Deploy only if security gate passes
  - stage: Deploy
    displayName: 'Deploy'
    dependsOn: SecurityGate
    condition: succeeded()
    jobs:
      - deployment: DeployApp
        environment: 'production'
        strategy:
          runOnce:
            deploy:
              steps:
                - script: echo "Deploying secure application"
                  displayName: 'Deploy'

Common Issues & Troubleshooting

"npm audit" Reports Vulnerabilities in Dev Dependencies

Not all vulnerabilities are equal. Dev dependencies only affect developers, not production:

# Audit only production dependencies
npm audit --omit=dev --json > audit-prod.json

# Or filter in your evaluation script
node -e "
var audit = require('./audit-results.json');
var prodVulns = {};
Object.keys(audit.vulnerabilities || {}).forEach(function(name) {
  var v = audit.vulnerabilities[name];
  if (!v.effects || v.effects.length > 0) {
    prodVulns[name] = v;  // Has production dependents
  }
});
console.log('Production vulnerabilities: ' + Object.keys(prodVulns).length);
"

Trivy Scan Fails with "failed to initialize analyzer"

Error: failed to initialize analyzer: unable to initialize
the elf analyzer

This usually means the Docker socket is not available or the image was not built:

# Ensure Docker is available and the image exists
steps:
  - script: docker images | grep $(imageName) || echo "Image not found"
    displayName: 'Verify image exists'

  # For self-hosted agents, ensure Docker socket is mounted
  - script: |
      ls -la /var/run/docker.sock
      docker info --format '{{.ServerVersion}}'
    displayName: 'Verify Docker access'

Gitleaks False Positives on Test Fixtures

Finding: generic-api-key
File: test/fixtures/sample-config.json
Secret: AKIA1234567890EXAMPLE

Add exclusions in .gitleaks.toml:

[allowlist]
paths = [
  '''test/fixtures/''',
  '''__tests__/''',
  '''\.example$''',
  '''\.sample$'''
]

commits = [
  "abc123def456"  # Known commit with test data
]

Checkov Flags Intentional Configuration

Some IaC findings are intentional design decisions. Use inline suppressions:

# Terraform - suppress specific check
resource "azurerm_storage_account" "public_assets" {
  #checkov:skip=CKV_AZURE_35: Public blob access is intentional for CDN-served assets
  name                     = "publicassets"
  allow_nested_items_to_be_public = true
}

Security Gate Blocks Deployment for Known Issues

When a vulnerability has no fix available yet:

// Add exception handling in your security gate
var EXCEPTIONS = [
  {
    package: "lodash",
    cve: "CVE-2024-99999",
    reason: "No fix available, mitigated by input validation",
    expires: "2026-03-01"
  }
];

function isExcepted(finding) {
  var now = new Date();
  return EXCEPTIONS.some(function(ex) {
    var notExpired = new Date(ex.expires) > now;
    var matches = finding.package === ex.package && finding.cve === ex.cve;
    if (matches && notExpired) {
      console.log("  Exception: " + ex.cve + " - " + ex.reason + " (expires " + ex.expires + ")");
    }
    return matches && notExpired;
  });
}

Best Practices

  • Fail fast on critical and high vulnerabilities — Set your gate to block on critical immediately and on high vulnerabilities above a threshold. Moderate and low should warn but not block.
  • Run secret detection on every commit, not just PRs — Secrets pushed to any branch are exposed. Use pre-commit hooks with Gitleaks locally and pipeline scanning as a backstop.
  • Scan container images, not just application dependencies — Base image OS packages contain vulnerabilities too. Pin your base images and scan the final built image.
  • Maintain a suppression/exception process — Not every finding is actionable. Have a documented process for acknowledging accepted risks with expiration dates so exceptions do not become permanent.
  • Publish scan results as pipeline artifacts — Store every scan result as a build artifact for audit trails. Security teams need historical data to track remediation progress.
  • Use the same scanning configuration across all projects — Centralize your .gitleaks.toml, suppression files, and gate thresholds in a shared repository. Inconsistent scanning creates blind spots.
  • Scan IaC changes in pull requests — Infrastructure misconfigurations are easier to fix before merge. Run Checkov or Terrascan on PR builds targeting infrastructure directories.
  • Set up dashboards for vulnerability trends — Individual scan results are useful, but trending data shows whether your security posture is improving or degrading over time.

References

Powered by Contentful