Advanced

Security and Compliance in Azure Pipelines

IaC scanning, secret detection, SAST, deployment gates and compliance remediation in Azure Pipelines.

Level: Intermediate / Advanced

Table of Contents

  1. Introduction to Pipeline Security
  2. IaC Scanning with PSRule
  3. Gitleaks – Secret Detection
  4. SAST – Static Application Security Testing
  5. Deployment Gates and Environments
  6. Remediation with Azure Policy
  7. Compliance Reports and Alerts
  8. Complete Security Pipeline
  9. Comparative Tables
  10. Glossary

1. Introduction to Pipeline Security

Why Security in CI/CD Pipelines?

CI/CD pipelines are the royal road from your code to production. If they are not secured, every commit becomes a potential attack vector. Risks include:

  • Exposed secrets in source code (API keys, passwords, tokens)
  • Poor IaC configurations deployed to production (open ports, disabled encryption)
  • Vulnerable dependencies included in the build
  • Unsecured Docker images deployed on AKS or ACI
  • Unauthorized deployments without human review in production

The answer is a layered security pipeline (defense in depth) that intercepts these issues at each stage, well before they reach production.

Security Architecture of a Pipeline

flowchart TD
    DEV[Developer\npush code]
    PR[Pull Request]
    
    subgraph VALIDATE["Validate Stage"]
        GIT[Gitleaks\nSecret Scanning]
        PSRULE[PSRule\nIaC Scanning]
        SAST[SAST\nCode Analysis]
        TRIVY[Trivy\nContainer Scan]
        LINT[Bicep Lint\nSyntax Check]
    end
    
    subgraph DEPLOY_STAGING["Deploy Staging"]
        WHATIF[What-if\nDry run]
        DEPLOY_S[Deploy\nto Staging]
        TEST_E2E[E2E Tests\nOWASP ZAP]
    end
    
    subgraph GATES["Gates"]
        POLICY_CHECK[Azure Policy\nCompliance Check]
        APPROVAL["Human Approval\n(AppSec team)"]
        MONITOR_CHECK[Azure Monitor\nNo active alerts]
    end
    
    subgraph DEPLOY_PROD["Deploy Production"]
        DEPLOY_P[Deploy\nto Production]
        REPORT[Publish\nCompliance Reports]
    end
    
    DEV --> PR
    PR --> VALIDATE
    VALIDATE -->|All checks pass| DEPLOY_STAGING
    DEPLOY_STAGING -->|Tests pass| GATES
    GATES -->|All gates pass| DEPLOY_PROD
    
    style VALIDATE fill:#f97316,color:#fff
    style DEPLOY_STAGING fill:#3b82f6,color:#fff
    style GATES fill:#ef4444,color:#fff
    style DEPLOY_PROD fill:#10b981,color:#fff

Basic principle: Each problem detected early in the pipeline is less expensive to fix. A secret exposed detected in the IDE costs 5 minutes. The same secret discovered in production can cost millions.


2. IaC Scanning with PSRule

What is PSRule?

PSRule is an open-source tool developed by Microsoft for validating Infrastructure as Code (Bicep, ARM, Terraform) files against security and compliance rules before deployment.

graph TD
    subgraph INPUT["IaC Templates"]
        BICEP["*.bicep"]
        ARM["*.json (ARM)"]
        TF["*.tf (Terraform)"]
    end
    
    subgraph PSRULE_ENGINE["PSRule Engine"]
        BUILTIN["Pre-built rules\nPSRule.Rules.Azure\n200+ Azure rules"]
        CUSTOM["Custom rules\n(YAML or PowerShell)"]
        MODULES["Third-party modules"]
    end
    
    subgraph OUTPUT["Results"]
        PASS["✅ Pass\n(rules satisfied)"]
        FAIL["❌ Fail\n(misconfigurations detected)"]
        WARN["⚠️ Warning\n(improvements suggested)"]
        REPORT["NUnit3/SARIF Report"]
    end
    
    INPUT --> PSRULE_ENGINE
    PSRULE_ENGINE --> OUTPUT
    OUTPUT --> REPORT
    
    style PSRULE_ENGINE fill:#7c3aed,color:#fff
    style PASS fill:#10b981,color:#fff
    style FAIL fill:#ef4444,color:#fff
    style WARN fill:#f59e0b,color:#000

Examples of built-in PSRule.Rules.Azure rules:

RuleDescriptionSeverity
Azure.Storage.SecureTransferHTTPS forced on storage accountError
Azure.KeyVault.SoftDeleteSoft delete enabled on Key VaultError
Azure.KeyVault.PurgeProtectPurge protection enabled on Key VaultWarning
Azure.SQL.TDETransparent Data Encryption enabledError
Azure.AKS.NetworkPolicyNetwork Policies configuredWarning
Azure.NSG.AnyInboundSourceNo NSG “Any” inbound rulesError
Azure.AppService.MinTLSTLS 1.2 minimum on App ServiceWarning
Azure.Storage.BlobPublicAccessPublic blob access disabledError

Custom PSRule Rules

YAML format:

# rules/LocalRules.Rule.yaml
---
# Rule: Force TLS 1.2 and HTTPS on Storage Accounts
apiVersion: github.com/microsoft/PSRule/v1
kind: Rule
metadata:
  name: Local.YAML.RequireTLS
  annotations:
    category: Security
    severity: Error
spec:
  level: Warning
  type:
    - Microsoft.Storage/storageAccounts
  condition:
    allOf:
      - field: properties.supportsHttpsTrafficOnly
        equals: true
      - field: properties.minimumTlsVersion
        equals: TLS1_2

---
# Rule: Mandatory tags on all resources
apiVersion: github.com/microsoft/PSRule/v1
kind: Rule
metadata:
  name: Local.YAML.RequiredTags
  annotations:
    category: Governance
    severity: Warning
spec:
  level: Warning
  condition:
    allOf:
      - field: tags.Environment
        exists: true
      - field: tags.Owner
        exists: true
      - field: tags.CostCenter
        exists: true

PowerShell format:

# rules/Local.Rule.ps1

# Rule: Force TLS 1.2 and HTTPS on Storage Accounts
Rule 'Local.PS.AppServiceHttpsOnly' -Type 'Microsoft.Web/sites' {
    $Assert.HasFieldValue($TargetObject, 'properties.httpsOnly', $true)
}

# Rule: Key Vault with RBAC
Rule 'Local.PS.KeyVaultRBAC' -Type 'Microsoft.KeyVault/vaults' {
    $Assert.HasFieldValue($TargetObject, 'properties.enableRbacAuthorization', $true)
}

# Rule: AKS with Network Policy
Rule 'Local.PS.AKSNetworkPolicy' -Type 'Microsoft.ContainerService/managedClusters' {
    $Assert.HasFieldValue($TargetObject, 'properties.networkProfile.networkPolicy')
    $Assert.In($TargetObject, 'properties.networkProfile.networkPolicy', @('azure', 'calico'))
}

PSRule Configuration

# ps-rule.yaml - PSRule configuration for the project

requires:
  PSRule.Rules.Azure: '>=1.35.0'

include:
  module:
    - PSRule.Rules.Azure

configuration:
  AZURE_BICEP_FILE_EXPANSION: true
  AZURE_BICEP_CHECK_TOOL: true

input:
  pathIgnore:
    - '*.tests.bicep'
    - 'sandbox/**'

output:
  culture: 'en-US'

rule:
  # Minimum severity level to fail the build
  failLevel: Error
  
  include:
    - PSRule.Rules.Azure
    - Local.*
  
  # Exclude specific rules (with justification)
  exclude:
    # Rule not applicable because we use Private Endpoint
    - Azure.Storage.Firewall

Integration in Azure DevOps Pipeline

# azure-pipelines.yml - Security validation stage

stages:
  - stage: Validate
    displayName: 'Security Validation'
    jobs:
      - job: IaCScan
        displayName: 'IaC Security Scan (PSRule)'
        pool:
          vmImage: 'ubuntu-latest'
        steps:
          - checkout: self
          
          # 1. Install PSRule and Azure module
          - task: PowerShell@2
            displayName: 'Install PSRule'
            inputs:
              targetType: inline
              pwsh: true
              script: |
                Install-Module -Name PSRule.Rules.Azure -Force -Scope CurrentUser -MinimumVersion 1.35.0
          
          # 2. Compile Bicep templates
          - task: AzureCLI@2
            displayName: 'Build Bicep templates'
            inputs:
              azureSubscription: 'AzureServiceConnection'
              scriptType: bash
              scriptLocation: inlineScript
              inlineScript: |
                az bicep install
                find . -name "*.bicep" -not -name "*.tests.bicep" | while read file; do
                  az bicep build --file "$file" --outdir "$(Build.ArtifactStagingDirectory)/compiled"
                done
          
          # 3. Run PSRule
          - task: PSRule.Rules.Azure.PSRule@2
            displayName: 'Run PSRule IaC Analysis'
            inputs:
              inputType: repository
              inputPath: '**/*.bicep'
              modules: 'PSRule.Rules.Azure'
              baseline: 'Azure.Default'
              outputFormat: 'NUnit3'
              outputPath: '$(Build.ArtifactStagingDirectory)/psrule-results.xml'
            continueOnError: true
          
          # 4. Publish results as tests
          - task: PublishTestResults@2
            displayName: 'Publish PSRule Results'
            inputs:
              testResultsFormat: 'NUnit'
              testResultsFiles: '**/psrule-results.xml'
              testRunTitle: 'PSRule - IaC Security Analysis'
              failTaskOnFailedTests: true
            condition: always()

3. Gitleaks – Secret Detection

Why Secret Detection is Critical

A single exposed secret in a repository can compromise your entire infrastructure. Common scenarios:

  • AWS/Azure key accidentally committed in a .env file
  • API token hardcoded in test code
  • Database password in a connection string

Types of secrets detected by Gitleaks:

CategoryExamples
Cloud keysAWS Access Keys, Azure SAS tokens, GCP service accounts
API KeysStripe, Twilio, SendGrid, GitHub, GitLab
TokensJWT, OAuth tokens, Bearer tokens
CredentialsPasswords in connection URLs
Cryptographic keysRSA/EC private keys, SSH keys
CertificatesPrivate keys in .pem files

Gitleaks Configuration

# .gitleaks.toml - Custom configuration

[extend]
useDefault = true

# Custom rules
[[rules]]
description = "Azure DevOps Personal Access Token"
id = "azure-devops-pat"
regex = '''(?i)(AZURE_DEVOPS_PAT|ADO_TOKEN|AZDO_TOKEN)[\s\S]{0,20}[=:]\s*['"]\s*[A-Za-z0-9+/=]{52}'''
tags = ["secret", "azure"]

# Allowlist - patterns to ignore
[allowlist]
description = "Ignore false positives"
regexes = [
  '''(?i)EXAMPLE_API_KEY''',
  '''(?i)YOUR_API_KEY_HERE''',
  '''(?i)placeholder''',
]

# Ignore certain files
paths = [
  '''.gitleaks.toml''',
  '''\.github/workflows/''',
  '''node_modules/''',
]

Gitleaks Integration in Azure Pipelines

- stage: SecretScanning
  displayName: 'Secret Detection (Gitleaks)'
  jobs:
    - job: SecretScan
      pool:
        vmImage: 'ubuntu-latest'
      steps:
        - checkout: self
          fetchDepth: 0  # Fetch complete history for thorough scan
        
        - task: Gitleaks@2
          displayName: 'Gitleaks - Scan for secrets'
          inputs:
            scantype: nogit
            scanlocation: '$(Build.SourcesDirectory)'
            configtype: custom
            configfile: '.gitleaks.toml'
            reportformat: sarif
            reportpath: '$(Build.ArtifactStagingDirectory)/gitleaks-report.sarif'
        
        - task: PublishBuildArtifacts@1
          displayName: 'Publish Gitleaks Report'
          inputs:
            PathtoPublish: '$(Build.ArtifactStagingDirectory)/gitleaks-report.sarif'
            ArtifactName: 'gitleaks-report'
          condition: always()

Best Practices Against Exposed Secrets

Rule #1: Never commit a secret to Git, even in a temporary branch. A secret in Git history is as dangerous as a secret in main.

Rule #2: If a secret is exposed, immediate rotation is the priority over everything. Revoke the secret before removing it from history.

Rule #3: Use pre-commit hooks (Gitleaks detect --pre-commit) to block commits before they leave the developer’s machine.

# Configure Gitleaks as a pre-commit hook
cat > .git/hooks/pre-commit << 'EOF'
#!/bin/bash
gitleaks protect --staged --redact --config .gitleaks.toml
if [ $? -ne 0 ]; then
    echo "ERROR: Gitleaks detected secrets in staged changes!"
    echo "Commit blocked. Check for secrets before committing."
    exit 1
fi
EOF
chmod +x .git/hooks/pre-commit

4. SAST – Static Application Security Testing

Types of SAST Scanning for DevOps

graph TD
    CODE[Source code\nand artifacts]
    
    subgraph IaC_SCAN["IaC Scanning"]
        PSRULE2["PSRule\n(Bicep/ARM)"]
        CHECKOV["Checkov\n(Multi-IaC)"]
        TERRASCAN["Terrascan\n(Terraform)"]
    end
    
    subgraph CODE_SCAN["Source Code Scanning"]
        SONAR["SonarCloud\n(multi-language)"]
        CODEQL["GitHub CodeQL\n(C#, Java, Python, JS)"]
    end
    
    subgraph CONTAINER_SCAN["Container Scanning"]
        TRIVY2["Trivy\n(images + IaC)"]
        SNYK["Snyk\n(dependencies + containers)"]
    end
    
    subgraph SECRET_SCAN["Secret Scanning"]
        GITLEAKS["Gitleaks\n(Git history)"]
    end
    
    CODE --> IaC_SCAN
    CODE --> CODE_SCAN
    CODE --> CONTAINER_SCAN
    CODE --> SECRET_SCAN
    
    style IaC_SCAN fill:#3b82f6,color:#fff
    style CODE_SCAN fill:#8b5cf6,color:#fff
    style CONTAINER_SCAN fill:#f59e0b,color:#000
    style SECRET_SCAN fill:#ef4444,color:#fff

Complete SAST Pipeline

- stage: SAST
  displayName: 'Static Security Analysis'
  dependsOn: Build
  jobs:
    # JOB 1: IaC Scanning
    - job: IaCScanning
      displayName: 'IaC Security Scan'
      pool:
        vmImage: 'ubuntu-latest'
      steps:
        - checkout: self
        
        - task: PowerShell@2
          displayName: 'PSRule - Scan Bicep/ARM'
          inputs:
            pwsh: true
            targetType: inline
            script: |
              Install-Module PSRule.Rules.Azure -Force -Scope CurrentUser
              Invoke-PSRule -InputPath infrastructure/ -Module PSRule.Rules.Azure -OutputFormat NUnit3 -OutputPath psrule-results.xml
        
        - script: |
            pip install checkov
            checkov -d infrastructure/ \
                --framework bicep arm_template \
                --output-format sarif \
                --output-file checkov-results.sarif \
                --compact
          displayName: 'Checkov - IaC Scan'
    
    # JOB 2: Container Scanning (Trivy)
    - job: ContainerScan
      displayName: 'Container Security Scan'
      pool:
        vmImage: 'ubuntu-latest'
      steps:
        - script: |
            # Install Trivy
            curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin
            
            # Scan Docker image
            trivy image \
                --severity HIGH,CRITICAL \
                --format sarif \
                --output $(Build.ArtifactStagingDirectory)/trivy-results.sarif \
                --exit-code 1 \
                myacr.azurecr.io/myapp:$(Build.BuildId)
          displayName: 'Trivy - Container Vulnerability Scan'
          continueOnError: true

5. Deployment Gates and Environments

Azure DevOps Environments

Environments are named deployment targets in Azure DevOps that allow applying automatic security controls and manual approvals.

sequenceDiagram
    participant PIPE as Pipeline
    participant ENV as Environment "production"
    participant APPROVER as AppSec Team
    participant AZURE as Azure Resources

    PIPE->>ENV: Deployment triggered
    Note over ENV: Checks evaluation
    ENV->>ENV: Check: Branch control\n(must be main)
    ENV->>ENV: Check: Azure Policy\ncompliance
    ENV->>ENV: Check: No Monitor alerts
    ENV-->>APPROVER: Email: Approval required
    Note over APPROVER: Review SAST results,\nwhat-if, tests
    APPROVER->>ENV: Approve deployment
    ENV-->>PIPE: All checks passed
    PIPE->>AZURE: Deploy to production
    PIPE->>ENV: Record deployment

Configure an Environment with security checks:

stages:
  - stage: DeployProduction
    displayName: 'Deploy to Production'
    jobs:
      - deployment: DeployProd
        displayName: 'Production Deployment'
        pool:
          vmImage: 'ubuntu-latest'
        environment: production    # References the Azure DevOps Environment
        strategy:
          runOnce:
            deploy:
              steps:
                - task: AzureCLI@2
                  displayName: 'Deploy Bicep template'
                  inputs:
                    azureSubscription: 'AzureServiceConnection'
                    scriptType: bash
                    scriptLocation: inlineScript
                    inlineScript: |
                      az deployment group create \
                          --resource-group production-rg \
                          --template-file infrastructure/main.bicep \
                          --parameters @infrastructure/params.prod.json

Available checks on an Environment:

CheckDescriptionUse Case
ApprovalsManual approval by designated personsProduction deployment — mandatory human review
Branch ControlForce deployment from a specific branchOnly from main, not feature branches
Business HoursRestrict to business hoursNo weekend deployments
Exclusive LockOne deployment at a timeAvoid parallel deployments
Azure Monitor AlertsBlock if alerts are activeNo deployment if app is degraded
Invoke Azure FunctionCustom logic via Azure FunctionComplex business checks

Azure Policy as a Deployment Gate

- task: AzureCLI@2
  displayName: 'Azure Policy Compliance Check'
  inputs:
    azureSubscription: 'AzureServiceConnection'
    scriptType: bash
    scriptLocation: inlineScript
    inlineScript: |
      RESOURCE_GROUP="production-rg"
      
      NON_COMPLIANT_POLICIES=$(az policy state summarize \
          --resource-group $RESOURCE_GROUP \
          --query "results.nonCompliantPolicies" \
          --output tsv)
      
      if [ "$NON_COMPLIANT_POLICIES" -gt "0" ]; then
          echo "⚠️ WARNING: $NON_COMPLIANT_POLICIES non-compliant policies detected!"
          
          az policy state list \
              --resource-group $RESOURCE_GROUP \
              --filter "complianceState eq 'NonCompliant'" \
              --query "[].{Policy:policyDefinitionName, Resource:resourceId, State:complianceState}" \
              --output table
          
          echo "Deployment blocked until non-compliances are resolved."
          exit 1
      else
          echo "✅ All Azure policies are compliant. Deployment authorized."
      fi

6. Remediation with Azure Policy

DeployIfNotExists Effect: Auto-Remediation

The DeployIfNotExists effect allows Azure Policy to automatically fix non-compliant resources by deploying a corrective ARM template.

flowchart TD
    RESOURCE["Azure Resource\n(created or modified)"]
    EVAL{Azure Policy\nEvaluation}
    COMPLIANT["✅ Compliant\n(no action)"]
    TASK[DeployIfNotExists\ncorrective ARM template]
    MI[Managed Identity\nof the policy assignment]
    FIXED[Resource fixed\nautomatically]
    
    RESOURCE --> EVAL
    EVAL -->|Compliant| COMPLIANT
    EVAL -->|Non-compliant| TASK
    TASK --> MI
    MI -->|Deploy ARM template| FIXED
    
    style RESOURCE fill:#3b82f6,color:#fff
    style COMPLIANT fill:#10b981,color:#fff
    style TASK fill:#f97316,color:#fff
    style FIXED fill:#10b981,color:#fff

Example: DeployIfNotExists Policy for diagnostic settings:

{
  "properties": {
    "displayName": "Enable diagnostics on Key Vault",
    "policyRule": {
      "if": {
        "field": "type",
        "equals": "Microsoft.KeyVault/vaults"
      },
      "then": {
        "effect": "DeployIfNotExists",
        "details": {
          "type": "Microsoft.Insights/diagnosticSettings",
          "name": "keyvault-diagnostics",
          "roleDefinitionIds": [
            "/providers/Microsoft.Authorization/roleDefinitions/b24988ac-6180-42a0-ab88-20f7382dd24c"
          ],
          "deployment": {
            "properties": {
              "mode": "incremental",
              "template": { /* ARM template for diagnostics */ }
            }
          }
        }
      }
    }
  }
}

Deploy and Manage Policy Assignments

# ===== CREATE A POLICY DEFINITION =====
az policy definition create \
    --name "require-keyvault-diagnostics" \
    --display-name "Require Key Vault diagnostic settings" \
    --rules keyvault-diagnostics-policy.json \
    --mode Indexed

# ===== ASSIGN THE POLICY =====
ASSIGNMENT_ID=$(az policy assignment create \
    --name "enforce-kv-diagnostics" \
    --scope "/subscriptions/{sub-id}" \
    --policy "require-keyvault-diagnostics" \
    --assign-identity \
    --location eastus \
    --query id \
    --output tsv)

# ===== LAUNCH A REMEDIATION TASK =====
az policy remediation create \
    --name "remediate-kv-diagnostics" \
    --policy-assignment $ASSIGNMENT_ID \
    --resource-discovery-mode ReEvaluateCompliance

# Check remediation status
az policy remediation show \
    --name "remediate-kv-diagnostics" \
    --query "{Status:provisioningState, Successful:summary.successfulDeployments, Failed:summary.failedDeployments}" \
    --output table

7. Compliance Reports and Alerts

Compliance Reporting Architecture

graph TD
    subgraph PIPELINE["Pipeline Stages"]
        GITLEAKS_RPT[Gitleaks\nSARIF Report]
        PSRULE_RPT[PSRule\nNUnit3 Report]
        TRIVY_RPT[Trivy\nSARIF Report]
        SONAR_RPT[SonarCloud\nReport]
    end
    
    subgraph ARTIFACTS["Build Artifacts"]
        SARIF_FILES["SARIF Files\n(machine-readable)"]
        HTML_REPORTS["HTML Reports\n(human-readable)"]
        JUNIT_XML["JUnit/NUnit XML\n(test results)"]
    end
    
    subgraph STORAGE["Long-term Storage"]
        BLOB["Azure Blob Storage\n(compliance archive)"]
        ADO_TESTS[Azure DevOps\nTest Results]
    end
    
    subgraph VISIBILITY["Visibility"]
        BOARD[Azure DevOps\nDashboard]
        POLICY_DASH[Azure Policy\nCompliance Dashboard]
        DEFENDER_DASH[Defender for Cloud\nSecure Score]
    end
    
    PIPELINE --> ARTIFACTS
    ARTIFACTS --> STORAGE
    STORAGE --> VISIBILITY

Publishing Compliance Reports

- stage: ComplianceReporting
  displayName: 'Compliance Reporting'
  condition: always()
  jobs:
    - job: PublishReports
      pool:
        vmImage: 'ubuntu-latest'
      steps:
        # 1. Publish as build artifact
        - task: PublishBuildArtifacts@1
          displayName: 'Publish All Security Reports'
          inputs:
            PathtoPublish: '$(Build.ArtifactStagingDirectory)'
            ArtifactName: 'SecurityReports-$(Build.BuildId)'
          condition: always()
        
        # 2. Archive in Azure Blob Storage (for long-term traceability)
        - task: AzureCLI@2
          displayName: 'Archive Reports to Blob Storage'
          inputs:
            azureSubscription: 'AzureServiceConnection'
            scriptType: bash
            scriptLocation: inlineScript
            inlineScript: |
              az storage blob upload-batch \
                  --account-name stcompliancereports \
                  --destination "reports/$(Build.BuildId)" \
                  --source "$(Build.ArtifactStagingDirectory)"

8. Complete Security Pipeline

Full Azure Pipelines YAML

# azure-pipelines-security.yml

trigger:
  branches:
    include:
      - main
      - develop

variables:
  azureSubscription: 'AzureServiceConnection'
  acrName: 'myacr'
  resourceGroup: 'production-rg'
  bicepFile: 'infrastructure/main.bicep'
  paramsFile: 'infrastructure/params.prod.json'

stages:
  # ============================================================
  # STAGE 1: Code and configuration security
  # ============================================================
  - stage: SecurityValidation
    displayName: '🔐 Security Validation'
    jobs:
      - job: SecretScan
        displayName: 'Secret Detection'
        pool:
          vmImage: 'ubuntu-latest'
        steps:
          - checkout: self
            fetchDepth: 0
          
          - script: |
              docker run --rm \
                -v $(Build.SourcesDirectory):/path \
                zricethezav/gitleaks:latest detect \
                --source="/path" \
                --config="/path/.gitleaks.toml" \
                --exit-code 1
            displayName: 'Gitleaks - Secret Detection'
      
      - job: IaCSecurityScan
        displayName: 'IaC Security (PSRule + Checkov)'
        pool:
          vmImage: 'ubuntu-latest'
        steps:
          - checkout: self
          
          - task: PowerShell@2
            displayName: 'PSRule IaC Analysis'
            inputs:
              pwsh: true
              targetType: inline
              script: |
                Install-Module PSRule.Rules.Azure -Force -Scope CurrentUser
                Invoke-PSRule \
                    -InputPath infrastructure/ \
                    -Module PSRule.Rules.Azure \
                    -Format NUnit3 \
                    -OutputPath psrule-results.xml
          
          - script: |
              pip install checkov
              checkov -d infrastructure/ \
                  --framework bicep \
                  --compact \
                  --output-format sarif \
                  --output-file checkov-results.sarif
            displayName: 'Checkov IaC Analysis'
          
          - task: PublishTestResults@2
            inputs:
              testResultsFiles: 'psrule-results.xml'
              testRunTitle: 'PSRule Security Analysis'
            condition: always()
      
      - job: WhatIfAnalysis
        displayName: 'What-if Deployment Analysis'
        pool:
          vmImage: 'ubuntu-latest'
        steps:
          - task: AzureCLI@2
            displayName: 'Bicep What-if'
            inputs:
              azureSubscription: $(azureSubscription)
              scriptType: bash
              scriptLocation: inlineScript
              inlineScript: |
                az bicep install
                az deployment group what-if \
                    --resource-group $(resourceGroup) \
                    --template-file $(bicepFile) \
                    --parameters @$(paramsFile) \
                    --output json > whatif-results.json
  
  # ============================================================
  # STAGE 2: Build and container scanning
  # ============================================================
  - stage: BuildAndScan
    displayName: '🔨 Build & Container Scan'
    dependsOn: SecurityValidation
    jobs:
      - job: BuildScanPush
        displayName: 'Build, Scan & Push Image'
        pool:
          vmImage: 'ubuntu-latest'
        steps:
          - task: Docker@2
            displayName: 'Build Docker Image'
            inputs:
              command: build
              repository: '$(acrName).azurecr.io/myapp'
              tags: '$(Build.BuildId)'
          
          - script: |
              curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin
              trivy image \
                  --severity HIGH,CRITICAL \
                  --exit-code 1 \
                  --format sarif \
                  --output trivy-results.sarif \
                  $(acrName).azurecr.io/myapp:$(Build.BuildId)
            displayName: 'Trivy Container Scan'
  
  # ============================================================
  # STAGE 3: Staging deployment
  # ============================================================
  - stage: DeployStaging
    displayName: '🚀 Deploy to Staging'
    dependsOn: BuildAndScan
    jobs:
      - deployment: DeployStaging
        environment: staging
        pool:
          vmImage: 'ubuntu-latest'
        strategy:
          runOnce:
            deploy:
              steps:
                - task: AzureCLI@2
                  displayName: 'Deploy to Staging'
                  inputs:
                    azureSubscription: $(azureSubscription)
                    scriptType: bash
                    scriptLocation: inlineScript
                    inlineScript: |
                      az deployment group create \
                          --resource-group staging-rg \
                          --template-file $(bicepFile) \
                          --parameters @infrastructure/params.staging.json
  
  # ============================================================
  # STAGE 4: Tests and validation
  # ============================================================
  - stage: Test
    displayName: '🧪 Integration & Security Tests'
    dependsOn: DeployStaging
    jobs:
      - job: E2ETests
        displayName: 'End-to-End Tests'
        pool:
          vmImage: 'ubuntu-latest'
        steps:
          - script: |
              npm install @playwright/test
              npx playwright test --reporter=junit
            displayName: 'Playwright E2E Tests'
          
          - script: |
              docker run --rm \
                  -v $(Build.ArtifactStagingDirectory):/zap/wrk \
                  owasp/zap2docker-stable:latest \
                  zap-baseline.py \
                  -t https://staging.my-app.com \
                  -r zap-report.html
            displayName: 'OWASP ZAP DAST Scan'
  
  # ============================================================
  # STAGE 5: Production deployment (with gates)
  # ============================================================
  - stage: DeployProduction
    displayName: '🏭 Deploy to Production'
    dependsOn: Test
    condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
    jobs:
      - deployment: DeployProd
        environment: production   # Environment with configured approvals
        pool:
          vmImage: 'ubuntu-latest'
        strategy:
          runOnce:
            deploy:
              steps:
                - task: AzureCLI@2
                  displayName: 'Azure Policy Compliance Check'
                  inputs:
                    azureSubscription: $(azureSubscription)
                    scriptType: bash
                    scriptLocation: inlineScript
                    inlineScript: |
                      NON_COMPLIANT=$(az policy state summarize \
                          --resource-group $(resourceGroup) \
                          --query "results.nonCompliantPolicies" \
                          --output tsv)
                      
                      if [ "$NON_COMPLIANT" -gt "0" ]; then
                          echo "❌ $NON_COMPLIANT non-compliant policies! Deployment blocked."
                          exit 1
                      fi
                
                - task: AzureCLI@2
                  displayName: 'Deploy to Production'
                  inputs:
                    azureSubscription: $(azureSubscription)
                    scriptType: bash
                    scriptLocation: inlineScript
                    inlineScript: |
                      az deployment group create \
                          --resource-group $(resourceGroup) \
                          --template-file $(bicepFile) \
                          --parameters @$(paramsFile)
  
  # ============================================================
  # STAGE 6: Compliance reports (always executed)
  # ============================================================
  - stage: ComplianceReports
    displayName: '📊 Compliance Reports'
    dependsOn:
      - SecurityValidation
      - BuildAndScan
      - Test
      - DeployProduction
    condition: always()
    jobs:
      - job: PublishReports
        pool:
          vmImage: 'ubuntu-latest'
        steps:
          - task: PublishBuildArtifacts@1
            displayName: 'Archive Compliance Reports'
            inputs:
              PathtoPublish: '$(Build.ArtifactStagingDirectory)'
              ArtifactName: 'ComplianceReports-$(Build.BuildId)'

9. Comparative Tables

Security Pipeline Tools: Comparison

ToolCategoryOpen SourceADO IntegrationCoverageSeverity
PSRuleIaC Scanning✅ TaskBicep, ARM, TerraformError/Warning
CheckovIaC Scanning✅ ScriptMulti-IaCLow→Critical
GitleaksSecret Detection✅ TaskGit history-
TrivyContainer + IaC✅ ScriptImages, FS, IaCUnknown→Critical
OWASP ZAPDAST✅ TaskWeb appsLow→High
SonarCloudSAST Code✅ Community✅ TaskMulti-languageLow→Blocker
SnykDependencies❌ Paid✅ TaskNPM, Maven, etc.Low→Critical
Defender for CloudRuntime❌ Paid✅ NativeAzure resourcesLow→High

Azure Policy Effects

EffectDescriptionActionUse Case
AuditLogs non-complianceNo corrective actionMonitoring, visibility
DenyRejects creation/modificationBlocks the resourceMandatory standards
DeployIfNotExistsDeploys missing resourcesAuto-remediation ARMDiagnostics, tags
ModifyModifies properties at deploymentAutomatic patchTags, encryption
AuditIfNotExistsAudits if a related resource is missingLogs onlyMissing dependencies
AppendAdds properties to the resourceAdds fieldsAdditional tags
DisabledDisables the ruleNoneTesting, exceptions

10. Glossary

TermDefinition
PSRuleOpen-source PowerShell module for validating IaC templates against best practice rules
GitleaksTool for detecting secrets in Git history
SASTStatic Application Security Testing — static code analysis without executing it
DASTDynamic Application Security Testing — tests on the running application
OWASP ZAPZed Attack Proxy — open-source DAST tool from OWASP
TrivyVersatile security scanner (Docker images, IaC, file systems)
SARIFStatic Analysis Results Interchange Format — industry standard for security results
NUnit3Test report format compatible with Azure DevOps PublishTestResults
EnvironmentAzure DevOps deployment target with checks, approvals and history
Deployment GateControl that must be passed before a deployment proceeds
ApprovalManual validation required before a deployment
Branch ControlCheck that restricts deployments to specific branches
DeployIfNotExistsAzure Policy effect for auto-remediating non-compliant resources
Remediation TaskManual or automatic task to fix non-compliant resources
Managed IdentityIdentity used by Azure Policy to execute remediation templates
CheckovOpen-source IaC scanning tool supporting Terraform, Bicep, ARM, CloudFormation
What-ifDry-run mode of ARM/Bicep deployment to preview changes
CVECommon Vulnerabilities and Exposures — vulnerability database

Search Terms

security · compliance · azure · pipelines · devops · iac · microsoft · pipeline · policy · psrule · gitleaks · sast · architecture · configuration · deployment · detection · environments · integration · reports · scanning · secret

Interested in this course?

Contact us to book it or get a custom training plan for your team.