diff --git a/.github/workflows/hypatia-scan.yml b/.github/workflows/hypatia-scan.yml index e6ac4c12..860a2b79 100644 --- a/.github/workflows/hypatia-scan.yml +++ b/.github/workflows/hypatia-scan.yml @@ -10,24 +10,35 @@ on: schedule: - cron: '0 0 * * 0' # Weekly on Sunday workflow_dispatch: - -permissions: read-all +# Estate guardrail: cancel superseded runs so re-pushes don't pile up +# queued runs across the estate. Safe here because this workflow only +# performs read-only checks/lint/test/scan with no publish or mutation. +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + # security-events: read lets the built-in GITHUB_TOKEN query this + # repo's own Dependabot alerts via the Hypatia DependabotAlerts rule + # (DA001-DA004). Without this, `scan_from_path` gets HTTP 403 and + # the rule silently returns no findings. + # See 007-lang/audits/audit-dependabot-automation-gap-2026-04-17.md. + security-events: read + # pull-requests: write lets the advisory "Comment on PR with findings" + # step post its summary. Without it the built-in GITHUB_TOKEN gets + # "Resource not accessible by integration" and (absent continue-on-error) + # hard-fails the scan — exactly what the gate-decoupling design forbids. + pull-requests: write jobs: scan: name: Hypatia Neurosymbolic Analysis runs-on: ubuntu-latest - env: - # ${{ env.HOME }} is empty in working-directory context (HOME is not in - # env unless explicitly set), which makes the directory resolve to "/hypatia" - # and the build step fails with "No such file or directory". A job-level env - # var is visible to both shell ($HYPATIA_DIR) and Actions expressions - # (${{ env.HYPATIA_DIR }}). - HYPATIA_DIR: /home/runner/hypatia steps: - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 # Full history for better pattern analysis @@ -39,33 +50,32 @@ jobs: - name: Clone Hypatia run: | - if [ ! -d "$HYPATIA_DIR" ]; then - git clone https://github.com/hyperpolymath/hypatia.git "$HYPATIA_DIR" + if [ ! -d "$HOME/hypatia" ]; then + git clone https://github.com/hyperpolymath/hypatia.git "$HOME/hypatia" fi - name: Build Hypatia scanner (if needed) - working-directory: ${{ env.HYPATIA_DIR }} run: | - if [ ! -f hypatia-v2 ]; then - echo "Building hypatia-v2 scanner..." + cd "$HOME/hypatia" + if [ ! -f hypatia ]; then + echo "Building hypatia scanner..." mix deps.get mix escript.build - mv hypatia hypatia-v2 fi - name: Run Hypatia scan id: scan - continue-on-error: true # scanner exits 1 on infra errors; critical findings block via the separate Check step env: - # Hypatia uses Dependabot alerts as one of its signal sources. - # Without GITHUB_TOKEN it warns and exits 1. The default GITHUB_TOKEN - # has the security_events:read scope needed to query alerts. + # Pass the built-in Actions token through to Hypatia so the + # DependabotAlerts rule can query this repo's own alerts. + # For cross-repo scanning (fleet-coordinator scan-supervised), + # a PAT with `security_events` scope is required instead. GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | echo "Scanning repository: ${{ github.repository }}" - # Run scanner - HYPATIA_FORMAT=json "$HYPATIA_DIR/hypatia-cli.sh" scan . > hypatia-findings.json + # Run scanner (exits non-zero when findings exist — suppress to continue) + HYPATIA_FORMAT=json "$HOME/hypatia/hypatia-cli.sh" scan . --exit-zero > hypatia-findings.json || true # Count findings FINDING_COUNT=$(jq '. | length' hypatia-findings.json 2>/dev/null || echo 0) @@ -206,7 +216,12 @@ jobs: - name: Comment on PR with findings if: github.event_name == 'pull_request' && steps.scan.outputs.findings_count > 0 - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v7 + # Advisory only — posting findings as a PR comment must never gate + # the scan (hypatia#213 gate decoupling). Belt-and-braces alongside + # the pull-requests: write permission above: a token/API hiccup or + # a fork PR (read-only token) skips the comment, not the check. + continue-on-error: true + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v7 with: script: | const fs = require('fs'); @@ -236,4 +251,4 @@ jobs: repo: context.repo.repo, issue_number: context.issue.number, body: comment - }); + }); \ No newline at end of file