name: Vulnerability Scan

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

permissions:
  contents: read

jobs:
  pip-audit:
    name: pip-audit
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
        with:
          persist-credentials: false

      - name: Restore global uv cache
        id: cache-restore
        uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
        with:
          path: |
            ~/.cache/uv
            ~/.local/share/uv
            .venv
          key: uv-main-py3.11-${{ hashFiles('uv.lock') }}
          restore-keys: |
            uv-main-py3.11-

      - name: Install uv
        uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6
        with:
          version: "0.11.3"
          python-version: "3.11"
          enable-cache: false

      - name: Install dependencies
        run: uv sync --all-groups --all-extras --no-install-project

      - name: Install pip-audit
        run: uv pip install pip-audit

      - name: Run pip-audit
        run: |
          uv run pip-audit --desc --aliases --skip-editable --format json --output pip-audit-report.json \
            --ignore-vuln PYSEC-2024-277 \
            --ignore-vuln PYSEC-2026-89 \
            --ignore-vuln PYSEC-2026-97 \
            --ignore-vuln PYSEC-2025-148 \
            --ignore-vuln PYSEC-2025-183 \
            --ignore-vuln PYSEC-2025-189 \
            --ignore-vuln PYSEC-2025-190 \
            --ignore-vuln PYSEC-2025-191 \
            --ignore-vuln PYSEC-2025-192 \
            --ignore-vuln PYSEC-2025-193 \
            --ignore-vuln PYSEC-2025-194 \
            --ignore-vuln PYSEC-2025-195 \
            --ignore-vuln PYSEC-2025-196 \
            --ignore-vuln PYSEC-2025-197 \
            --ignore-vuln PYSEC-2025-210 \
            --ignore-vuln PYSEC-2026-139 \
            --ignore-vuln PYSEC-2025-211 \
            --ignore-vuln PYSEC-2025-212 \
            --ignore-vuln PYSEC-2025-213 \
            --ignore-vuln PYSEC-2025-214 \
            --ignore-vuln PYSEC-2025-215 \
            --ignore-vuln PYSEC-2025-216 \
            --ignore-vuln PYSEC-2025-217 \
            --ignore-vuln PYSEC-2025-218
        # Ignored CVEs:
        #   PYSEC-2024-277      - joblib 1.5.3: disputed; NumpyArrayWrapper only used with trusted caches
        #   PYSEC-2026-89       - markdown 3.10.2: DoS via malformed HTML; fix 3.8.1 — already past, advisory range is stale
        #   PYSEC-2026-97       - nltk 3.9.4: arbitrary file read in filestring(); no fix available
        #   PYSEC-2025-148      - onnx 1.21.0: path traversal in save_external_data; no fix available
        #   PYSEC-2025-183      - pyjwt 2.12.1: disputed weak-encryption claim; key length is application-chosen
        #   PYSEC-2025-189..197 - torch 2.11.0: memory-corruption/DoS in functions only reachable via untrusted models; no fix available
        #   PYSEC-2025-210, PYSEC-2026-139 - torch 2.11.0: profiler/deserialization issues; no fix available
        #   PYSEC-2025-211..218 - transformers 5.5.4: deserialization/code injection via malicious model checkpoints; no fix available
        continue-on-error: true

      - name: Display results
        if: always()
        run: |
          if [ -f pip-audit-report.json ]; then
            echo "## pip-audit Results" >> $GITHUB_STEP_SUMMARY
            echo '```json' >> $GITHUB_STEP_SUMMARY
            cat pip-audit-report.json | python3 -m json.tool >> $GITHUB_STEP_SUMMARY
            echo '```' >> $GITHUB_STEP_SUMMARY
            # Fail if vulnerabilities found
            python3 -c "
          import json, sys
          with open('pip-audit-report.json') as f:
              data = json.load(f)
          vulns = [d for d in data.get('dependencies', []) if d.get('vulns')]
          if vulns:
              print(f'::error::Found vulnerabilities in {len(vulns)} package(s)')
              for v in vulns:
                  for vuln in v['vulns']:
                      print(f'  - {v[\"name\"]}=={v[\"version\"]}: {vuln[\"id\"]}')
              sys.exit(1)
          print('No known vulnerabilities found')
          "
          else
            echo "::error::pip-audit failed to produce a report. Check the pip-audit step logs."
            exit 1
          fi

      - name: Upload pip-audit report
        if: always()
        uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
        with:
          name: pip-audit-report
          path: pip-audit-report.json

      - name: Save uv caches
        if: steps.cache-restore.outputs.cache-hit != 'true'
        uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
        with:
          path: |
            ~/.cache/uv
            ~/.local/share/uv
            .venv
          key: uv-main-py3.11-${{ hashFiles('uv.lock') }}

