name: DPDPA Threat Model Gate

# Enforces the design-phase threat modeling gate from the DPDPA
# threat modeling methodology (article: /blog/dpdpa-threat-modeling-linddun-stride).
#
# What this gate checks on every PR:
#   1. Any PR adding a new DFD element (process, store, external entity, or
#      trust boundary) must include a corresponding threat-model.yaml update.
#   2. Every new element must have at least one LINDDUN and one STRIDE finding.
#   3. Every finding with present=true must have a non-empty mitigation string.
#   4. Every finding must reference at least one DPDPA section or Rule.
#   5. OPA policies are evaluated against any changed Terraform or K8s manifests.
#
# Legal anchor: DPDPA 2023 Section 8(5) "reasonable security safeguards" —
# the threat model is the primary evidence artefact for this standard.

on:
  pull_request:
    paths:
      - '**threat-model.yaml'
      - '**threat-model-*.yaml'
      - '**.tf'
      - '**.rego'
      - '**/k8s/**/*.yaml'
      - '**/kubernetes/**/*.yaml'

jobs:
  # ---------------------------------------------------------------------------
  # Job 1: Threat Model Schema Validation
  # Parses all threat-model.yaml files touched by the PR and validates them
  # against the DPDPA requirements.
  # ---------------------------------------------------------------------------
  validate-threat-model:
    name: Validate DPDPA Threat Model
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.11'

      - name: Install dependencies
        run: pip install pyyaml jsonschema

      - name: Detect changed threat-model files
        id: changed
        run: |
          git diff --name-only origin/${{ github.base_ref }}...HEAD \
            | grep -E 'threat-model.*\.yaml$' > changed_tm_files.txt || true
          echo "files=$(cat changed_tm_files.txt | tr '\n' ',')" >> $GITHUB_OUTPUT

      - name: Validate threat model YAML
        run: |
          python - <<'EOF'
          import yaml, sys, os, glob

          REQUIRED_LINDDUN = [
              "linkability", "identifiability", "non_repudiation",
              "detectability", "disclosure", "unawareness", "non_compliance",
          ]
          REQUIRED_STRIDE = [
              "spoofing", "tampering", "repudiation",
              "information_disclosure", "denial_of_service", "elevation_of_privilege",
          ]

          def validate_element(elem, errors):
              eid = elem.get("id", "<unknown>")

              for category in REQUIRED_LINDDUN:
                  entry = elem.get("linddun", {}).get(category)
                  if entry is None:
                      errors.append(f"{eid}: missing LINDDUN.{category}")
                      continue
                  if entry.get("present") and not entry.get("mitigation", "").strip():
                      errors.append(f"{eid}: LINDDUN.{category} present=true but mitigation is empty")
                  if entry.get("present") and not entry.get("dpdpa_sections"):
                      errors.append(f"{eid}: LINDDUN.{category} present=true but dpdpa_sections is empty")

              for category in REQUIRED_STRIDE:
                  entry = elem.get("stride", {}).get(category)
                  if entry is None:
                      errors.append(f"{eid}: missing STRIDE.{category}")
                      continue
                  if entry.get("present") and not entry.get("mitigation", "").strip():
                      errors.append(f"{eid}: STRIDE.{category} present=true but mitigation is empty")
                  if entry.get("present") and not entry.get("dpdpa_sections"):
                      errors.append(f"{eid}: STRIDE.{category} present=true but dpdpa_sections is empty")

          def validate_file(path, errors):
              with open(path) as f:
                  tm = yaml.safe_load(f)

              if not tm:
                  errors.append(f"{path}: empty or invalid YAML")
                  return

              elements = tm.get("dfd_elements", [])
              if not elements:
                  errors.append(f"{path}: dfd_elements is empty — add at least one DFD element")
                  return

              for elem in elements:
                  if not elem.get("id"):
                      continue   # skip blank template entries
                  validate_element(elem, errors)

          errors = []
          # Use the changed-file list written by the Detect step when running on a PR.
          # Fall back to glob for manual / workflow_dispatch runs where the list is absent.
          if os.path.exists("changed_tm_files.txt") and os.path.getsize("changed_tm_files.txt") > 0:
              with open("changed_tm_files.txt") as fl:
                  files = [f.strip() for f in fl if f.strip()]
          else:
              files = glob.glob("**/threat-model*.yaml", recursive=True)
          if not files:
              print("No threat-model files found — nothing to validate.")
              sys.exit(0)

          for f in files:
              print(f"Validating: {f}")
              validate_file(f, errors)

          if errors:
              print("\n❌ DPDPA Threat Model Validation FAILED")
              print("=" * 60)
              for e in errors:
                  print(f"  • {e}")
              print()
              print("Every finding with present=true requires:")
              print("  - A non-empty mitigation string")
              print("  - At least one dpdpa_sections entry (e.g. S.8(5), Rule 6(1)(a))")
              print()
              print("Legal anchor: DPDPA 2023 Section 8(5) — 'reasonable security safeguards'")
              sys.exit(1)
          else:
              print(f"\n✅ All {len(files)} threat model file(s) pass DPDPA validation.")
          EOF

  # ---------------------------------------------------------------------------
  # Job 2: OPA Policy Evaluation against IaC
  # Runs the DPDPA OPA policy bundle against any changed Terraform plans
  # or Kubernetes manifests.
  # ---------------------------------------------------------------------------
  opa-iac-policy:
    name: OPA — DPDPA IaC Policies
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Install OPA and Conftest
        run: |
          curl -sSL -o /usr/local/bin/opa \
            https://openpolicyagent.org/downloads/latest/opa_linux_amd64_static
          chmod +x /usr/local/bin/opa

          curl -sSL -o conftest.tar.gz \
            https://github.com/open-policy-agent/conftest/releases/latest/download/conftest_Linux_x86_64.tar.gz
          tar xzf conftest.tar.gz && mv conftest /usr/local/bin/

      - name: Run DPDPA encryption policy against Terraform files
        run: |
          # Evaluate encryption-at-rest policy against any .tf files in the PR
          find . -name "*.tf" -not -path "./.terraform/*" | while read tf; do
            echo "Checking $tf..."
            conftest test "$tf" \
              --policy policies/dpdpa_purpose_scope.rego \
              --namespace dpdpa \
              --output table || exit 1
          done

      - name: Run DPDPA policies against Kubernetes manifests
        run: |
          find . \( -path "*/k8s/*.yaml" -o -path "*/kubernetes/*.yaml" \) | while read manifest; do
            echo "Checking $manifest..."
            conftest test "$manifest" \
              --policy policies/dpdpa_purpose_scope.rego \
              --namespace dpdpa \
              --output table || exit 1
          done

  # ---------------------------------------------------------------------------
  # Job 3: DPIA Coverage Check (SDF-only)
  # If is_significant_data_fiduciary: true, verify a DPIA document exists
  # and was updated in this PR if the threat model changed.
  # ---------------------------------------------------------------------------
  dpia-coverage:
    name: DPIA Coverage (SDF)
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Check DPIA document presence for SDFs
        run: |
          python - <<'EOF'
          import yaml, glob, sys

          errors = []
          for tm_path in glob.glob("**/threat-model*.yaml", recursive=True):
              with open(tm_path) as f:
                  tm = yaml.safe_load(f)
              if not tm:
                  continue
              if tm.get("system_info", {}).get("is_significant_data_fiduciary"):
                  import os
                  dpia_path = os.path.join(os.path.dirname(tm_path), "dpia-template.md")
                  if not os.path.exists(dpia_path):
                      errors.append(
                          f"{tm_path}: is_significant_data_fiduciary=true "
                          f"but no dpia-template.md found alongside it. "
                          f"DPDPA S.10(2)(c)(i) requires a DPIA for SDFs."
                      )

          if errors:
              print("\n❌ DPIA coverage check FAILED")
              for e in errors:
                  print(f"  • {e}")
              sys.exit(1)
          else:
              print("✅ DPIA coverage check passed.")
          EOF

  # ---------------------------------------------------------------------------
  # Job 4: Summary Comment on PR
  # Posts a summary of findings as a PR comment.
  # ---------------------------------------------------------------------------
  post-summary:
    name: Post Gate Summary
    runs-on: ubuntu-latest
    needs: [validate-threat-model, opa-iac-policy, dpia-coverage]
    if: always()
    steps:
      - name: Post result summary
        uses: actions/github-script@v7
        with:
          script: |
            const tm   = '${{ needs.validate-threat-model.result }}';
            const opa  = '${{ needs.opa-iac-policy.result }}';
            const dpia = '${{ needs.dpia-coverage.result }}';

            const icon = r => r === 'success' ? '✅' : r === 'skipped' ? '⏭️' : '❌';

            const body = [
              '## DPDPA Threat Model Gate Results',
              '',
              `| Check | Result |`,
              `|-------|--------|`,
              `| Threat model YAML validation | ${icon(tm)} ${tm} |`,
              `| OPA IaC policy evaluation | ${icon(opa)} ${opa} |`,
              `| DPIA coverage (SDF check) | ${icon(dpia)} ${dpia} |`,
              '',
              '**Legal anchor:** DPDPA 2023 Section 8(5) (reasonable safeguards) · Section 10(2)(c)(i) (SDF DPIA)',
              '',
              '_Results generated by [DPDPA Threat Model Gate](https://statechecksecurity.com/blog/dpdpa-threat-modeling-linddun-stride)_',
            ].join('\n');

            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body,
            });
