1010 schedule :
1111 - cron : ' 0 0 * * 0' # Weekly on Sunday
1212 workflow_dispatch :
13+ # Estate guardrail: cancel superseded runs so re-pushes don't pile up
14+ # queued runs across the estate. Safe here because this workflow only
15+ # performs read-only checks/lint/test/scan with no publish or mutation.
16+ concurrency :
17+ group : ${{ github.workflow }}-${{ github.ref }}
18+ cancel-in-progress : true
1319
1420permissions :
1521 contents : read
16- # `pull-requests: write` is needed for the "Comment on PR with findings"
17- # step to POST a results summary. Note: on Dependabot PRs the token is
18- # downgraded to read-only regardless, so that step is also marked
19- # continue-on-error below.
22+ # security-events: read lets the built-in GITHUB_TOKEN query this
23+ # repo's own Dependabot alerts via the Hypatia DependabotAlerts rule
24+ # (DA001-DA004). Without this, `scan_from_path` gets HTTP 403 and
25+ # the rule silently returns no findings.
26+ # See 007-lang/audits/audit-dependabot-automation-gap-2026-04-17.md.
27+ security-events : read
28+ # pull-requests: write lets the advisory "Comment on PR with findings"
29+ # step post its summary. Without it the built-in GITHUB_TOKEN gets
30+ # "Resource not accessible by integration" and (absent continue-on-error)
31+ # hard-fails the scan — exactly what the gate-decoupling design forbids.
2032 pull-requests : write
2133
2234jobs :
2638
2739 steps :
2840 - name : Checkout repository
29- uses : actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4
41+ uses : actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
3042 with :
3143 fetch-depth : 0 # Full history for better pattern analysis
3244
3648 elixir-version : ' 1.19.4'
3749 otp-version : ' 28.3'
3850
39- - name : Clone Hypatia (or use checkout when scanning hypatia itself)
40- env :
41- REPO : ${{ github.repository }}
51+ - name : Clone Hypatia
4252 run : |
43- # When scanning hypatia from inside hypatia, point $HOME/hypatia
44- # at the PR/branch checkout instead of cloning main — otherwise
45- # CLI changes can never pass their own gate (the scanner binary
46- # would always come from main and ignore new flags).
47- if [ "$REPO" = "hyperpolymath/hypatia" ]; then
48- ln -sfn "${GITHUB_WORKSPACE}" "$HOME/hypatia"
49- elif [ ! -d "$HOME/hypatia" ]; then
53+ if [ ! -d "$HOME/hypatia" ]; then
5054 git clone https://github.com/hyperpolymath/hypatia.git "$HOME/hypatia"
5155 fi
5256
@@ -62,39 +66,16 @@ jobs:
6266 - name : Run Hypatia scan
6367 id : scan
6468 env :
65- # Suppress the "Warning: Dependabot alerts unavailable: GITHUB_TOKEN
66- # not set" line so the run is silent-warning-free. The token is
67- # read-only by default and only used to query Dependabot alerts.
69+ # Pass the built-in Actions token through to Hypatia so the
70+ # DependabotAlerts rule can query this repo's own alerts.
71+ # For cross-repo scanning (fleet-coordinator scan-supervised),
72+ # a PAT with `security_events` scope is required instead.
6873 GITHUB_TOKEN : ${{ secrets.GITHUB_TOKEN }}
69- REPO : ${{ github.repository }}
7074 run : |
71- echo "Scanning repository: $REPO"
72-
73- # Run scanner with --exit-zero so a findings-found exit-1 does
74- # NOT short-circuit the rest of this step under `set -e`. The
75- # downstream "Check for critical or high-severity issues" step
76- # is the explicit gate. See hyperpolymath/hypatia#213.
77- #
78- # Guard against the scanner producing no output (a crash, an
79- # unknown flag, etc.): if hypatia-findings.json is empty or
80- # missing after the run, fall back to "[]" so the jq calls
81- # below don't 9 the whole gate. We surface stderr so the
82- # underlying scanner failure is still visible in the log.
83- set +e
84- HYPATIA_FORMAT=json "$HOME/hypatia/hypatia-cli.sh" scan . --exit-zero \
85- > hypatia-findings.json 2> hypatia-scan.stderr
86- SCAN_EXIT=$?
87- set -e
88- echo "Scanner exit: $SCAN_EXIT"
89- if [ -s hypatia-scan.stderr ]; then
90- echo "--- scanner stderr ---"
91- cat hypatia-scan.stderr
92- echo "--- end stderr ---"
93- fi
94- if ! jq empty hypatia-findings.json 2>/dev/null; then
95- echo "Scanner did not produce valid JSON; defaulting to empty findings."
96- echo "[]" > hypatia-findings.json
97- fi
75+ echo "Scanning repository: ${{ github.repository }}"
76+
77+ # Run scanner (exits non-zero when findings exist — suppress to continue)
78+ HYPATIA_FORMAT=json "$HOME/hypatia/hypatia-cli.sh" scan . --exit-zero > hypatia-findings.json || true
9879
9980 # Count findings
10081 FINDING_COUNT=$(jq '. | length' hypatia-findings.json 2>/dev/null || echo 0)
11697 echo "- Medium: $MEDIUM" >> $GITHUB_STEP_SUMMARY
11798
11899 - name : Upload findings artifact
119- uses : actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
100+ uses : actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
120101 with :
121102 name : hypatia-findings
122103 path : hypatia-findings.json
@@ -191,73 +172,31 @@ jobs:
191172 # Cleanup
192173 rm -rf "$FLEET_DIR"
193174
194- - name : Check for critical or high-severity issues
195- if : steps.scan.outputs.critical > 0 || steps.scan.outputs.high > 0
196- env :
197- CRITICAL_COUNT : ${{ steps.scan.outputs.critical }}
198- HIGH_COUNT : ${{ steps.scan.outputs.high }}
175+ - name : Check for critical issues
176+ if : steps.scan.outputs.critical > 0
199177 run : |
200- echo "Total critical/high: $CRITICAL_COUNT critical, $HIGH_COUNT high"
201-
202- # Baseline-aware gate: pre-existing accepted findings live in
203- # .hypatia-baseline.json (committed). New critical/high findings
204- # not in the baseline still fail the build. Findings are matched
205- # on (severity, rule_module, type, file) tuple with absolute
206- # build paths normalised to repo-relative.
207- if [ -f .hypatia-baseline.json ]; then
208- # Normalise + project just the comparable keys from the current scan.
209- jq '[ .[] | select(.severity == "critical" or .severity == "high")
210- | {severity, rule_module, type,
211- file: (.file | sub("^/home/runner/work/[^/]+/[^/]+/"; "")
212- | sub("^/github/workspace/"; "")),
213- action} ]' hypatia-findings.json > findings-current.json
214-
215- # Subtract baseline. A current finding is "new" iff there's no
216- # element in baseline equal to it (by-value).
217- jq --slurpfile base .hypatia-baseline.json \
218- '. as $cur | $cur | map(. as $f | select(($base[0] | any(. == $f)) | not))' \
219- findings-current.json > findings-new.json
220- new_count=$(jq 'length' findings-new.json)
221-
222- if [ "$new_count" -gt 0 ]; then
223- echo "::error::$new_count new critical/high finding(s) outside the baseline:"
224- jq -r '.[] | " [\(.severity)] \(.rule_module)/\(.type) — \(.file)"' findings-new.json
225- echo
226- echo "If these are intentional, regenerate .hypatia-baseline.json:"
227- echo " jq '[.[] | select(.severity == \"critical\" or .severity == \"high\") | {severity, rule_module, type, file, action}] | sort_by(.severity, .rule_module, .type, .file)' hypatia-findings.json > .hypatia-baseline.json"
228- exit 1
229- fi
230- echo "All critical/high findings present in baseline — gate passes."
231- else
232- echo "No .hypatia-baseline.json — failing on any critical/high (legacy behaviour)."
233- echo "Review hypatia-findings.json for details"
234- exit 1
235- fi
178+ echo "⚠️ Critical security issues found!"
179+ echo "Review hypatia-findings.json for details"
180+ # Don't fail the build yet - just warn
181+ # exit 1
236182
237183 - name : Generate scan report
238- env :
239- REPO : ${{ github.repository }}
240- SHA : ${{ github.sha }}
241- CRITICAL_COUNT : ${{ steps.scan.outputs.critical }}
242- HIGH_COUNT : ${{ steps.scan.outputs.high }}
243- MEDIUM_COUNT : ${{ steps.scan.outputs.medium }}
244- TOTAL_COUNT : ${{ steps.scan.outputs.findings_count }}
245184 run : |
246185 cat << EOF > hypatia-report.md
247186 # Hypatia Security Scan Report
248187
249- **Repository:** $REPO
188+ **Repository:** ${{ github.repository }}
250189 **Scan Date:** $(date -u +"%Y-%m-%d %H:%M:%S UTC")
251- **Commit:** $SHA
190+ **Commit:** ${{ github.sha }}
252191
253192 ## Summary
254193
255194 | Severity | Count |
256195 |----------|-------|
257- | Critical | $CRITICAL_COUNT |
258- | High | $HIGH_COUNT |
259- | Medium | $MEDIUM_COUNT |
260- | **Total**| $TOTAL_COUNT |
196+ | Critical | ${{ steps.scan.outputs.critical }} |
197+ | High | ${{ steps.scan.outputs.high }} |
198+ | Medium | ${{ steps.scan.outputs.medium }} |
199+ | **Total**| ${{ steps.scan.outputs.findings_count }} |
261200
262201 ## Next Steps
263202
@@ -276,14 +215,13 @@ jobs:
276215 cat hypatia-report.md >> $GITHUB_STEP_SUMMARY
277216
278217 - name : Comment on PR with findings
279- # Dependabot PRs always run with a read-only token regardless of the
280- # workflow's declared permissions, so the createComment call below
281- # would 403 on every dep-bump PR. The PR comment is informational
282- # (the check result is already visible in the PR UI); we don't want
283- # its absence to block merge.
284218 if : github.event_name == 'pull_request' && steps.scan.outputs.findings_count > 0
219+ # Advisory only — posting findings as a PR comment must never gate
220+ # the scan (hypatia#213 gate decoupling). Belt-and-braces alongside
221+ # the pull-requests: write permission above: a token/API hiccup or
222+ # a fork PR (read-only token) skips the comment, not the check.
285223 continue-on-error : true
286- uses : actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v7
224+ uses : actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v7
287225 with :
288226 script : |
289227 const fs = require('fs');
@@ -313,4 +251,4 @@ jobs:
313251 repo: context.repo.repo,
314252 issue_number: context.issue.number,
315253 body: comment
316- });
254+ });
0 commit comments