Level: Intermediate / Advanced
Table of Contents
- Introduction to Pipeline Security
- IaC Scanning with PSRule
- Gitleaks – Secret Detection
- SAST – Static Application Security Testing
- Deployment Gates and Environments
- Remediation with Azure Policy
- Compliance Reports and Alerts
- Complete Security Pipeline
- Comparative Tables
- 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:
| Rule | Description | Severity |
|---|---|---|
Azure.Storage.SecureTransfer | HTTPS forced on storage account | Error |
Azure.KeyVault.SoftDelete | Soft delete enabled on Key Vault | Error |
Azure.KeyVault.PurgeProtect | Purge protection enabled on Key Vault | Warning |
Azure.SQL.TDE | Transparent Data Encryption enabled | Error |
Azure.AKS.NetworkPolicy | Network Policies configured | Warning |
Azure.NSG.AnyInboundSource | No NSG “Any” inbound rules | Error |
Azure.AppService.MinTLS | TLS 1.2 minimum on App Service | Warning |
Azure.Storage.BlobPublicAccess | Public blob access disabled | Error |
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
.envfile - API token hardcoded in test code
- Database password in a connection string
Types of secrets detected by Gitleaks:
| Category | Examples |
|---|---|
| Cloud keys | AWS Access Keys, Azure SAS tokens, GCP service accounts |
| API Keys | Stripe, Twilio, SendGrid, GitHub, GitLab |
| Tokens | JWT, OAuth tokens, Bearer tokens |
| Credentials | Passwords in connection URLs |
| Cryptographic keys | RSA/EC private keys, SSH keys |
| Certificates | Private 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:
| Check | Description | Use Case |
|---|---|---|
| Approvals | Manual approval by designated persons | Production deployment — mandatory human review |
| Branch Control | Force deployment from a specific branch | Only from main, not feature branches |
| Business Hours | Restrict to business hours | No weekend deployments |
| Exclusive Lock | One deployment at a time | Avoid parallel deployments |
| Azure Monitor Alerts | Block if alerts are active | No deployment if app is degraded |
| Invoke Azure Function | Custom logic via Azure Function | Complex 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
| Tool | Category | Open Source | ADO Integration | Coverage | Severity |
|---|---|---|---|---|---|
| PSRule | IaC Scanning | ✅ | ✅ Task | Bicep, ARM, Terraform | Error/Warning |
| Checkov | IaC Scanning | ✅ | ✅ Script | Multi-IaC | Low→Critical |
| Gitleaks | Secret Detection | ✅ | ✅ Task | Git history | - |
| Trivy | Container + IaC | ✅ | ✅ Script | Images, FS, IaC | Unknown→Critical |
| OWASP ZAP | DAST | ✅ | ✅ Task | Web apps | Low→High |
| SonarCloud | SAST Code | ✅ Community | ✅ Task | Multi-language | Low→Blocker |
| Snyk | Dependencies | ❌ Paid | ✅ Task | NPM, Maven, etc. | Low→Critical |
| Defender for Cloud | Runtime | ❌ Paid | ✅ Native | Azure resources | Low→High |
Azure Policy Effects
| Effect | Description | Action | Use Case |
|---|---|---|---|
| Audit | Logs non-compliance | No corrective action | Monitoring, visibility |
| Deny | Rejects creation/modification | Blocks the resource | Mandatory standards |
| DeployIfNotExists | Deploys missing resources | Auto-remediation ARM | Diagnostics, tags |
| Modify | Modifies properties at deployment | Automatic patch | Tags, encryption |
| AuditIfNotExists | Audits if a related resource is missing | Logs only | Missing dependencies |
| Append | Adds properties to the resource | Adds fields | Additional tags |
| Disabled | Disables the rule | None | Testing, exceptions |
10. Glossary
| Term | Definition |
|---|---|
| PSRule | Open-source PowerShell module for validating IaC templates against best practice rules |
| Gitleaks | Tool for detecting secrets in Git history |
| SAST | Static Application Security Testing — static code analysis without executing it |
| DAST | Dynamic Application Security Testing — tests on the running application |
| OWASP ZAP | Zed Attack Proxy — open-source DAST tool from OWASP |
| Trivy | Versatile security scanner (Docker images, IaC, file systems) |
| SARIF | Static Analysis Results Interchange Format — industry standard for security results |
| NUnit3 | Test report format compatible with Azure DevOps PublishTestResults |
| Environment | Azure DevOps deployment target with checks, approvals and history |
| Deployment Gate | Control that must be passed before a deployment proceeds |
| Approval | Manual validation required before a deployment |
| Branch Control | Check that restricts deployments to specific branches |
| DeployIfNotExists | Azure Policy effect for auto-remediating non-compliant resources |
| Remediation Task | Manual or automatic task to fix non-compliant resources |
| Managed Identity | Identity used by Azure Policy to execute remediation templates |
| Checkov | Open-source IaC scanning tool supporting Terraform, Bicep, ARM, CloudFormation |
| What-if | Dry-run mode of ARM/Bicep deployment to preview changes |
| CVE | Common 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