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,76 +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- # Identity is the 4-tuple (severity, rule_module, type, file) per
210- # the contract above. `action` is advisory metadata that varies
211- # by run/strategy and MUST NOT be part of finding identity —
212- # including it made every baseline entry fail to match.
213- jq '[ .[] | select(.severity == "critical" or .severity == "high")
214- | {severity, rule_module, type,
215- file: (.file | sub("^/home/runner/work/[^/]+/[^/]+/"; "")
216- | sub("^/github/workspace/"; "")) } ]' hypatia-findings.json > findings-current.json
217-
218- # Subtract baseline. A current finding is "new" iff there's no
219- # element in baseline equal to it (by-value).
220- jq --slurpfile base .hypatia-baseline.json \
221- '. as $cur | $cur | map(. as $f | select(($base[0] | any(. == $f)) | not))' \
222- findings-current.json > findings-new.json
223- new_count=$(jq 'length' findings-new.json)
224-
225- if [ "$new_count" -gt 0 ]; then
226- echo "::error::$new_count new critical/high finding(s) outside the baseline:"
227- jq -r '.[] | " [\(.severity)] \(.rule_module)/\(.type) — \(.file)"' findings-new.json
228- echo
229- echo "If these are intentional, regenerate .hypatia-baseline.json:"
230- echo " jq '[.[] | select(.severity == \"critical\" or .severity == \"high\") | {severity, rule_module, type, file}] | sort_by(.severity, .rule_module, .type, .file)' hypatia-findings.json > .hypatia-baseline.json"
231- exit 1
232- fi
233- echo "All critical/high findings present in baseline — gate passes."
234- else
235- echo "No .hypatia-baseline.json — failing on any critical/high (legacy behaviour)."
236- echo "Review hypatia-findings.json for details"
237- exit 1
238- 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
239182
240183 - name : Generate scan report
241- env :
242- REPO : ${{ github.repository }}
243- SHA : ${{ github.sha }}
244- CRITICAL_COUNT : ${{ steps.scan.outputs.critical }}
245- HIGH_COUNT : ${{ steps.scan.outputs.high }}
246- MEDIUM_COUNT : ${{ steps.scan.outputs.medium }}
247- TOTAL_COUNT : ${{ steps.scan.outputs.findings_count }}
248184 run : |
249185 cat << EOF > hypatia-report.md
250186 # Hypatia Security Scan Report
251187
252- **Repository:** $REPO
188+ **Repository:** ${{ github.repository }}
253189 **Scan Date:** $(date -u +"%Y-%m-%d %H:%M:%S UTC")
254- **Commit:** $SHA
190+ **Commit:** ${{ github.sha }}
255191
256192 ## Summary
257193
258194 | Severity | Count |
259195 |----------|-------|
260- | Critical | $CRITICAL_COUNT |
261- | High | $HIGH_COUNT |
262- | Medium | $MEDIUM_COUNT |
263- | **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 }} |
264200
265201 ## Next Steps
266202
@@ -279,14 +215,13 @@ jobs:
279215 cat hypatia-report.md >> $GITHUB_STEP_SUMMARY
280216
281217 - name : Comment on PR with findings
282- # Dependabot PRs always run with a read-only token regardless of the
283- # workflow's declared permissions, so the createComment call below
284- # would 403 on every dep-bump PR. The PR comment is informational
285- # (the check result is already visible in the PR UI); we don't want
286- # its absence to block merge.
287218 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.
288223 continue-on-error : true
289- uses : actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v7
224+ uses : actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v7
290225 with :
291226 script : |
292227 const fs = require('fs');
@@ -316,4 +251,4 @@ jobs:
316251 repo: context.repo.repo,
317252 issue_number: context.issue.number,
318253 body: comment
319- });
254+ });
0 commit comments