DevSecOps Shift-Left in Azure DevOps: A Production Blueprint
Security as a last gate before production is dead. In my work at Revantage Asia and across Accenture engagements, I've baked security into every pipeline stage. Here's the exact blueprint.
The Shift-Left Mindset
Traditional: Dev → Test → Stage → [Security Scan] → Prod ← too late
Shift-left: [Sec] Dev → [Sec] Build → [Sec] Test → [Sec] Deploy → Prod
Cost of fixing a bug: Dev ($1) → Test ($10) → Prod ($100) → Breach ($10,000+)
The Five Security Layers
1. SAST — Static Application Security Testing
# azure-pipelines.yml
- task: Bash@3
displayName: 'SAST - Semgrep Scan'
inputs:
targetType: 'inline'
script: |
docker run --rm -v $(Build.SourcesDirectory):/src \
returntocorp/semgrep semgrep --config=auto \
--sarif --output=/src/semgrep-results.sarif /src
- task: PublishTestResults@2
inputs:
testResultsFormat: 'JUnit'
searchFolder: '$(Build.SourcesDirectory)'
testResultsFiles: '**/semgrep-results.sarif'
2. SCA — Software Composition Analysis (Snyk)
- task: SnykSecurityScan@1
displayName: 'SCA - Snyk Dependency Scan'
inputs:
serviceConnectionEndpoint: 'snyk-connection'
testType: 'app'
targetFile: 'requirements.txt'
severityThreshold: 'high'
failOnIssues: true
monitorWhenNoIssues: true
additionalArguments: '--all-projects --policy-path=.snyk'
3. Container Scanning (Trivy)
- task: Bash@3
displayName: 'Container Scan - Trivy'
inputs:
targetType: 'inline'
script: |
# Install Trivy
wget -qO- https://aquasecurity.github.io/trivy-repo/deb/public.key | \
sudo apt-key add -
sudo apt-get install -y trivy
# Scan with fail on HIGH/CRITICAL
trivy image \
--exit-code 1 \
--severity HIGH,CRITICAL \
--format sarif \
--output trivy-results.sarif \
$(containerRegistry)/$(imageRepository):$(Build.BuildId)
- task: PublishBuildArtifacts@1
condition: always()
inputs:
PathtoPublish: 'trivy-results.sarif'
ArtifactName: 'SecurityReports'
4. IaC Security (Checkov + tfsec)
- task: Bash@3
displayName: 'IaC Scan - Checkov'
inputs:
targetType: 'inline'
script: |
pip install checkov
checkov -d ./terraform \
--framework terraform \
--output sarif \
--output-file-path checkov-results.sarif \
--compact \
--skip-check CKV_AZURE_33,CKV_AZURE_44 # documented exceptions only
- task: Bash@3
displayName: 'IaC Scan - tfsec'
inputs:
targetType: 'inline'
script: |
docker run --rm -v $(Build.SourcesDirectory):/src \
aquasec/tfsec /src/terraform \
--format sarif \
--out /src/tfsec-results.sarif
5. OPA Policy Gates
This is where policy-as-code prevents mis-configured K8s deployments:
# policies/k8s-security.rego
package kubernetes.security
deny[msg] {
input.kind == "Deployment"
container := input.spec.template.spec.containers[_]
not container.securityContext.runAsNonRoot
msg := sprintf("Container '%v' must run as non-root", [container.name])
}
deny[msg] {
input.kind == "Deployment"
container := input.spec.template.spec.containers[_]
container.securityContext.privileged
msg := sprintf("Container '%v' must not be privileged", [container.name])
}
deny[msg] {
input.kind == "Deployment"
container := input.spec.template.spec.containers[_]
not container.resources.limits.memory
msg := sprintf("Container '%v' must have memory limits", [container.name])
}
- task: Bash@3
displayName: 'Policy Gate - OPA/Conftest'
inputs:
targetType: 'inline'
script: |
conftest test ./kubernetes/manifests/ \
--policy ./policies/ \
--output tap
Full Pipeline YAML
trigger:
branches:
include: ['main', 'develop', 'release/*']
variables:
containerRegistry: 'acr-revantage.azurecr.io'
imageRepository: 'api-service'
stages:
- stage: SecurityGates
displayName: 'Security Scan'
jobs:
- job: SAST
pool:
vmImage: 'ubuntu-latest'
steps:
- template: templates/sast-scan.yml
- job: SCA
pool:
vmImage: 'ubuntu-latest'
steps:
- template: templates/snyk-scan.yml
- stage: Build
dependsOn: SecurityGates
condition: succeeded()
jobs:
- job: BuildAndScan
steps:
- task: Docker@2
inputs:
command: 'buildAndPush'
repository: $(imageRepository)
- template: templates/trivy-scan.yml
- stage: IaCSecurity
dependsOn: Build
jobs:
- job: TerraformSecurity
steps:
- template: templates/checkov-tfsec.yml
- template: templates/opa-gate.yml
- stage: Deploy
dependsOn: IaCSecurity
condition: succeeded()
jobs:
- deployment: Production
environment: 'production'
strategy:
runOnce:
deploy:
steps:
- template: templates/deploy.yml
SARIF Reports in Azure DevOps
SARIF (Static Analysis Results Interchange Format) integrates directly into Azure DevOps Security scans tab — developers see issues inline with code:
- task: PublishBuildArtifacts@1
condition: always()
inputs:
PathtoPublish: '$(Build.SourcesDirectory)'
ArtifactName: 'CodeAnalysisLogs'
# This enables the Security tab in Azure DevOps
- task: PublishSecurityAnalysisLogs@3
condition: always()
inputs:
ArtifactName: 'CodeAnalysisLogs'
ArtifactType: 'Container'
Results from Production
After implementing this across enterprise pipelines:
- 93% of CVEs caught before staging
- Zero HIGH/CRITICAL CVEs in production (12 months)
- Developer adoption: Initially 40% pushback → 95% after seeing inline feedback
- Mean time to remediate: 4 days → 6 hours (issues caught earlier = easier fixes)
The complete pipeline blueprint is at github.com/suhail39ahmed/devsecops-pipeline-blueprint