Deep Dive

Enterprise DevSecOps Platform: GitHub Advanced Security + Azure DevOps + Container Security

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.

DevSecOps Platform Overview

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'

Security Pipeline

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);
    }
}

Security Dashboard

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

  1. Shift left pragmatically: Developers need findings in their IDE/PR, not in a separate security tool
  2. SBOM everything: Generate and attach SBOMs to every artifact — it's becoming a regulatory requirement
  3. Sign container images: Use Notation/Notary v2 with admission controllers to prevent unsigned images
  4. Secret scanning with push protection: Block commits containing secrets before they reach the repository
  5. Automate dependency updates: Dependabot + auto-merge for patch updates, manual review for major
  6. IaC scanning in PR: Catch Bicep/Terraform misconfigurations before they deploy
  7. 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

Public Examples from Official Sources

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

Further Reading

AI Assistant
AI Assistant

Article Assistant

Ask me about this article

AI
Hi! I'm here to help you understand this article. Ask me anything about the content, concepts, or implementation details.