# Security Scan for Pull Requests
# Runs Trivy security scanning on PR Docker images after the build workflow completes
# This workflow extracts the charon binary from the container and performs filesystem scanning
name: Security Scan (PR)

on:
  workflow_run:
    workflows: ["Docker Build, Publish & Test"]
    types: [completed]
  workflow_dispatch:
    inputs:
      pr_number:
        description: 'PR number to scan'
        required: true
        type: string
  pull_request:
  push:
    branches: [main]


concurrency:
  group: security-pr-${{ github.event_name == 'workflow_run' && github.event.workflow_run.event || github.event_name }}-${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_branch || github.ref }}
  cancel-in-progress: true

jobs:
  security-scan:
    name: Trivy Binary Scan
    runs-on: ubuntu-latest
    timeout-minutes: 10
    # Run for manual dispatch, direct PR/push, or successful upstream workflow_run
    if: >-
      github.event_name == 'workflow_dispatch' ||
      github.event_name == 'pull_request' ||
      github.event_name == 'push' ||
      (github.event_name == 'workflow_run' &&
       github.event.workflow_run.event == 'pull_request' &&
       github.event.workflow_run.status == 'completed' &&
       github.event.workflow_run.conclusion == 'success')

    permissions:
      contents: read
      security-events: write
      actions: read

    steps:
      - name: Checkout repository
        # actions/checkout v4.2.2
        uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98
        with:
          ref: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_sha || github.sha }}

      - name: Extract PR number from workflow_run
        id: pr-info
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          if [[ "${{ github.event_name }}" == "push" ]]; then
            echo "pr_number=" >> "$GITHUB_OUTPUT"
            echo "is_push=true" >> "$GITHUB_OUTPUT"
            echo "✅ Push event detected; using local image path"
            exit 0
          fi

          if [[ "${{ github.event_name }}" == "pull_request" ]]; then
            echo "pr_number=${{ github.event.pull_request.number }}" >> "$GITHUB_OUTPUT"
            echo "is_push=false" >> "$GITHUB_OUTPUT"
            echo "✅ Pull request event detected: PR #${{ github.event.pull_request.number }}"
            exit 0
          fi

          if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
            INPUT_PR_NUMBER="${{ inputs.pr_number }}"
            if [[ -z "${INPUT_PR_NUMBER}" ]]; then
              echo "❌ workflow_dispatch requires inputs.pr_number"
              exit 1
            fi

            if [[ ! "${INPUT_PR_NUMBER}" =~ ^[0-9]+$ ]]; then
              echo "❌ reason_category=invalid_input"
              echo "reason=workflow_dispatch pr_number must be digits-only"
              exit 1
            fi

            PR_NUMBER="${INPUT_PR_NUMBER}"
            echo "pr_number=${PR_NUMBER}" >> "$GITHUB_OUTPUT"
            echo "is_push=false" >> "$GITHUB_OUTPUT"
            echo "✅ Using manually provided PR number: ${PR_NUMBER}"
            exit 0
          fi

          if [[ "${{ github.event_name }}" == "workflow_run" ]]; then
            if [[ "${{ github.event.workflow_run.event }}" != "pull_request" ]]; then
              # Explicit contract validation happens in the dedicated guard step.
              echo "pr_number=" >> "$GITHUB_OUTPUT"
              echo "is_push=false" >> "$GITHUB_OUTPUT"
              exit 0
            fi

            if [[ -n "${{ github.event.workflow_run.pull_requests[0].number || '' }}" ]]; then
              echo "pr_number=${{ github.event.workflow_run.pull_requests[0].number }}" >> "$GITHUB_OUTPUT"
              echo "is_push=false" >> "$GITHUB_OUTPUT"
              echo "✅ Found PR number from workflow_run payload: ${{ github.event.workflow_run.pull_requests[0].number }}"
              exit 0
            fi
          fi

          # Extract PR number from context
          HEAD_SHA="${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_sha || github.event.pull_request.head.sha || github.sha }}"
          echo "🔍 Looking for PR with head SHA: ${HEAD_SHA}"

          # Query GitHub API for PR associated with this commit
          PR_NUMBER=$(gh api \
            -H "Accept: application/vnd.github+json" \
            -H "X-GitHub-Api-Version: 2022-11-28" \
            "/repos/${{ github.repository }}/commits/${HEAD_SHA}/pulls" \
            --jq '.[0].number // empty' 2>/dev/null || echo "")

          if [[ -n "${PR_NUMBER}" ]]; then
            echo "pr_number=${PR_NUMBER}" >> "$GITHUB_OUTPUT"
            echo "is_push=false" >> "$GITHUB_OUTPUT"
            echo "✅ Found PR number: ${PR_NUMBER}"
          else
            echo "❌ Could not determine PR number for workflow_run SHA: ${HEAD_SHA}"
            exit 1
          fi

      - name: Validate workflow_run trust boundary and event contract
        if: github.event_name == 'workflow_run'
        run: |
          if [[ "${{ github.event.workflow_run.name }}" != "Docker Build, Publish & Test" ]]; then
            echo "❌ reason_category=unexpected_upstream_workflow"
            echo "workflow_name=${{ github.event.workflow_run.name }}"
            exit 1
          fi

          if [[ "${{ github.event.workflow_run.event }}" != "pull_request" ]]; then
            echo "❌ reason_category=unsupported_upstream_event"
            echo "upstream_event=${{ github.event.workflow_run.event }}"
            echo "run_id=${{ github.event.workflow_run.id }}"
            exit 1
          fi

          if [[ "${{ github.event.workflow_run.head_repository.full_name }}" != "${{ github.repository }}" ]]; then
            echo "❌ reason_category=untrusted_upstream_repository"
            echo "upstream_head_repository=${{ github.event.workflow_run.head_repository.full_name }}"
            echo "expected_repository=${{ github.repository }}"
            exit 1
          fi

          echo "✅ workflow_run trust boundary and event contract validated"

      - name: Build Docker image (Local)
        if: github.event_name == 'push' || github.event_name == 'pull_request'
        run: |
          echo "Building image locally for security scan..."
          docker build -t charon:local .
          echo "✅ Successfully built charon:local"

      - name: Check for PR image artifact
        id: check-artifact
        if: github.event_name == 'workflow_run' || github.event_name == 'workflow_dispatch'
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          PR_NUMBER="${{ steps.pr-info.outputs.pr_number }}"
          if [[ ! "${PR_NUMBER}" =~ ^[0-9]+$ ]]; then
            echo "❌ reason_category=invalid_input"
            echo "reason=Resolved PR number must be digits-only"
            exit 1
          fi

          ARTIFACT_NAME="pr-image-${PR_NUMBER}"
          RUN_ID="${{ github.event_name == 'workflow_run' && github.event.workflow_run.id || '' }}"

          echo "🔍 Checking for artifact: ${ARTIFACT_NAME}"

          if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
            # Manual replay path: find latest successful docker-build pull_request run for this PR.
            RUNS_JSON=$(gh api \
              -H "Accept: application/vnd.github+json" \
              -H "X-GitHub-Api-Version: 2022-11-28" \
              "/repos/${{ github.repository }}/actions/workflows/docker-build.yml/runs?event=pull_request&status=success&per_page=100" 2>&1)
            RUNS_STATUS=$?

            if [[ ${RUNS_STATUS} -ne 0 ]]; then
              echo "❌ reason_category=api_error"
              echo "reason=Failed to query workflow runs for PR lookup"
              echo "upstream_run_id=unknown"
              echo "artifact_name=${ARTIFACT_NAME}"
              echo "api_output=${RUNS_JSON}"
              exit 1
            fi

            RUN_ID=$(printf '%s' "${RUNS_JSON}" | jq -r --argjson pr "${PR_NUMBER}" '.workflow_runs[] | select((.pull_requests // []) | any(.number == $pr)) | .id' | head -n 1)

            if [[ -z "${RUN_ID}" ]]; then
              echo "❌ reason_category=not_found"
              echo "reason=No successful docker-build pull_request run found for PR #${PR_NUMBER}"
              echo "upstream_run_id=unknown"
              echo "artifact_name=${ARTIFACT_NAME}"
              exit 1
            fi
          fi

          echo "run_id=${RUN_ID}" >> "$GITHUB_OUTPUT"

          # Check if the artifact exists in the workflow run
          ARTIFACTS_JSON=$(gh api \
            -H "Accept: application/vnd.github+json" \
            -H "X-GitHub-Api-Version: 2022-11-28" \
            "/repos/${{ github.repository }}/actions/runs/${RUN_ID}/artifacts" 2>&1)
          ARTIFACTS_STATUS=$?

          if [[ ${ARTIFACTS_STATUS} -ne 0 ]]; then
            echo "❌ reason_category=api_error"
            echo "reason=Failed to query artifacts for upstream run"
            echo "upstream_run_id=${RUN_ID}"
            echo "artifact_name=${ARTIFACT_NAME}"
            echo "api_output=${ARTIFACTS_JSON}"
            exit 1
          fi

          ARTIFACT_ID=$(printf '%s' "${ARTIFACTS_JSON}" | jq -r --arg name "${ARTIFACT_NAME}" '.artifacts[] | select(.name == $name) | .id' | head -n 1)

          if [[ -z "${ARTIFACT_ID}" ]]; then
            echo "❌ reason_category=not_found"
            echo "reason=Required artifact was not found"
            echo "upstream_run_id=${RUN_ID}"
            echo "artifact_name=${ARTIFACT_NAME}"
            exit 1
          fi

          {
            echo "artifact_exists=true"
            echo "artifact_id=${ARTIFACT_ID}"
            echo "artifact_name=${ARTIFACT_NAME}"
          } >> "$GITHUB_OUTPUT"
          echo "✅ Found artifact: ${ARTIFACT_NAME} (ID: ${ARTIFACT_ID})"

      - name: Download PR image artifact
        if: github.event_name == 'workflow_run' || github.event_name == 'workflow_dispatch'
        # actions/download-artifact v4.1.8
        uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3
        with:
          name: ${{ steps.check-artifact.outputs.artifact_name }}
          run-id: ${{ steps.check-artifact.outputs.run_id }}
          github-token: ${{ secrets.GITHUB_TOKEN }}

      - name: Load Docker image
        if: github.event_name == 'workflow_run' || github.event_name == 'workflow_dispatch'
        id: load-image
        run: |
          echo "📦 Loading Docker image..."

          if [[ ! -r "charon-pr-image.tar" ]]; then
            echo "❌ ERROR: Artifact image tar is missing or unreadable"
            exit 1
          fi

          MANIFEST_TAGS=""
          if tar -tf charon-pr-image.tar | grep -qx "manifest.json"; then
            MANIFEST_TAGS=$(tar -xOf charon-pr-image.tar manifest.json 2>/dev/null | jq -r '.[]?.RepoTags[]?' 2>/dev/null | sed '/^$/d' || true)
          else
            echo "⚠️ manifest.json not found in artifact tar; will try docker-load-image-id fallback"
          fi

          LOAD_OUTPUT=$(docker load < charon-pr-image.tar 2>&1)
          echo "${LOAD_OUTPUT}"

          SOURCE_IMAGE_REF=""
          SOURCE_RESOLUTION_MODE=""

          while IFS= read -r tag; do
            [[ -z "${tag}" ]] && continue
            if docker image inspect "${tag}" >/dev/null 2>&1; then
              SOURCE_IMAGE_REF="${tag}"
              SOURCE_RESOLUTION_MODE="manifest_tag"
              break
            fi
          done <<< "${MANIFEST_TAGS}"

          if [[ -z "${SOURCE_IMAGE_REF}" ]]; then
            LOAD_IMAGE_ID=$(printf '%s\n' "${LOAD_OUTPUT}" | sed -nE 's/^Loaded image ID: (sha256:[0-9a-f]+)$/\1/p' | head -n1)
            if [[ -n "${LOAD_IMAGE_ID}" ]] && docker image inspect "${LOAD_IMAGE_ID}" >/dev/null 2>&1; then
              SOURCE_IMAGE_REF="${LOAD_IMAGE_ID}"
              SOURCE_RESOLUTION_MODE="load_image_id"
            fi
          fi

          if [[ -z "${SOURCE_IMAGE_REF}" ]]; then
            echo "❌ ERROR: Could not resolve a valid image reference from manifest tags or docker load image ID"
            exit 1
          fi

          docker tag "${SOURCE_IMAGE_REF}" "charon:artifact"

          {
            echo "source_image_ref=${SOURCE_IMAGE_REF}"
            echo "source_resolution_mode=${SOURCE_RESOLUTION_MODE}"
            echo "image_ref=charon:artifact"
          } >> "$GITHUB_OUTPUT"

          echo "✅ Docker image resolved via ${SOURCE_RESOLUTION_MODE} and tagged as charon:artifact"
          docker images | grep charon

      - name: Extract charon binary from container
        if: steps.check-artifact.outputs.artifact_exists == 'true' || github.event_name == 'push' || github.event_name == 'pull_request'
        id: extract
        run: |
          # Use local image for Push/PR events
          if [[ "${{ github.event_name }}" == "push" || "${{ github.event_name }}" == "pull_request" ]]; then
            echo "Using local image: charon:local"
            CONTAINER_ID=$(docker create "charon:local")
            echo "container_id=${CONTAINER_ID}" >> "$GITHUB_OUTPUT"

            # Extract the charon binary
            mkdir -p ./scan-target
            docker cp "${CONTAINER_ID}:/app/charon" ./scan-target/charon
            docker rm "${CONTAINER_ID}"

            if [[ -f "./scan-target/charon" ]]; then
              echo "✅ Binary extracted successfully"
              ls -lh ./scan-target/charon
              echo "binary_path=./scan-target" >> "$GITHUB_OUTPUT"
            else
              echo "❌ Failed to extract binary"
              exit 1
            fi
            exit 0
          fi

          # For workflow_run artifact path, always use locally tagged image from loaded artifact.
          IMAGE_REF="${{ steps.load-image.outputs.image_ref }}"
          if [[ -z "${IMAGE_REF}" ]]; then
            echo "❌ ERROR: Loaded artifact image reference is empty"
            exit 1
          fi

          echo "🔍 Extracting binary from: ${IMAGE_REF}"

          # Create container without starting it
          CONTAINER_ID=$(docker create "${IMAGE_REF}")
          echo "container_id=${CONTAINER_ID}" >> "$GITHUB_OUTPUT"

          # Extract the charon binary
          mkdir -p ./scan-target
          docker cp "${CONTAINER_ID}:/app/charon" ./scan-target/charon

          # Cleanup container
          docker rm "${CONTAINER_ID}"

          # Verify extraction
          if [[ -f "./scan-target/charon" ]]; then
            echo "✅ Binary extracted successfully"
            ls -lh ./scan-target/charon
            echo "binary_path=./scan-target" >> "$GITHUB_OUTPUT"
          else
            echo "❌ Failed to extract binary"
            exit 1
          fi

      - name: Run Trivy filesystem scan (SARIF output)
        if: steps.check-artifact.outputs.artifact_exists == 'true' || github.event_name == 'push' || github.event_name == 'pull_request'
        # aquasecurity/trivy-action v0.33.1
        uses: aquasecurity/trivy-action@4c61e6329bab9be735ca35291551614bc663dff3
        with:
          scan-type: 'fs'
          scan-ref: ${{ steps.extract.outputs.binary_path }}
          format: 'sarif'
          output: 'trivy-binary-results.sarif'
          severity: 'CRITICAL,HIGH,MEDIUM'
        continue-on-error: true

      - name: Check Trivy SARIF output exists
        if: always() && (steps.check-artifact.outputs.artifact_exists == 'true' || github.event_name == 'push' || github.event_name == 'pull_request')
        id: trivy-sarif-check
        run: |
          if [[ -f trivy-binary-results.sarif ]]; then
            echo "exists=true" >> "$GITHUB_OUTPUT"
          else
            echo "exists=false" >> "$GITHUB_OUTPUT"
            echo "ℹ️ No Trivy SARIF output found; skipping SARIF/artifact upload steps"
          fi

      - name: Upload Trivy SARIF to GitHub Security
        if: always() && steps.trivy-sarif-check.outputs.exists == 'true'
        # github/codeql-action v4
        uses: github/codeql-action/upload-sarif@0ec47d036c68ae0cf94c629009b1029407111281
        with:
          sarif_file: 'trivy-binary-results.sarif'
          category: ${{ steps.pr-info.outputs.is_push == 'true' && format('security-scan-{0}', github.event_name == 'workflow_run' && github.event.workflow_run.head_branch || github.ref_name) || format('security-scan-pr-{0}', steps.pr-info.outputs.pr_number) }}
        continue-on-error: true

      - name: Run Trivy filesystem scan (fail on CRITICAL/HIGH)
        if: steps.check-artifact.outputs.artifact_exists == 'true' || github.event_name == 'push' || github.event_name == 'pull_request'
        # aquasecurity/trivy-action v0.33.1
        uses: aquasecurity/trivy-action@4c61e6329bab9be735ca35291551614bc663dff3
        with:
          scan-type: 'fs'
          scan-ref: ${{ steps.extract.outputs.binary_path }}
          format: 'table'
          severity: 'CRITICAL,HIGH'
          exit-code: '1'

      - name: Upload scan artifacts
        if: always() && steps.trivy-sarif-check.outputs.exists == 'true'
        # actions/upload-artifact v4.4.3
        uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f
        with:
          name: ${{ steps.pr-info.outputs.is_push == 'true' && format('security-scan-{0}', github.event_name == 'workflow_run' && github.event.workflow_run.head_branch || github.ref_name) || format('security-scan-pr-{0}', steps.pr-info.outputs.pr_number) }}
          path: |
            trivy-binary-results.sarif
          retention-days: 14

      - name: Create job summary
        if: always() && (steps.check-artifact.outputs.artifact_exists == 'true' || github.event_name == 'push' || github.event_name == 'pull_request')
        run: |
          {
            if [[ "${{ steps.pr-info.outputs.is_push }}" == "true" ]]; then
              echo "## 🔒 Security Scan Results - Branch: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_branch || github.ref_name }}"
            else
              echo "## 🔒 Security Scan Results - PR #${{ steps.pr-info.outputs.pr_number }}"
            fi
            echo ""
            echo "**Scan Type**: Trivy Filesystem Scan"
            echo "**Target**: \`/app/charon\` binary"
            echo "**Severity Filter**: CRITICAL, HIGH"
            echo ""
            if [[ "${{ job.status }}" == "success" ]]; then
              echo "✅ **PASSED**: No CRITICAL or HIGH vulnerabilities found"
            else
              echo "❌ **FAILED**: CRITICAL or HIGH vulnerabilities detected"
              echo ""
              echo "Please review the Trivy scan output and address the vulnerabilities."
            fi
          } >> "$GITHUB_STEP_SUMMARY"

      - name: Cleanup
        if: always() && steps.check-artifact.outputs.artifact_exists == 'true'
        run: |
          echo "🧹 Cleaning up..."
          rm -rf ./scan-target
          echo "✅ Cleanup complete"
