From b0c5f7920ed10bdd2f3f9c825a3126aed303d0b9 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Thu, 14 May 2026 11:47:15 +0100 Subject: [PATCH 1/3] ci(workflow): adopt hardened hypatia-scan from hyperpolymath/hypatia#237 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the local copy of `.github/workflows/hypatia-scan.yml` with the canonical version from upstream main. The old copy had three issues that combined to break every Dependabot PR: 1. `working-directory: \${{ env.HOME }}/hypatia\``, where `env.HOME` is not a GHA context — it evaluated to empty, so `cd /hypatia` failed and the scanner was never built. 2. `hypatia-cli.sh scan .` without `--exit-zero` — scanner exit-1 on findings short-circuited the rest of the step under `set -e`. 3. No baseline gate, so any pre-existing critical/high failed the build. Upstream version: - captures scanner exit code + stderr (visible on crash) - falls back to `[]` on missing/invalid JSON - reads `.hypatia-baseline.json` and fails only on NET-NEW critical/high - scopes permissions narrowly (contents: read, pull-requests: write) - marks the PR-comment step `continue-on-error: true` so Dependabot PRs (read-only token) don't fail on the unavoidable 403 Baseline file follows in a second commit on this branch — first we need the new workflow to actually run and capture current findings. Unblocks PR #59 (CODEOWNERS) which is stuck on this exact scan. Co-Authored-By: Claude Opus 4.7 --- .github/workflows/hypatia-scan.yml | 184 +---------------------------- 1 file changed, 1 insertion(+), 183 deletions(-) diff --git a/.github/workflows/hypatia-scan.yml b/.github/workflows/hypatia-scan.yml index 2474316..7c02a13 100644 --- a/.github/workflows/hypatia-scan.yml +++ b/.github/workflows/hypatia-scan.yml @@ -1,183 +1 @@ -# SPDX-License-Identifier: PMPL-1.0-or-later -# Hypatia Neurosymbolic CI/CD Security Scan -name: Hypatia Security Scan - -on: - push: - branches: [ main, master, develop ] - pull_request: - branches: [ main, master ] - schedule: - - cron: '0 0 * * 0' # Weekly on Sunday - workflow_dispatch: - -permissions: - contents: read - # security-events: read lets the built-in GITHUB_TOKEN query this - # repo\'s own Dependabot alerts via the Hypatia DependabotAlerts rule. - security-events: read - -jobs: - 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@e6d7c94229049569db56a7ad5a540c051a010af9 # v1.18.2 - with: - elixir-version: '1.19.4' - otp-version: '28.3' - - - name: Clone Hypatia - run: | - 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.HOME }}/hypatia - run: | - if [ ! -f hypatia-v2 ]; then - echo "Building hypatia-v2 scanner..." - cd scanner - mix deps.get - mix escript.build - mv hypatia ../hypatia-v2 - fi - - - name: Run Hypatia scan - id: scan - run: | - echo "Scanning repository: ${{ github.repository }}" - - # Run scanner - HYPATIA_FORMAT=json "$HOME/hypatia/hypatia-cli.sh" scan . > hypatia-findings.json - - # 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@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 - 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 }} - 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 - bash "$FLEET_DIR/scripts/submit-finding.sh" hypatia-findings.json - - # Cleanup - rm -rf "$FLEET_DIR" - - echo "✅ Finding submission complete" - - - name: Check for critical issues - if: steps.scan.outputs.critical > 0 - run: | - echo "⚠️ Critical security issues found!" - echo "Review hypatia-findings.json for details" - # Don't fail the build yet - just warn - # exit 1 - - - 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 - if: github.event_name == 'pull_request' && steps.scan.outputs.findings_count > 0 - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # 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 += `
View findings\n\n`; - comment += `\`\`\`json\n${JSON.stringify(findings.slice(0, 10), null, 2)}\n\`\`\`\n`; - comment += `
\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 - }); \ No newline at end of file +# 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 += `
View findings\n\n`; comment += `\`\`\`json\n${JSON.stringify(findings.slice(0, 10), null, 2)}\n\`\`\`\n`; comment += `
\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 }); \ No newline at end of file From 9fcb0e58d901f75cbe4a4aa73c51b666996a0e76 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Thu, 14 May 2026 11:57:14 +0100 Subject: [PATCH 2/3] ci(fixup): restore newlines in hypatia-scan.yml Previous commit on this branch wrote the YAML as a single line due to a PowerShell encoding/-NoNewline mistake on my end. This re-applies the canonical workflow content byte-for-byte, with line breaks intact, so GitHub Actions can parse it. Co-Authored-By: Claude Opus 4.7 --- .github/workflows/hypatia-scan.yml | 268 ++++++++++++++++++++++++++++- 1 file changed, 267 insertions(+), 1 deletion(-) diff --git a/.github/workflows/hypatia-scan.yml b/.github/workflows/hypatia-scan.yml index 7c02a13..7537078 100644 --- a/.github/workflows/hypatia-scan.yml +++ b/.github/workflows/hypatia-scan.yml @@ -1 +1,267 @@ -# 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 += `
View findings\n\n`; comment += `\`\`\`json\n${JSON.stringify(findings.slice(0, 10), null, 2)}\n\`\`\`\n`; comment += `
\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 }); \ No newline at end of file +# SPDX-License-Identifier: PMPL-1.0-or-later +# Hypatia Neurosymbolic CI/CD Security Scan +name: Hypatia Security Scan + +on: + 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: write + +jobs: + 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 += `
View findings\n\n`; + comment += `\`\`\`json\n${JSON.stringify(findings.slice(0, 10), null, 2)}\n\`\`\`\n`; + comment += `
\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 + }); From 0b0cc3495d65e65e52232a7ab333eaafd7cbcbd3 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Thu, 14 May 2026 12:03:06 +0100 Subject: [PATCH 3/3] ci(baseline): seed/refresh .hypatia-baseline.json from new workflow's first scan Captured from run 25856301390 on this branch. 32 critical+high entries accepted as pre-existing baseline. Net-new findings going forward will still fail the gate. Co-Authored-By: Claude Opus 4.7 --- .hypatia-baseline.json | 194 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 194 insertions(+) create mode 100644 .hypatia-baseline.json diff --git a/.hypatia-baseline.json b/.hypatia-baseline.json new file mode 100644 index 0000000..14645be --- /dev/null +++ b/.hypatia-baseline.json @@ -0,0 +1,194 @@ +[ + { + "severity": "critical", + "rule_module": "cicd_rules", + "type": "banned_language_file", + "file": "tests/aspect/security_test.ts" + }, + { + "severity": "critical", + "rule_module": "cicd_rules", + "type": "banned_language_file", + "file": "tests/bench/graph_bench.ts" + }, + { + "severity": "critical", + "rule_module": "cicd_rules", + "type": "banned_language_file", + "file": "tests/e2e/graph_lifecycle_test.ts" + }, + { + "severity": "critical", + "rule_module": "cicd_rules", + "type": "banned_language_file", + "file": "tests/property/graph_properties_test.ts" + }, + { + "severity": "critical", + "rule_module": "cicd_rules", + "type": "banned_language_file", + "file": "tests/unit/evidence_graph_test.ts" + }, + { + "severity": "critical", + "rule_module": "security_errors", + "type": "secret_detected", + "file": ".gitlab-ci.yml" + }, + { + "severity": "critical", + "rule_module": "security_errors", + "type": "secret_detected", + "file": "bofig.trustfile.a2ml" + }, + { + "severity": "critical", + "rule_module": "security_errors", + "type": "secret_detected", + "file": "config/dev.exs" + }, + { + "severity": "critical", + "rule_module": "security_errors", + "type": "secret_detected", + "file": "config/test.exs" + }, + { + "severity": "critical", + "rule_module": "security_errors", + "type": "secret_detected", + "file": "docs/database-evaluation.md" + }, + { + "severity": "critical", + "rule_module": "security_errors", + "type": "secret_detected", + "file": "lib/evidence_graph/accounts.ex" + }, + { + "severity": "critical", + "rule_module": "security_errors", + "type": "secret_detected", + "file": "lib/evidence_graph/lithoglyph/client.ex" + }, + { + "severity": "critical", + "rule_module": "security_errors", + "type": "secret_detected", + "file": "lib/evidence_graph/zotero/client.ex" + }, + { + "severity": "critical", + "rule_module": "security_errors", + "type": "secret_detected", + "file": "test/evidence_graph/accounts_test.exs" + }, + { + "severity": "critical", + "rule_module": "security_errors", + "type": "secret_detected", + "file": "test/support/data_case.ex" + }, + { + "severity": "critical", + "rule_module": "workflow_audit", + "type": "actions_expression_injection", + "file": "boj-build.yml" + }, + { + "severity": "critical", + "rule_module": "workflow_audit", + "type": "actions_expression_injection", + "file": "hypatia-scan.yml" + }, + { + "severity": "critical", + "rule_module": "workflow_audit", + "type": "actions_expression_injection", + "file": "mirror.yml" + }, + { + "severity": "critical", + "rule_module": "workflow_audit", + "type": "actions_expression_injection", + "file": "quality.yml" + }, + { + "severity": "high", + "rule_module": "cicd_rules", + "type": "missing_requirement", + "file": ".github/dependabot.yml" + }, + { + "severity": "high", + "rule_module": "cicd_rules", + "type": "missing_requirement", + "file": ".github/workflows/scorecard.yml" + }, + { + "severity": "high", + "rule_module": "cicd_rules", + "type": "missing_requirement", + "file": "permissions: read-all" + }, + { + "severity": "high", + "rule_module": "code_safety", + "type": "js_innerhtml", + "file": "assets/js/hooks/evidence_graph_hook.js" + }, + { + "severity": "high", + "rule_module": "code_safety", + "type": "js_innerhtml", + "file": "assets/js/hooks/prompt_radar_hook.js" + }, + { + "severity": "high", + "rule_module": "code_safety", + "type": "js_innerhtml", + "file": "assets/js/hooks/timeline_hook.js" + }, + { + "severity": "high", + "rule_module": "code_safety", + "type": "js_innerhtml", + "file": "assets/vendor/d3.v7.min.js" + }, + { + "severity": "high", + "rule_module": "git_state", + "type": "GS005", + "file": "." + }, + { + "severity": "high", + "rule_module": "workflow_audit", + "type": "download_then_run", + "file": "mirror.yml" + }, + { + "severity": "high", + "rule_module": "workflow_audit", + "type": "npermissions_typo", + "file": "elixir-ci.yml" + }, + { + "severity": "high", + "rule_module": "workflow_audit", + "type": "npermissions_typo", + "file": "rescript-deno-ci.yml" + }, + { + "severity": "high", + "rule_module": "workflow_audit", + "type": "unsafe_curl_payload", + "file": "boj-build.yml" + }, + { + "severity": "high", + "rule_module": "workflow_audit", + "type": "unsafe_curl_payload", + "file": "hypatia-scan.yml" + } +]