name: OSV-Scanner

# Scans lockfiles (uv.lock, package-lock.json) against the OSV vulnerability
# database. Runs on every PR/push (via the ci.yml orchestrator's workflow_call)
# and on a weekly schedule against main.
#
# This is detection-only — OSV-Scanner does NOT open PRs or modify pins.
# It reports known CVEs in currently-pinned dependency versions so we can
# decide when and how to patch on our own schedule. Our pinning strategy
# (full SHA / exact version) is preserved; only the notification signal
# is added.
#
# Complements the supply-chain-audit.yml workflow (which scans for malicious
# code patterns in PR diffs) by covering the orthogonal "currently-pinned
# dep became known-vulnerable" case.
#
# Uses Google's officially-recommended reusable workflow, pinned by SHA.
# Findings land in the repo's Security tab (Code Scanning > OSV-Scanner).
# fail-on-vuln is disabled so the job does not block merges on pre-existing
# vulnerabilities in pinned deps that we may need to patch deliberately.
#
# The reusable workflow can't emit custom outputs, so a wrapper job
# downloads the SARIF result and summarizes the vulnerability count into
# a review_status for the unified PR comment.

on:
  workflow_call:
  schedule:
    # Weekly scan against main — catches CVEs published after merge for
    # deps that haven't changed since.
    - cron: '0 9 * * 1'
  workflow_dispatch:

permissions:
  # Required to upload SARIF file to CodeQL. See: https://github.com/github/codeql-action/issues/2117
  actions: read
  contents: read
  security-events: write

jobs:
  scan:
    name: Scan lockfiles
    uses: google/osv-scanner-action/.github/workflows/osv-scanner-reusable.yml@9a498708959aeaef5ef730655706c5a1df1edbc2 # v2.3.8
    with:
      # Scan explicit lockfiles rather than recursing, so we only look at
      # the five sources of truth and skip vendored / test / worktree dirs.
      scan-args: |-
        --lockfile=uv.lock
        --lockfile=package-lock.json
        --lockfile=website/package-lock.json
        --lockfile=plugins/platforms/photon/sidecar/package-lock.json
        --lockfile=scripts/whatsapp-bridge/package-lock.json
      # The upstream reusable workflow uploads this exact file under its
      # fixed artifact name, which the wrapper downloads below.
      results-file-name: osv-results.sarif
      fail-on-vuln: false

  emit-status:
    name: Emit review status
    runs-on: ubuntu-latest
    needs: scan
    if: always()
    outputs:
      review_status: ${{ steps.emit.outputs.review_status }}
    steps:
      - name: Checkout code
        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

      - name: Download SARIF result
        uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
        with:
          name: OSV Scanner SARIF file
          path: /tmp/osv-results
        continue-on-error: true

      - name: Emit review_status
        id: emit
        run: |
          set -euo pipefail
          STATUS="[]"

          if [ -f /tmp/osv-results/osv-results.sarif ]; then
            # Count vulnerabilities from the SARIF file
            VULN_COUNT=$(python3 -c "
          import json, sys
          try:
              with open('/tmp/osv-results/osv-results.sarif') as f:
                  data = json.load(f)
              count = 0
              vulns = []
              for run in data.get('runs', []):
                  for result in run.get('results', []):
                      count += 1
                      rule_id = result.get('ruleId', 'unknown')
                      message = result.get('message', {}).get('text', '')
                      loc = result.get('locations', [{}])[0].get('physicalLocation', {}).get('artifactLocation', {}).get('uri', '')
                      vulns.append(f'- {rule_id} in {loc}: {message}')
              print(count)
              if vulns:
                  print('\n'.join(vulns[:20]), file=sys.stderr)
          except Exception:
              print(0)
          ")

            VULN_DETAIL=""
            if [ "$VULN_COUNT" -gt 0 ] 2>/dev/null; then
              VULN_PLURAL=$([ "$VULN_COUNT" -eq 1 ] && echo "y" || echo "ies")
              VULN_DETAIL=$(python3 -c "
          import json, sys
          try:
              with open('/tmp/osv-results/osv-results.sarif') as f:
                  data = json.load(f)
              vulns = []
              for run in data.get('runs', []):
                  for result in run.get('results', []):
                      rule_id = result.get('ruleId', 'unknown')
                      loc = result.get('locations', [{}])[0].get('physicalLocation', {}).get('artifactLocation', {}).get('uri', '')
                      vulns.append(f'- {rule_id} in {loc}')
              print(json.dumps('\n'.join(vulns[:20])))
          except Exception:
              print(json.dumps(''))
          ")
              STATUS="[{\"source\":\"osv scan\",\"results\":[{\"kind\":\"warning\",\"title\":\"OSV vulnerability scan\",\"summary\":\"${VULN_COUNT} known vulnerabilit${VULN_PLURAL} found in pinned dependencies.\",\"detail\":${VULN_DETAIL},\"how_to_fix\":\"Review the findings in the [Security tab](../../security/code-scanning). Update the affected dependencies if a patched version is available.\"}]}]"
            else
              STATUS="[]"
            fi
          fi

          echo "review_status=${STATUS}" >> "$GITHUB_OUTPUT"
          echo "review_status=${STATUS}" > review-status.json

      - name: Upload review status artifact
        if: always() && steps.emit.outcome != 'skipped'
        continue-on-error: true
        uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
        with:
          name: review-status-osv-scanner
          path: review-status.json
          retention-days: 1
          overwrite: true
          if-no-files-found: ignore
