1- # SPDX-License-Identifier: PMPL-1.0-or-later
2- # Hypatia Neurosymbolic CI/CD Security Scan
3- name : Hypatia Security Scan
4-
5- on :
6- push :
7- branches : [ main, master, develop ]
8- pull_request :
9- branches : [ main, master ]
10- schedule :
11- - cron : ' 0 0 * * 0' # Weekly on Sunday
12- workflow_dispatch :
13-
14- permissions :
15- contents : read
16- # security-events: read lets the built-in GITHUB_TOKEN query this
17- # repo\'s own Dependabot alerts via the Hypatia DependabotAlerts rule.
18- security-events : read
19-
20- jobs :
21- scan :
22- name : Hypatia Neurosymbolic Analysis
23- runs-on : ubuntu-latest
24-
25- steps :
26- - name : Checkout repository
27- uses : actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
28- with :
29- fetch-depth : 0 # Full history for better pattern analysis
30-
31- - name : Setup Elixir for Hypatia scanner
32- uses : erlef/setup-beam@e6d7c94229049569db56a7ad5a540c051a010af9 # v1.18.2
33- with :
34- elixir-version : ' 1.19.4'
35- otp-version : ' 28.3'
36-
37- - name : Clone Hypatia
38- run : |
39- if [ ! -d "$HOME/hypatia" ]; then
40- git clone https://github.com/hyperpolymath/hypatia.git "$HOME/hypatia"
41- fi
42-
43- - name : Build Hypatia scanner (if needed)
44- working-directory : ${{ env.HOME }}/hypatia
45- run : |
46- if [ ! -f hypatia ]; then
47- echo "Building hypatia scanner..."
48- mix deps.get
49- mix escript.build
50- fi
51-
52- - name : Run Hypatia scan
53- id : scan
54- run : |
55- echo "Scanning repository: ${{ github.repository }}"
56-
57- # Run scanner (exits non-zero when findings exist — suppress to continue)
58- HYPATIA_FORMAT=json "$HOME/hypatia/hypatia-cli.sh" scan . > hypatia-findings.json || true
59-
60- # Count findings
61- FINDING_COUNT=$(jq '. | length' hypatia-findings.json 2>/dev/null || echo 0)
62- echo "findings_count=$FINDING_COUNT" >> $GITHUB_OUTPUT
63-
64- # Extract severity counts
65- CRITICAL=$(jq '[.[] | select(.severity == "critical")] | length' hypatia-findings.json)
66- HIGH=$(jq '[.[] | select(.severity == "high")] | length' hypatia-findings.json)
67- MEDIUM=$(jq '[.[] | select(.severity == "medium")] | length' hypatia-findings.json)
68-
69- echo "critical=$CRITICAL" >> $GITHUB_OUTPUT
70- echo "high=$HIGH" >> $GITHUB_OUTPUT
71- echo "medium=$MEDIUM" >> $GITHUB_OUTPUT
72-
73- echo "## Hypatia Scan Results" >> $GITHUB_STEP_SUMMARY
74- echo "- Total findings: $FINDING_COUNT" >> $GITHUB_STEP_SUMMARY
75- echo "- Critical: $CRITICAL" >> $GITHUB_STEP_SUMMARY
76- echo "- High: $HIGH" >> $GITHUB_STEP_SUMMARY
77- echo "- Medium: $MEDIUM" >> $GITHUB_STEP_SUMMARY
78-
79- - name : Upload findings artifact
80- uses : actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
81- with :
82- name : hypatia-findings
83- path : hypatia-findings.json
84- retention-days : 90
85-
86- - name : Submit findings to gitbot-fleet (Phase 2)
87- if : steps.scan.outputs.findings_count > 0
88- env :
89- GITHUB_TOKEN : ${{ secrets.GITHUB_TOKEN }}
90- GITHUB_REPOSITORY : ${{ github.repository }}
91- GITHUB_SHA : ${{ github.sha }}
92- run : |
93- echo "📤 Submitting ${{ steps.scan.outputs.findings_count }} findings to gitbot-fleet..."
94-
95- # Clone gitbot-fleet to temp directory
96- FLEET_DIR="/tmp/gitbot-fleet-$$"
97- git clone https://github.com/hyperpolymath/gitbot-fleet.git "$FLEET_DIR"
98-
99- # Run submission script
100- bash "$FLEET_DIR/scripts/submit-finding.sh" hypatia-findings.json
101-
102- # Cleanup
103- rm -rf "$FLEET_DIR"
104-
105- echo "✅ Finding submission complete"
106-
107- - name : Check for critical issues
108- if : steps.scan.outputs.critical > 0
109- run : |
110- echo "⚠️ Critical security issues found!"
111- echo "Review hypatia-findings.json for details"
112- # Don't fail the build yet - just warn
113- # exit 1
114-
115- - name : Generate scan report
116- run : |
117- cat << EOF > hypatia-report.md
118- # Hypatia Security Scan Report
119-
120- **Repository:** ${{ github.repository }}
121- **Scan Date:** $(date -u +"%Y-%m-%d %H:%M:%S UTC")
122- **Commit:** ${{ github.sha }}
123-
124- ## Summary
125-
126- | Severity | Count |
127- |----------|-------|
128- | Critical | ${{ steps.scan.outputs.critical }} |
129- | High | ${{ steps.scan.outputs.high }} |
130- | Medium | ${{ steps.scan.outputs.medium }} |
131- | **Total**| ${{ steps.scan.outputs.findings_count }} |
132-
133- ## Next Steps
134-
135- 1. Review findings in the artifact: hypatia-findings.json
136- 2. Auto-fixable issues will be addressed by robot-repo-automaton (Phase 3)
137- 3. Manual review required for complex issues
138-
139- ## Learning
140-
141- These findings feed Hypatia's learning engine to improve future rules.
142-
143- ---
144- *Powered by [Hypatia](https://github.com/hyperpolymath/hypatia) - Neurosymbolic CI/CD Intelligence*
145- EOF
146-
147- cat hypatia-report.md >> $GITHUB_STEP_SUMMARY
148-
149- - name : Comment on PR with findings
150- if : github.event_name == 'pull_request' && steps.scan.outputs.findings_count > 0
151- uses : actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v7
152- with :
153- script : |
154- const fs = require('fs');
155- const findings = JSON.parse(fs.readFileSync('hypatia-findings.json', 'utf8'));
156-
157- const critical = findings.filter(f => f.severity === 'critical').length;
158- const high = findings.filter(f => f.severity === 'high').length;
159-
160- let comment = `## 🔍 Hypatia Security Scan\n\n`;
161- comment += `**Findings:** ${findings.length} issues detected\n\n`;
162- comment += `| Severity | Count |\n|----------|-------|\n`;
163- comment += `| 🔴 Critical | ${critical} |\n`;
164- comment += `| 🟠 High | ${high} |\n`;
165- comment += `| 🟡 Medium | ${findings.length - critical - high} |\n\n`;
166-
167- if (critical > 0) {
168- comment += `⚠️ **Action Required:** Critical security issues found!\n\n`;
169- }
170-
171- comment += `<details><summary>View findings</summary>\n\n`;
172- comment += `\`\`\`json\n${JSON.stringify(findings.slice(0, 10), null, 2)}\n\`\`\`\n`;
173- comment += `</details>\n\n`;
174- comment += `*Powered by Hypatia Neurosymbolic CI/CD Intelligence*`;
175-
176- github.rest.issues.createComment({
177- owner: context.repo.owner,
178- repo: context.repo.repo,
179- issue_number: context.issue.number,
180- body: comment
181- });
1+ # SPDX-License-Identifier: PMPL-1.0-or-later# Hypatia Neurosymbolic CI/CD Security Scanname: Hypatia Security Scanon: push: branches: [ main, master, develop ] pull_request: branches: [ main, master ] schedule: - cron: '0 0 * * 0' # Weekly on Sunday workflow_dispatch:permissions: contents: read # `pull-requests: write` is needed for the "Comment on PR with findings" # step to POST a results summary. Note: on Dependabot PRs the token is # downgraded to read-only regardless, so that step is also marked # continue-on-error below. pull-requests: writejobs: scan: name: Hypatia Neurosymbolic Analysis runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4 with: fetch-depth: 0 # Full history for better pattern analysis - name: Setup Elixir for Hypatia scanner uses: erlef/setup-beam@fc68ffb90438ef2936bbb3251622353b3dcb2f93 # v1.18.2 with: elixir-version: '1.19.4' otp-version: '28.3' - name: Clone Hypatia (or use checkout when scanning hypatia itself) run: | # When scanning hypatia from inside hypatia, point $HOME/hypatia # at the PR/branch checkout instead of cloning main — otherwise # CLI changes can never pass their own gate (the scanner binary # would always come from main and ignore new flags). if [ "${{ github.repository }}" = "hyperpolymath/hypatia" ]; then ln -sfn "${GITHUB_WORKSPACE}" "$HOME/hypatia" elif [ ! -d "$HOME/hypatia" ]; then git clone https://github.com/hyperpolymath/hypatia.git "$HOME/hypatia" fi - name: Build Hypatia scanner (if needed) run: | cd "$HOME/hypatia" if [ ! -f hypatia ]; then echo "Building hypatia scanner..." mix deps.get mix escript.build fi - name: Run Hypatia scan id: scan env: # Suppress the "Warning: Dependabot alerts unavailable: GITHUB_TOKEN # not set" line so the run is silent-warning-free. The token is # read-only by default and only used to query Dependabot alerts. GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | echo "Scanning repository: ${{ github.repository }}" # Run scanner with --exit-zero so a findings-found exit-1 does # NOT short-circuit the rest of this step under `set -e`. The # downstream "Check for critical or high-severity issues" step # is the explicit gate. See hyperpolymath/hypatia#213. # # Guard against the scanner producing no output (a crash, an # unknown flag, etc.): if hypatia-findings.json is empty or # missing after the run, fall back to "[]" so the jq calls # below don't 9 the whole gate. We surface stderr so the # underlying scanner failure is still visible in the log. set +e HYPATIA_FORMAT=json "$HOME/hypatia/hypatia-cli.sh" scan . --exit-zero \ > hypatia-findings.json 2> hypatia-scan.stderr SCAN_EXIT=$? set -e echo "Scanner exit: $SCAN_EXIT" if [ -s hypatia-scan.stderr ]; then echo "--- scanner stderr ---" cat hypatia-scan.stderr echo "--- end stderr ---" fi if ! jq empty hypatia-findings.json 2>/dev/null; then echo "Scanner did not produce valid JSON; defaulting to empty findings." echo "[]" > hypatia-findings.json fi # Count findings FINDING_COUNT=$(jq '. | length' hypatia-findings.json 2>/dev/null || echo 0) echo "findings_count=$FINDING_COUNT" >> $GITHUB_OUTPUT # Extract severity counts CRITICAL=$(jq '[.[] | select(.severity == "critical")] | length' hypatia-findings.json) HIGH=$(jq '[.[] | select(.severity == "high")] | length' hypatia-findings.json) MEDIUM=$(jq '[.[] | select(.severity == "medium")] | length' hypatia-findings.json) echo "critical=$CRITICAL" >> $GITHUB_OUTPUT echo "high=$HIGH" >> $GITHUB_OUTPUT echo "medium=$MEDIUM" >> $GITHUB_OUTPUT echo "## Hypatia Scan Results" >> $GITHUB_STEP_SUMMARY echo "- Total findings: $FINDING_COUNT" >> $GITHUB_STEP_SUMMARY echo "- Critical: $CRITICAL" >> $GITHUB_STEP_SUMMARY echo "- High: $HIGH" >> $GITHUB_STEP_SUMMARY echo "- Medium: $MEDIUM" >> $GITHUB_STEP_SUMMARY - name: Upload findings artifact uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: hypatia-findings path: hypatia-findings.json retention-days: 90 - name: Submit findings to gitbot-fleet (Phase 2) if: steps.scan.outputs.findings_count > 0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} FLEET_PUSH_TOKEN: ${{ secrets.HYPATIA_DISPATCH_PAT }} FLEET_DISPATCH_TOKEN: ${{ secrets.HYPATIA_DISPATCH_PAT }} GITHUB_REPOSITORY: ${{ github.repository }} GITHUB_SHA: ${{ github.sha }} run: | echo "📤 Submitting ${{ steps.scan.outputs.findings_count }} findings to gitbot-fleet..." # Clone gitbot-fleet to temp directory FLEET_DIR="/tmp/gitbot-fleet-$$" git clone https://github.com/hyperpolymath/gitbot-fleet.git "$FLEET_DIR" # Run submission script. Pass the findings path as ABSOLUTE — # submit-finding.sh cd's into its own working dir before reading # the file, so a relative path would resolve to the wrong place # and the script fails with "No such file or directory". bash "$FLEET_DIR/scripts/submit-finding.sh" "$GITHUB_WORKSPACE/hypatia-findings.json" # Cleanup rm -rf "$FLEET_DIR" echo "✅ Finding submission complete" - name: Check for critical or high-severity issues if: steps.scan.outputs.critical > 0 || steps.scan.outputs.high > 0 run: | echo "Total critical/high: ${{ steps.scan.outputs.critical }} critical, ${{ steps.scan.outputs.high }} high" # Baseline-aware gate: pre-existing accepted findings live in # .hypatia-baseline.json (committed). New critical/high findings # not in the baseline still fail the build. Findings are matched # on (severity, rule_module, type, file) tuple with absolute # build paths normalised to repo-relative. if [ -f .hypatia-baseline.json ]; then # Normalise + project the FINDING IDENTITY tuple from the current # scan. Identity is (severity, rule_module, type, file) — `action` # is remediation guidance that can legitimately drift between # scanner versions (e.g. "flag" -> "create_branch") and is NOT # part of what makes two findings the same. jq '[ .[] | select(.severity == "critical" or .severity == "high") | {severity, rule_module, type, file: (.file | sub("^/home/runner/work/[^/]+/[^/]+/"; "") | sub("^/github/workspace/"; "")) } ]' \ hypatia-findings.json > findings-current.json # Subtract baseline. A current finding is "new" iff there's no # baseline element with the same identity tuple. Baseline entries # may include extra fields (e.g. `action`); strip them before the # comparison so legacy baselines keep working. jq --slurpfile base .hypatia-baseline.json \ '($base[0] | map({severity, rule_module, type, file})) as $bk | map(. as $f | select(($bk | any(. == $f)) | not))' \ findings-current.json > findings-new.json new_count=$(jq 'length' findings-new.json) if [ "$new_count" -gt 0 ]; then echo "::error::$new_count new critical/high finding(s) outside the baseline:" jq -r '.[] | " [\(.severity)] \(.rule_module)/\(.type) — \(.file)"' findings-new.json echo echo "If these are intentional, regenerate .hypatia-baseline.json:" 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" exit 1 fi echo "All critical/high findings present in baseline — gate passes." else echo "No .hypatia-baseline.json — failing on any critical/high (legacy behaviour)." echo "Review hypatia-findings.json for details" exit 1 fi - name: Generate scan report run: | cat << EOF > hypatia-report.md # Hypatia Security Scan Report **Repository:** ${{ github.repository }} **Scan Date:** $(date -u +"%Y-%m-%d %H:%M:%S UTC") **Commit:** ${{ github.sha }} ## Summary | Severity | Count | |----------|-------| | Critical | ${{ steps.scan.outputs.critical }} | | High | ${{ steps.scan.outputs.high }} | | Medium | ${{ steps.scan.outputs.medium }} | | **Total**| ${{ steps.scan.outputs.findings_count }} | ## Next Steps 1. Review findings in the artifact: hypatia-findings.json 2. Auto-fixable issues will be addressed by robot-repo-automaton (Phase 3) 3. Manual review required for complex issues ## Learning These findings feed Hypatia's learning engine to improve future rules. --- *Powered by [Hypatia](https://github.com/hyperpolymath/hypatia) - Neurosymbolic CI/CD Intelligence* EOF cat hypatia-report.md >> $GITHUB_STEP_SUMMARY - name: Comment on PR with findings # Dependabot PRs always run with a read-only token regardless of the # workflow's declared permissions, so the createComment call below # would 403 on every dep-bump PR. The PR comment is informational # (the check result is already visible in the PR UI); we don't want # its absence to block merge. if: github.event_name == 'pull_request' && steps.scan.outputs.findings_count > 0 continue-on-error: true uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v7 with: script: | const fs = require('fs'); const findings = JSON.parse(fs.readFileSync('hypatia-findings.json', 'utf8')); const critical = findings.filter(f => f.severity === 'critical').length; const high = findings.filter(f => f.severity === 'high').length; let comment = `## 🔍 Hypatia Security Scan\n\n`; comment += `**Findings:** ${findings.length} issues detected\n\n`; comment += `| Severity | Count |\n|----------|-------|\n`; comment += `| 🔴 Critical | ${critical} |\n`; comment += `| 🟠 High | ${high} |\n`; comment += `| 🟡 Medium | ${findings.length - critical - high} |\n\n`; if (critical > 0) { comment += `⚠️ **Action Required:** Critical security issues found!\n\n`; } comment += `<details><summary>View findings</summary>\n\n`; comment += `\`\`\`json\n${JSON.stringify(findings.slice(0, 10), null, 2)}\n\`\`\`\n`; comment += `</details>\n\n`; comment += `*Powered by Hypatia Neurosymbolic CI/CD Intelligence*`; github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, body: comment });
0 commit comments