# Privileged access evidence for the ISSO: a sample workflow for a GCC tenant with commercial Azure.
#
# Illustrative. This file is NOT active in the modernization-workbook repository: it lives under
# examples/, not .github/workflows/. Copy it into the estate's own repository, review every line,
# and run it there first against a test tenant (developer workbook Building Privileged Access,
# Chapter 08, Lab 2). It is checked offline by python/workflow_lint.py and tests/test_pa_evidence.py;
# it has not been run against Azure.
#
# What it does, once a day and on demand:
#   1. signs in with a federated credential (OIDC), as drift-pipeline.yml does: no secret anywhere;
#   2. runs examples/privileged-access/Invoke-PaEvidenceRun.ps1 read-only: Chapter 01's inventory,
#      Chapter 02's emergency access check, and the PIM audit evidence for the last 24 hours;
#   3. fetches the previous package, verifies it, and compares the two runs by fingerprint;
#   4. packages the run with a SHA-256 per file, chained to the previous package, and writes it to
#      an immutable container;
#   5. in a second job with no Azure access, opens issues without names: Tier 0 drift as incidents;
#   6. on the first day of a month, assembles the previous month's packages for the ISSO (A4.3).
#
# GCC uses the worldwide endpoints (login.microsoftonline.com, graph.microsoft.com,
# management.azure.com). GCC High and DoD use separate hosts; `python
# examples/drift-pipeline/python/drift_pipeline.py cloud --name <cloud>` prints them.
#
# Repository settings it expects (identifiers, not credentials):
#   secrets AZURE_CLIENT_ID, AZURE_TENANT_ID, AZURE_SUBSCRIPTION_ID
#   variables EVIDENCE_STORAGE_ACCOUNT, AZ_PS_VERSION, GRAPH_PS_VERSION (exact versions, never "latest"),
#     PA_AZURE_SCOPES (space-separated scopes the inventory reads)
#   an environment named pa-evidence-gcc, deployable only from the default branch; the app's
#   federated credential subject is repo:<owner>/<repo>:environment:pa-evidence-gcc
#   inputs/ in the repository: emergency-access.json, emergency-validations.csv, exceptions.csv

name: Privileged access evidence (GCC)

on:
  schedule:
    - cron: "23 6 * * *"        # 06:23 UTC daily
  workflow_dispatch:

permissions: {}

concurrency:
  group: pa-evidence-gcc
  cancel-in-progress: false

env:
  CLOUD: gcc
  GRAPH_HOST: https://graph.microsoft.com
  MG_ENVIRONMENT: Global
  EVIDENCE_ACCOUNT: ${{ vars.EVIDENCE_STORAGE_ACCOUNT }}
  EVIDENCE_CONTAINER: pa-evidence

jobs:
  evidence:
    runs-on: ubuntu-latest
    environment: pa-evidence-gcc
    timeout-minutes: 60
    permissions:
      contents: read
      id-token: write
    steps:
      - name: Check out
        uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
        with:
          persist-credentials: false

      - name: Sign in with the federated credential
        uses: azure/login@a641126d1b8aa4d1fa005f4f92df94a3a4c4c906 # v3.1.0
        with:
          client-id: ${{ secrets.AZURE_CLIENT_ID }}
          tenant-id: ${{ secrets.AZURE_TENANT_ID }}
          subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
          environment: AzureCloud
          audience: api://AzureADTokenExchange
          enable-AzPSSession: true

      - name: Fetch and verify the previous evidence package
        run: |
          mkdir -p previous
          prev=$(az storage blob list --account-name "$EVIDENCE_ACCOUNT" --container-name "$EVIDENCE_CONTAINER" \
                   --auth-mode login --prefix runs/ --num-results "*" \
                   --query "[?ends_with(name, '/EVIDENCE-MANIFEST.json')].name" -o tsv | sort | tail -n 1)
          if [ -n "$prev" ]; then
            prev_id=$(basename "$(dirname "$prev")")
            az storage blob download-batch --account-name "$EVIDENCE_ACCOUNT" --source "$EVIDENCE_CONTAINER" \
              --auth-mode login --pattern "runs/$prev_id/*" --destination previous
            python examples/drift-pipeline/python/pa_evidence.py verify "previous/runs/$prev_id"
            echo "PREV_ID=$prev_id" >> "$GITHUB_ENV"
            # The new window starts where the last one ended, so no hour goes unread.
            since=$(jq -r .audit_window.until "previous/runs/$prev_id/EVIDENCE-MANIFEST.json")
            echo "PA_SINCE=$since" >> "$GITHUB_ENV"
          fi

      - name: Run the read-only evidence run
        uses: azure/powershell@f5b8adcfff1904872c7b98d4012d4914d74b1a82 # v3.0.0
        with:
          azPSVersion: ${{ vars.AZ_PS_VERSION }}
          inlineScript: |
            Install-Module Microsoft.Graph.Authentication -RequiredVersion $env:GRAPH_PS_VERSION -Scope CurrentUser -Force
            $token = Get-AzAccessToken -ResourceUrl $env:GRAPH_HOST -AsSecureString
            Connect-MgGraph -AccessToken $token.Token -Environment $env:MG_ENVIRONMENT -NoWelcome
            $a = @{ Cloud = $env:CLOUD; TenantId = $env:AZURE_TENANT_ID; OutputPath = 'runs'
                    EmergencyConfigPath = 'inputs/emergency-access.json'; ValidationsPath = 'inputs/emergency-validations.csv'
                    ExceptionsPath = 'inputs/exceptions.csv'; AzureScope = @($env:PA_AZURE_SCOPES -split ' ') }
            if ($env:PA_SINCE) { $a.Since = [datetime]::Parse($env:PA_SINCE, [Globalization.CultureInfo]::InvariantCulture, [Globalization.DateTimeStyles]::AdjustToUniversal) }
            ./examples/privileged-access/Invoke-PaEvidenceRun.ps1 @a
        env:
          GRAPH_PS_VERSION: ${{ vars.GRAPH_PS_VERSION }}
          AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
          PA_AZURE_SCOPES: ${{ vars.PA_AZURE_SCOPES }}

      - name: Compare with the previous run
        run: |
          run_id=$(ls runs | sort | tail -n 1)
          echo "RUN_ID=$run_id" >> "$GITHUB_ENV"
          set +e
          if [ -n "${PREV_ID:-}" ]; then
            python examples/drift-pipeline/python/pa_evidence.py compare \
              --previous "previous/runs/$PREV_ID/run" --current "runs/$run_id" --out drift
          else
            python examples/drift-pipeline/python/pa_evidence.py compare --current "runs/$run_id" --out drift
          fi
          rc=$?
          set -e
          # 0 no drift, 2 drift: both go on to the evidence. 1 is a refusal and stops the run.
          [ "$rc" -eq 0 ] || [ "$rc" -eq 2 ]

      - name: Package and write the evidence
        run: |
          prev_args=()
          if [ -n "${PREV_ID:-}" ]; then prev_args=(--previous-manifest "previous/runs/$PREV_ID/EVIDENCE-MANIFEST.json"); fi
          python examples/drift-pipeline/python/pa_evidence.py package --run "runs/$RUN_ID" \
            --diff drift/pa-drift-report.json "${prev_args[@]}" --meta-from-env --out evidence
          python examples/drift-pipeline/python/pa_evidence.py verify "evidence/$RUN_ID"
          # No --overwrite: a package is written once; the locked retention policy keeps it.
          az storage blob upload-batch --account-name "$EVIDENCE_ACCOUNT" --destination "$EVIDENCE_CONTAINER" \
            --auth-mode login --source evidence --destination-path runs

      - name: Assemble last month's evidence for the ISSO
        run: |
          if [ "$(date -u +%d)" != "01" ]; then exit 0; fi
          since=$(date -u -d "$(date -u +%Y-%m-01) -1 month" +%Y-%m-%dT00:00:00Z)
          until=$(date -u +%Y-%m-01T00:00:00Z)
          az storage blob download-batch --account-name "$EVIDENCE_ACCOUNT" --source "$EVIDENCE_CONTAINER" \
            --auth-mode login --pattern "runs/*" --destination month
          set +e
          python examples/drift-pipeline/python/pa_evidence.py period --evidence month/runs --since "$since" --until "$until" --out periods
          rc=$?
          set -e
          [ "$rc" -eq 0 ] || [ "$rc" -eq 2 ]
          az storage blob upload-batch --account-name "$EVIDENCE_ACCOUNT" --destination "$EVIDENCE_CONTAINER" \
            --auth-mode login --source periods --destination-path periods

      - name: Hand the drift report to the findings job
        uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
        with:
          name: pa-drift-report
          path: drift/pa-drift-report.json
          retention-days: 7

  findings:
    needs: evidence
    runs-on: ubuntu-latest
    timeout-minutes: 15
    permissions:
      contents: read
      issues: write
    steps:
      - name: Check out
        uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
        with:
          persist-credentials: false

      - name: Get the drift report
        uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
        with:
          name: pa-drift-report
          path: drift

      - name: Open and close findings, with object IDs and no names
        env:
          GH_TOKEN: ${{ github.token }}
        run: |
          python examples/drift-pipeline/python/pa_evidence.py issues --diff drift/pa-drift-report.json --out drift/issues.json
          jq -c '.[]' drift/issues.json | while read -r item; do
            action=$(jq -r .action <<<"$item"); fp=$(jq -r .fingerprint <<<"$item")
            existing=$(gh issue list --state open --label drift --search "$fp in:body" --json number --jq '.[0].number')
            if [ "$action" = "comment" ] && [ -n "$existing" ]; then
              gh issue comment "$existing" --body "$(jq -r .body <<<"$item")"
            elif [ "$action" != "close" ] && [ -z "$existing" ]; then
              gh issue create --title "$(jq -r .title <<<"$item")" --body "$(jq -r .body <<<"$item")" \
                $(jq -r '.labels[] | "--label " + .' <<<"$item")
            elif [ "$action" = "close" ] && [ -n "$existing" ]; then
              gh issue close "$existing" --comment "$(jq -r .body <<<"$item")"
            fi
          done
