# Security scanning workflow
#
# Runs security checks on dependencies and code.
# Triggered on PRs and scheduled weekly.

name: Security

on:
  pull_request:
    branches: [main]
  schedule:
    # Run weekly on Monday at 9:00 UTC
    - cron: "0 9 * * 1"
  workflow_dispatch:

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

# Read by default; the CodeQL job needs `security-events: write` to upload its
# SARIF and declares that itself, which REPLACES this default for that job.
permissions:
  contents: read

jobs:
  dependency-audit:
    name: Dependency Audit
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7

      - name: Set up Python
        uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7
        with:
          python-version: "3.11"

      - name: Install dependencies
        run: |
          # Upgrade the ambient build tooling too: the runner ships an older
          # setuptools/wheel that pip-audit flags (e.g. PYSEC-2026-3447), even
          # though the project builds with hatchling and does not depend on it.
          # `safety` is deliberately not installed here. pip-audit audits the
          # ENVIRONMENT, so anything installed beside the project is audited as
          # if the project depended on it: safety pulls in nltk, and
          # PYSEC-2026-3740 (nltk, no fixed version) failed this job on every
          # PR while nltk appears in neither pyproject.toml nor uv.lock. The
          # safety step itself was `continue-on-error` and needs an API key it
          # does not have, so it could never fail the build or report anything
          # pip-audit does not.
          python -m pip install --upgrade pip setuptools wheel
          pip install pip-audit

      - name: Run pip-audit
        run: |
          pip install -e .
          pip-audit --skip-editable --desc on

  codeql:
    name: CodeQL Analysis
    runs-on: ubuntu-latest
    permissions:
      security-events: write
      actions: read
      contents: read

    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7

      - name: Initialize CodeQL
        uses: github/codeql-action/init@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0
        with:
          languages: python
          queries: security-extended

      - name: Perform CodeQL Analysis
        uses: github/codeql-action/analyze@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0
        with:
          category: "/language:python"

  container-scan:
    name: Container Security Scan
    runs-on: ubuntu-latest
    if: github.event_name == 'pull_request'

    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7

      - name: Build Docker image
        run: docker build -t mcp-hangar:scan .

      - name: Run Trivy vulnerability scanner
        uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
        with:
          image-ref: "mcp-hangar:scan"
          format: "sarif"
          output: "trivy-results.sarif"
          severity: "CRITICAL,HIGH"

      - name: Upload Trivy scan results
        uses: github/codeql-action/upload-sarif@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0
        if: always()
        with:
          sarif_file: "trivy-results.sarif"

  import-boundary:
    name: Import Boundary Check
    runs-on: ubuntu-latest

    steps:
      - name: No-op (import boundary retired)
        run: echo "Import boundary check retired — all code lives in src/mcp_hangar/"

  secrets-scan:
    name: Secrets Scan
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
        with:
          fetch-depth: 0

      - name: Run Gitleaks
        uses: gitleaks/gitleaks-action@e0c47f4f8be36e29cdc102c57e68cb5cbf0e8d1e # v3
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          GITLEAKS_LICENSE: ${{ secrets.GITLEAKS_LICENSE }}
        continue-on-error: true  # License required for org repos; skip gracefully if absent

  semgrep:
    name: Semgrep SAST
    runs-on: ubuntu-latest
    permissions:
      security-events: write

    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7

      - name: Run Semgrep
        uses: semgrep/semgrep-action@713efdd345f3035192eaa63f56867b88e63e4e5d # v1
        with:
          config: >-
            p/python
            p/secrets
            p/owasp-top-ten
        env:
          SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }}
        continue-on-error: true  # Token optional; runs open rules without token

  sbom:
    name: SBOM Generation
    runs-on: ubuntu-latest
    if: github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch'

    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7

      - name: Set up Python
        uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7
        with:
          python-version: "3.11"

      - name: Install dependencies
        run: pip install -e .

      - name: Generate Python SBOM (CycloneDX)
        run: |
          pip install cyclonedx-bom
          cyclonedx-py environment --of json -o sbom-python.json

      - name: Upload SBOM artifact
        uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
        with:
          name: sbom
          path: sbom-python.json
          retention-days: 90
