Introduction: Security Shifts Left, Right, and Everywhere
DevSecOps is not about adding security gates to your pipeline — it's about embedding security into every phase of the software development lifecycle. This deep dive builds a complete DevSecOps platform that catches vulnerabilities in code (SAST), dependencies (SCA), containers (image scanning), infrastructure (IaC scanning), runtime (DAST), and supply chain (SBOM + provenance). Security findings are automatically triaged, tracked, and fed back to developers where they work.

Prerequisites
- GitHub Enterprise Cloud with Advanced Security licenses
- Azure DevOps organization with pipeline agents
- Azure Container Registry with Defender for Containers
- Microsoft Defender for Cloud (CSPM plan)
- Azure Key Vault for secrets management
- Familiarity with YAML pipelines, Docker, and Kubernetes
Phase 1: GitHub Advanced Security Configuration
Code Scanning with CodeQL
# .github/workflows/codeql-analysis.yml
name: "CodeQL Security Analysis"
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
schedule:
- cron: '0 6 * * 1' # Weekly Monday 6 AM
permissions:
security-events: write
contents: read
actions: read
jobs:
analyze:
name: Analyze (${{ matrix.language }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
language: ['csharp', 'javascript', 'python']
include:
- language: csharp
build-mode: manual
- language: javascript
build-mode: none
- language: python
build-mode: none
steps:
- uses: actions/checkout@v4
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
queries: security-extended,security-and-quality
config: |
paths-ignore:
- '**/test/**'
- '**/tests/**'
- '**/*.test.*'
query-filters:
- exclude:
tags: /test/
- name: Build (.NET)
if: matrix.language == 'csharp'
run: dotnet build --configuration Release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
with:
category: "/language:${{ matrix.language }}"
upload: true
dependency-review:
name: Dependency Review
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
steps:
- uses: actions/checkout@v4
- name: Dependency Review
uses: actions/dependency-review-action@v4
with:
fail-on-severity: high
deny-licenses: GPL-3.0, AGPL-3.0
allow-ghsas: GHSA-xxxx-xxxx-xxxx # Known accepted risks
comment-summary-in-pr: always
Secret Scanning with Custom Patterns
# .github/secret-scanning/custom-patterns.yml
patterns:
- name: "Internal API Key"
pattern: "CORP-API-[A-Z0-9]{32}"
description: "Corporate internal API key format"
severity: "critical"
- name: "Database Connection String"
pattern: "Server=(?:tcp:)?[a-zA-Z0-9.-]+;.*Password=[^;\"']+"
description: "SQL Server connection string with embedded password"
severity: "critical"
- name: "Azure Storage Account Key"
pattern: "[A-Za-z0-9+/]{86}=="
description: "Potential Azure Storage account key (Base64, 88 chars)"
severity: "high"
additional_match_requirements:
- context_contains: ["AccountKey", "StorageAccountKey", "AZURE_STORAGE"]
- name: "Internal Certificate Passphrase"
pattern: "CERT-PASS-[A-Za-z0-9!@#$%]{16,}"
description: "Internal certificate passphrase format"
severity: "critical"
Phase 2: Azure DevOps CI/CD Security Pipeline
Comprehensive Security Pipeline
# azure-pipelines/security-pipeline.yml
trigger:
branches:
include: [main, develop, release/*]
pool:
vmImage: 'ubuntu-latest'
variables:
- group: 'security-scanning-config'
- name: acrName
value: 'corpcontainerregistry'
- name: imageRepository
value: 'app/order-service'
stages:
- stage: SecurityScan
displayName: 'Security Scanning'
jobs:
- job: SAST
displayName: 'Static Application Security Testing'
steps:
- task: UseDotNet@2
inputs:
version: '8.x'
- script: |
dotnet tool install --global security-scan
dotnet tool install --global dotnet-retire
displayName: 'Install security tools'
- script: |
dotnet restore
dotnet build --no-restore
displayName: 'Build application'
- task: SonarCloudPrepare@2
inputs:
SonarCloud: 'sonarcloud-connection'
organization: 'contoso'
scannerMode: 'MSBuild'
projectKey: 'contoso_order-service'
extraProperties: |
sonar.cs.opencover.reportsPaths=$(Build.SourcesDirectory)/coverage.xml
sonar.exclusions=**/Migrations/**,**/Tests/**
- task: SonarCloudAnalyze@2
displayName: 'Run SonarCloud Analysis'
- task: SonarCloudPublish@2
inputs:
pollingTimeoutSec: '300'
- job: SCA
displayName: 'Software Composition Analysis'
steps:
- script: |
# Check for known vulnerabilities in NuGet packages
dotnet list package --vulnerable --include-transitive --format json > vulnerable-packages.json
# Parse and fail on critical/high vulnerabilities
CRITICAL=$(jq '[.projects[].frameworks[].topLevelPackages[]? | select(.vulnerabilities[]?.severity == "Critical")] | length' vulnerable-packages.json)
HIGH=$(jq '[.projects[].frameworks[].topLevelPackages[]? | select(.vulnerabilities[]?.severity == "High")] | length' vulnerable-packages.json)
echo "Critical vulnerabilities: $CRITICAL"
echo "High vulnerabilities: $HIGH"
if [ "$CRITICAL" -gt 0 ]; then
echo "##vso[task.logissue type=error]Critical vulnerabilities found in dependencies"
exit 1
fi
displayName: 'Check NuGet vulnerabilities'
- script: |
# Generate SBOM
dotnet tool install --global Microsoft.Sbom.DotNetTool
sbom-tool generate \
-b $(Build.BinariesDirectory) \
-bc $(Build.SourcesDirectory) \
-pn "OrderService" \
-pv $(Build.BuildNumber) \
-ps "Contoso" \
-nsb "https://contoso.com/sbom" \
-V Verbose
displayName: 'Generate SBOM (SPDX)'
- publish: $(Build.BinariesDirectory)/_manifest
artifact: 'SBOM'
displayName: 'Publish SBOM artifact'
- job: IaCScan
displayName: 'Infrastructure-as-Code Scanning'
steps:
- script: |
pip install checkov
checkov -d infra/ \
--framework bicep arm_template dockerfile \
--output junitxml \
--output-file-path checkov-results.xml \
--soft-fail-on LOW \
--hard-fail-on CRITICAL,HIGH \
--skip-check CKV_AZURE_35,CKV_AZURE_43
displayName: 'Run Checkov IaC scan'
- task: PublishTestResults@2
inputs:
testResultsFormat: 'JUnit'
testResultsFiles: 'checkov-results.xml'
testRunTitle: 'IaC Security Scan'
condition: always()
- stage: ContainerSecurity
displayName: 'Container Security'
dependsOn: SecurityScan
jobs:
- job: BuildAndScan
displayName: 'Build and Scan Container Image'
steps:
- task: Docker@2
displayName: 'Build container image'
inputs:
containerRegistry: 'acr-connection'
repository: $(imageRepository)
command: 'build'
Dockerfile: '**/Dockerfile'
tags: |
$(Build.BuildId)
latest
- script: |
# Install and run Trivy for container scanning
curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin
trivy image \
--severity CRITICAL,HIGH \
--exit-code 1 \
--format json \
--output trivy-results.json \
--ignore-unfixed \
$(acrName).azurecr.io/$(imageRepository):$(Build.BuildId)
# Also generate SARIF for GitHub Advanced Security integration
trivy image \
--format sarif \
--output trivy-results.sarif \
$(acrName).azurecr.io/$(imageRepository):$(Build.BuildId)
displayName: 'Trivy container scan'
- script: |
# Sign container image with Notation (Notary v2)
notation sign \
--signature-format cose \
--key azure-kv://corp-keyvault/signing-key \
$(acrName).azurecr.io/$(imageRepository):$(Build.BuildId)
displayName: 'Sign container image'
- script: |
# Attach SBOM to container image
oras attach \
--artifact-type application/spdx+json \
$(acrName).azurecr.io/$(imageRepository):$(Build.BuildId) \
$(Build.BinariesDirectory)/_manifest/spdx_2.2/manifest.spdx.json
displayName: 'Attach SBOM to image'
- task: Docker@2
displayName: 'Push signed image'
inputs:
containerRegistry: 'acr-connection'
repository: $(imageRepository)
command: 'push'
tags: |
$(Build.BuildId)
- stage: DAST
displayName: 'Dynamic Security Testing'
dependsOn: ContainerSecurity
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
jobs:
- job: ZAPScan
displayName: 'OWASP ZAP Scan'
steps:
- script: |
# Deploy to staging environment
kubectl apply -f k8s/staging/ --namespace staging
kubectl rollout status deployment/order-service --namespace staging --timeout=300s
displayName: 'Deploy to staging'
- script: |
# Run OWASP ZAP baseline scan
docker run --rm \
-v $(Build.SourcesDirectory)/zap:/zap/wrk:rw \
ghcr.io/zaproxy/zaproxy:stable \
zap-baseline.py \
-t https://staging-api.contoso.com \
-J zap-report.json \
-r zap-report.html \
-c zap-config.conf \
-I
displayName: 'OWASP ZAP baseline scan'
- publish: $(Build.SourcesDirectory)/zap
artifact: 'DAST-Results'

Phase 3: Supply Chain Security
SLSA Provenance Generation
# .github/workflows/slsa-provenance.yml
name: SLSA Provenance
on:
push:
branches: [main]
permissions:
id-token: write
contents: read
attestations: write
jobs:
build-with-provenance:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build artifact
run: |
dotnet publish -c Release -o ./publish
tar -czf order-service-$(date +%Y%m%d).tar.gz -C ./publish .
- name: Generate SLSA provenance
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.0.0
with:
base64-subjects: |
$(sha256sum order-service-*.tar.gz | base64 -w0)
upload-assets: true
- name: Attest build provenance
uses: actions/attest-build-provenance@v1
with:
subject-path: 'order-service-*.tar.gz'
Admission Controller for Image Verification
# k8s/admission-policy.yaml
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
name: "require-signed-images"
spec:
failurePolicy: Fail
matchConstraints:
resourceRules:
- apiGroups: [""]
apiVersions: ["v1"]
operations: ["CREATE", "UPDATE"]
resources: ["pods"]
namespaceSelector:
matchLabels:
security-policy: "strict"
validations:
- expression: |
object.spec.containers.all(c,
c.image.startsWith('corpcontainerregistry.azurecr.io/')
)
message: "All container images must come from the corporate registry"
- expression: |
!object.spec.containers.exists(c,
c.securityContext == null || c.securityContext.runAsNonRoot != true
)
message: "All containers must run as non-root"
- expression: |
!object.spec.containers.exists(c,
c.securityContext == null || c.securityContext.readOnlyRootFilesystem != true
)
message: "All containers must use read-only root filesystem"
---
# Ratify configuration for signature verification
apiVersion: config.ratify.deislabs.io/v1beta1
kind: Verifier
metadata:
name: notation-verifier
spec:
name: notation
artifactTypes: application/vnd.cncf.notary.signature
parameters:
verificationCertStores:
ca:
ca-certs:
- corporate-signing-ca
trustPolicyDoc:
version: "1.0"
trustPolicies:
- name: "corporate-images"
registryScopes:
- "corpcontainerregistry.azurecr.io/**"
signatureVerification:
level: strict
trustStores:
- "ca:ca-certs"
trustedIdentities:
- "x509.subject: CN=Contoso Code Signing"
Phase 4: Security Findings Management
Unified Security Dashboard
public class SecurityFindingsAggregator
{
private readonly ILogger<SecurityFindingsAggregator> _logger;
public async Task<SecurityDashboard> AggregateFindings()
{
var dashboard = new SecurityDashboard();
// Aggregate from GitHub Advanced Security
var codeqlFindings = await _githubClient.GetCodeScanningAlertsAsync("contoso", "order-service");
var secretAlerts = await _githubClient.GetSecretScanningAlertsAsync("contoso", "order-service");
var dependabotAlerts = await _githubClient.GetDependabotAlertsAsync("contoso", "order-service");
// Aggregate from container scanning
var containerFindings = await _defenderClient.GetContainerVulnerabilitiesAsync("corpcontainerregistry");
// Aggregate from Defender for Cloud
var cloudFindings = await _defenderCloudClient.GetSecurityAlertsAsync();
dashboard.Summary = new SecuritySummary
{
CodeVulnerabilities = codeqlFindings.Count(f => f.State == "open"),
SecretExposures = secretAlerts.Count(f => f.State == "open"),
DependencyVulnerabilities = dependabotAlerts.Count(f => f.State == "open"),
ContainerVulnerabilities = containerFindings.Count(f => f.Severity >= Severity.High),
CloudMisconfigurations = cloudFindings.Count(f => f.Status == "Active"),
MeanTimeToRemediate = CalculateMTTR(codeqlFindings.Concat<ISecurityFinding>(dependabotAlerts)),
SLACompliance = CalculateSLACompliance(codeqlFindings)
};
// Calculate risk score
dashboard.RiskScore = CalculateRiskScore(dashboard.Summary);
return dashboard;
}
private double CalculateRiskScore(SecuritySummary summary)
{
return 100.0
- (summary.CodeVulnerabilities * 5)
- (summary.SecretExposures * 20)
- (summary.DependencyVulnerabilities * 3)
- (summary.ContainerVulnerabilities * 8)
- (summary.CloudMisconfigurations * 4);
}
}

SLA for Security Findings
| Severity | Detection → Triage | Triage → Fix | Fix → Deploy | Total SLA |
|---|---|---|---|---|
| Critical | 1 hour | 4 hours | 2 hours | 7 hours |
| High | 4 hours | 3 days | 1 day | 4 days |
| Medium | 1 day | 2 weeks | 1 week | 3 weeks |
| Low | 1 week | Next sprint | Regular release | 6 weeks |
Best Practices
- Shift left pragmatically: Developers need findings in their IDE/PR, not in a separate security tool
- SBOM everything: Generate and attach SBOMs to every artifact — it's becoming a regulatory requirement
- Sign container images: Use Notation/Notary v2 with admission controllers to prevent unsigned images
- Secret scanning with push protection: Block commits containing secrets before they reach the repository
- Automate dependency updates: Dependabot + auto-merge for patch updates, manual review for major
- IaC scanning in PR: Catch Bicep/Terraform misconfigurations before they deploy
- DAST in staging: Run OWASP ZAP against staging on every main branch build
Architecture Decision and Tradeoffs
When designing integrated solutions solutions with Azure + Power Platform, consider these key architectural trade-offs:
| Approach | Best For | Tradeoff |
|---|---|---|
| Managed / platform service | Rapid delivery, reduced ops burden | Less customisation, potential vendor lock-in |
| Custom / self-hosted | Full control, advanced tuning | Higher operational overhead and cost |
Recommendation: Start with the managed approach for most workloads and move to custom only when specific requirements demand it.
Validation and Versioning
- Last validated: April 2026
- Validate examples against your tenant, region, and SKU constraints before production rollout.
- Keep module, CLI, and SDK versions pinned in automation pipelines and review quarterly.
Security and Governance Considerations
- Apply least-privilege access using RBAC roles and just-in-time elevation for admin tasks.
- Store secrets in managed secret stores and avoid embedding credentials in scripts or source files.
- Enable audit logging, data protection policies, and periodic access reviews for regulated workloads.
Cost and Performance Notes
- Define budgets and alerts, then monitor usage and cost trends continuously after go-live.
- Baseline performance with synthetic and real-user checks before and after major changes.
- Scale resources with measured thresholds and revisit sizing after usage pattern changes.
Official Microsoft References
- https://learn.microsoft.com/azure/architecture/
- https://learn.microsoft.com/azure/well-architected/
- https://learn.microsoft.com/power-platform/guidance/
Public Examples from Official Sources
- These examples are sourced from official public Microsoft documentation and sample repositories.
- Documentation examples: https://learn.microsoft.com/azure/well-architected/
- Sample repositories: https://github.com/Azure/ArchitectureCenter
- Prefer adapting these examples to your tenant, subscriptions, and governance requirements before production use.
Key Takeaways
- DevSecOps requires security at every stage: code, dependencies, containers, infrastructure, and runtime
- GitHub Advanced Security provides CodeQL, secret scanning, Dependabot, and dependency review in one platform
- Azure DevOps orchestrates the security pipeline with gates that prevent insecure code from reaching production
- SBOM and SLSA provenance are essential for software supply chain security
- Container signing with admission controllers creates a trust chain from build to runtime