# security-scan.yml — PR-gate security scanning (SOC2/FedRAMP compliance).
#
# Runs two independent scan jobs on every PR and push to main:
#   1. trivy          — Container image scan for CRITICAL CVEs
#   2. class-coverage — Data-class registry coverage gate (defence-in-depth, LLD 18)
#
# PR gate: fails on actionable findings only.
#
# Scope history (#740, PRD #738):
#   * Phase 1 (#740) dropped the `govulncheck` and `gosec` jobs from the PR
#     gate. They ran as `continue-on-error: true` here (stdlib CVEs + the
#     G115/G118/G703 ADR-0010 baseline) which added ~2-3 billed minutes per
#     PR for signal that was either non-actionable or informational.
#     Both scanners now run on main HEAD via `.github/workflows/
#     daily-security-sweep.yml`, where a new finding (i.e. not in the ADR
#     baseline) files a dated P0/P1 issue via `scripts/daily-finding-filer.py`.
#
# Path filtering (see #728 P2):
#   A `changes` gate detects whether each job needs to run work. Both jobs
#   still execute (so their required check-runs report green — branch
#   protection depends on `Container Image Scan` and `Data-Class Registry
#   Coverage` reporting on every PR) but the heavyweight steps are gated
#   behind `if: needs.changes.outputs.<flag>` so doc-only / config-only
#   PRs finish in ~10s each.

name: Security Scan

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

permissions:
  contents: read
  # dorny/paths-filter needs pull-requests:read to compute the diff on PR
  # events. Without it, the `changes` job fails with "Resource not accessible
  # by integration" and all downstream jobs skip.
  pull-requests: read

jobs:
  changes:
    name: Detect relevant changes
    runs-on: ubuntu-latest
    outputs:
      go: ${{ steps.filter.outputs.go }}
      docker: ${{ steps.filter.outputs.docker }}
      migrations: ${{ steps.filter.outputs.migrations }}
      workflow: ${{ steps.filter.outputs.workflow }}
    steps:
      - uses: actions/checkout@v6
      - name: Filter changed paths
        id: filter
        uses: dorny/paths-filter@v3
        with:
          filters: |
            go:
              - '**/*.go'
              - 'go.mod'
              - 'go.sum'
            docker:
              - 'Dockerfile'
              - 'services/*/Dockerfile'
              - '.trivyignore'
              - 'deploy/**'
            migrations:
              - 'internal/context/store/migrations/**'
            workflow:
              - '.github/workflows/security-scan.yml'

  trivy:
    name: Container Image Scan
    runs-on: ubuntu-latest
    needs: changes
    steps:
      - name: Skip if no relevant changes
        if: needs.changes.outputs.go != 'true' && needs.changes.outputs.docker != 'true' && needs.changes.outputs.workflow != 'true'
        run: echo "No Go/Docker/workflow changes — skipping container scan."

      - name: Checkout
        if: needs.changes.outputs.go == 'true' || needs.changes.outputs.docker == 'true' || needs.changes.outputs.workflow == 'true'
        uses: actions/checkout@v6

      - name: Set up Docker Buildx
        if: needs.changes.outputs.go == 'true' || needs.changes.outputs.docker == 'true' || needs.changes.outputs.workflow == 'true'
        uses: docker/setup-buildx-action@v4

      - name: Build context-engine image for scanning
        if: needs.changes.outputs.go == 'true' || needs.changes.outputs.docker == 'true' || needs.changes.outputs.workflow == 'true'
        run: |
          docker build \
            -t upsquad/context-engine:scan \
            --build-arg SERVICE=context-engine \
            .

      - name: Run Trivy vulnerability scanner
        if: needs.changes.outputs.go == 'true' || needs.changes.outputs.docker == 'true' || needs.changes.outputs.workflow == 'true'
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: upsquad/context-engine:scan
          format: table
          exit-code: '1'
          severity: CRITICAL
          trivyignores: .trivyignore

  class-coverage:
    # Defence-in-depth for LLD 18 (#461): re-runs the data-class registry
    # coverage gate from the CLI so a broken Go-test (test/lint/data_class_coverage_test.go)
    # cannot silently bypass the invariant. Both gates are expected green;
    # both cover the same check — that every RLS-enabled migration row
    # maps to a classregistry.Scope entry.
    #
    # Follow-up from closed PR #469 / issue #470.
    name: Data-Class Registry Coverage
    runs-on: ubuntu-latest
    needs: changes
    steps:
      - name: Skip if no relevant changes
        if: needs.changes.outputs.go != 'true' && needs.changes.outputs.migrations != 'true' && needs.changes.outputs.workflow != 'true'
        run: echo "No Go/migrations/workflow changes — skipping class-coverage gate."

      - name: Checkout
        if: needs.changes.outputs.go == 'true' || needs.changes.outputs.migrations == 'true' || needs.changes.outputs.workflow == 'true'
        uses: actions/checkout@v6

      - name: Set up Go
        if: needs.changes.outputs.go == 'true' || needs.changes.outputs.migrations == 'true' || needs.changes.outputs.workflow == 'true'
        uses: actions/setup-go@v6
        with:
          go-version-file: go.mod
          cache: true

      - name: Run class-coverage gate (CLI)
        if: needs.changes.outputs.go == 'true' || needs.changes.outputs.migrations == 'true' || needs.changes.outputs.workflow == 'true'
        run: |
          go run ./cmd/schema-coverage-aggregator \
            -mode=class-coverage \
            -migrations-dir internal/context/store/migrations
