|
| 1 | +# SPDX-License-Identifier: PMPL-1.0-or-later |
| 2 | +# Gap Matrix — alerts × hypatia × bots × directives |
| 3 | +# Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) |
| 4 | +# |
| 5 | +# Scope: GitHub source-of-truth only (mirror-side sweep is PARKED — see |
| 6 | +# ~/Desktop/AI-WORK.md §0, requires Atlassian + GitLab tokens). |
| 7 | +# When the mirror sweep runs, a sibling gap-matrix-mirror-*.a2ml |
| 8 | +# will cover the delta-vs-GitHub column. |
| 9 | +# |
| 10 | +# Purpose: identify where a GitHub security signal does NOT currently |
| 11 | +# have end-to-end coverage through Hypatia rules + gitbot-fleet |
| 12 | +# + bot directives. Each gap row is an actionable wiring task. |
| 13 | +# |
| 14 | +# Method: structural matrix first (signal → rule → bot → directive), |
| 15 | +# then concrete-alert sample from 5 representative repos (2026-04-17). |
| 16 | + |
| 17 | +@abstract: |
| 18 | +Cross-reference of GitHub security signals against Hypatia rule coverage, |
| 19 | +gitbot-fleet action capability, and bot directive presence. Output row = |
| 20 | +one signal × one repo; gap = missing coverage at any stage of the pipeline. |
| 21 | +@end |
| 22 | + |
| 23 | +## Pipeline stages |
| 24 | + |
| 25 | +[[stages]] |
| 26 | +id = "signal" |
| 27 | +description = "GitHub security signal (Dependabot, CodeQL, secret-scanning, workflow)" |
| 28 | + |
| 29 | +[[stages]] |
| 30 | +id = "rule" |
| 31 | +description = "Hypatia rule module that classifies / routes the signal" |
| 32 | + |
| 33 | +[[stages]] |
| 34 | +id = "bot" |
| 35 | +description = "gitbot-fleet bot that can take a physical action (PR, comment, patch)" |
| 36 | + |
| 37 | +[[stages]] |
| 38 | +id = "directive" |
| 39 | +description = "Bot directive in .machine_readable/bot_directives/<bot>.a2ml governing per-repo behaviour" |
| 40 | + |
| 41 | +## Structural matrix — signal → rule → bot → directive |
| 42 | + |
| 43 | +[[coverage]] |
| 44 | +signal_type = "dependabot.alert.open" |
| 45 | +hypatia_rule = "Hypatia.Rules.DependabotAlerts (DA001-DA008, classify_alert, to_recipe, fixes_from_alerts)" |
| 46 | +dispatch_clause = "FleetDispatcher.dispatch_routed_action({:dependabot_fix, recipe, pattern}) — wired commit dd64722" |
| 47 | +bot = "rhodibot (substitute tier — PR open) | sustainabot (control tier — advisory) | rhodibot (eliminate tier — auto-dispatch via Fleet)" |
| 48 | +directive_coverage = "present in all 44 migrated repos post-2026-04-17 A2ML sweep" |
| 49 | +status = "wired" |
| 50 | +notes = "Full pipeline: alert → classify → safety-triangle tier → dispatch → bot action → (Kin Gate + rate limiter) → PR/comment" |
| 51 | + |
| 52 | +[[coverage]] |
| 53 | +signal_type = "dependabot.alert.dismissed" |
| 54 | +hypatia_rule = "classify_alert → :control tier (respects human dismissal)" |
| 55 | +dispatch_clause = "control → sustainabot advisory only" |
| 56 | +bot = "sustainabot" |
| 57 | +directive_coverage = "yes" |
| 58 | +status = "wired" |
| 59 | +notes = "Explicit no-op on dismissed/fixed alerts — safety triangle respects human acceptance of risk." |
| 60 | + |
| 61 | +[[coverage]] |
| 62 | +signal_type = "codeql.finding" |
| 63 | +hypatia_rule = "Hypatia.Rules.CodeSafety (partial — covers SHA-pinning, language policy, secret detection)" |
| 64 | +dispatch_clause = "missing — no FleetDispatcher clause routes CodeQL alerts" |
| 65 | +bot = "cipherbot (for crypto findings) | panicbot (for unsafe patterns) | rhodibot (for version pins)" |
| 66 | +directive_coverage = "partial — cipherbot.a2ml and panicbot.a2ml present; no CodeQL-specific directive schema" |
| 67 | +status = "GAP" |
| 68 | +notes = "CodeQL alerts visible via `gh api repos/.../code-scanning/alerts` but no Hypatia rule currently consumes them. Symmetric work to DA001-DA008 pending." |
| 69 | + |
| 70 | +[[coverage]] |
| 71 | +signal_type = "secret_scanning.alert" |
| 72 | +hypatia_rule = "Hypatia.Rules.SecurityErrors.detect_secrets/1 — detection only, no alert-listing" |
| 73 | +dispatch_clause = "missing" |
| 74 | +bot = "cipherbot would be the natural handler" |
| 75 | +directive_coverage = "cipherbot.a2ml present but no secret-scanning-alert schema" |
| 76 | +status = "GAP" |
| 77 | +notes = "GitHub secret-scanning alerts not consumed by Hypatia. Cipherbot has the capability (rotate/revoke workflows exist) but no trigger path." |
| 78 | + |
| 79 | +[[coverage]] |
| 80 | +signal_type = "workflow.failure (CI red)" |
| 81 | +hypatia_rule = "Hypatia.Rules.WorkflowAudit + Hypatia.Rules.CICDRules" |
| 82 | +dispatch_clause = "FleetDispatcher dispatches to panicbot / rhodibot for SHA-pin drift + workflow repair" |
| 83 | +bot = "rhodibot (SHA-pin updates) | panicbot (test-failure triage) | finishingbot (stale-PR pruning)" |
| 84 | +directive_coverage = "rhodibot.a2ml, panicbot.a2ml, finishingbot.a2ml present" |
| 85 | +status = "wired" |
| 86 | +notes = "Note: GitHub Actions billing freeze (AI-WORK.md §0 blocker 2026-04-17) blinds this signal — cannot distinguish CI reds from billing stalls until resolved." |
| 87 | + |
| 88 | +[[coverage]] |
| 89 | +signal_type = "scorecard.score_drop" |
| 90 | +hypatia_rule = "Hypatia.Rules.ScorecardCompliance" |
| 91 | +dispatch_clause = "missing — rule reads scorecard but no dispatch to auto-remediate drops" |
| 92 | +bot = "rhodibot (for branch-protection / SHA-pin fixes) | sustainabot (for maintenance-signal drops)" |
| 93 | +directive_coverage = "yes for both bots" |
| 94 | +status = "GAP" |
| 95 | +notes = "Rule reports score but does not route to remediation. Wiring comparable to DA005-DA008 would be symmetric work." |
| 96 | + |
| 97 | +[[coverage]] |
| 98 | +signal_type = "dependabot.pr.open (pull_request)" |
| 99 | +hypatia_rule = "none — Dependabot-generated PRs not inspected" |
| 100 | +dispatch_clause = "missing" |
| 101 | +bot = "rhodibot could merge safe ones; finishingbot could prune stale ones" |
| 102 | +directive_coverage = "neither has a Dependabot-PR-specific directive schema" |
| 103 | +status = "GAP" |
| 104 | +notes = "Many Dependabot PRs sit open across the estate — a future DA009 rule + dispatch clause would close this loop." |
| 105 | + |
| 106 | +## Sampled concrete data — 5 repos, 2026-04-17 |
| 107 | + |
| 108 | +[[sample]] |
| 109 | +repo = "hyperpolymath/standards" |
| 110 | +total_open_alerts = 5 |
| 111 | +distinct_advisories = 3 |
| 112 | +classification_summary = "5× :eliminate (rust patch bumps, confidence ≥0.95) | 0× :substitute | 0× :control" |
| 113 | +notes = "All 5 are Rust low-severity patch-in-0.x-series. Post-wiring (commit dd64722), these should auto-route to rhodibot via dispatch_eliminate_via_fleet." |
| 114 | + |
| 115 | +[[sample.advisories]] |
| 116 | +ghsa = "GHSA-965h-392x-2mh5" |
| 117 | +package = "rustls-webpki" |
| 118 | +ecosystem = "rust" |
| 119 | +vulnerable_range = ">= 0.101.0, < 0.103.12" |
| 120 | +first_patched = "0.103.12" |
| 121 | +bump_class = "patch (within 0.103 series)" |
| 122 | +hypatia_classification = ":eliminate — strict-semver ecosystem + patch bump → confidence ≥ 0.95" |
| 123 | +expected_route = "Fleet → rhodibot PR (auto-executable tier)" |
| 124 | + |
| 125 | +[[sample.advisories]] |
| 126 | +ghsa = "GHSA-xgp8-3hg3-c2mh" |
| 127 | +package = "rustls-webpki" |
| 128 | +ecosystem = "rust" |
| 129 | +vulnerable_range = ">= 0.101.0, < 0.103.12" |
| 130 | +first_patched = "0.103.12" |
| 131 | +bump_class = "patch" |
| 132 | +hypatia_classification = ":eliminate" |
| 133 | +expected_route = "Fleet → rhodibot PR" |
| 134 | + |
| 135 | +[[sample.advisories]] |
| 136 | +ghsa = "GHSA-cq8v-f236-94qc" |
| 137 | +package = "rand" |
| 138 | +ecosystem = "rust" |
| 139 | +vulnerable_range = ">= 0.7.0, < 0.9.3" |
| 140 | +first_patched = "0.9.3" |
| 141 | +bump_class = "major (pre-1.0 — 0.7 → 0.9 crosses minor under 0.x semantics)" |
| 142 | +hypatia_classification = ":substitute — major bump flagged for review" |
| 143 | +expected_route = "rhodibot PR with requires_human = true" |
| 144 | + |
| 145 | +[[sample]] |
| 146 | +repo = "hyperpolymath/gitbot-fleet" |
| 147 | +total_open_alerts = 18 |
| 148 | +distinct_advisories = 3 |
| 149 | +classification_summary = "same 3 GHSAs as standards, multiplied across 6 workspace crates each" |
| 150 | +notes = "18 = 3 advisories × 6 crates. Fleet dispatch should deduplicate at the advisory level (one PR per advisory, not per Cargo.lock occurrence). Worth verifying the dedup logic in fixes_from_alerts." |
| 151 | + |
| 152 | +[[sample]] |
| 153 | +repo = "hyperpolymath/boj-server" |
| 154 | +total_open_alerts = 3 |
| 155 | +distinct_advisories = 3 |
| 156 | +classification_summary = "same 3 GHSAs, single occurrence each" |
| 157 | +notes = "Clean 3-for-3 pattern. Expected route identical to standards." |
| 158 | + |
| 159 | +[[sample]] |
| 160 | +repo = "hyperpolymath/idaptik" |
| 161 | +total_open_alerts = 1 |
| 162 | +distinct_advisories = 1 |
| 163 | +classification_summary = "1× rand GHSA-cq8v-f236-94qc :substitute" |
| 164 | +notes = "Single major-bump item. Will require maintainer review post-rhodibot PR." |
| 165 | + |
| 166 | +[[sample]] |
| 167 | +repo = "hyperpolymath/hypatia" |
| 168 | +total_open_alerts = 0 |
| 169 | +distinct_advisories = 0 |
| 170 | +classification_summary = "n/a" |
| 171 | +notes = "Clean — Hypatia itself has no open Dependabot alerts. Expected for a pure-Elixir repo with few Rust dependencies." |
| 172 | + |
| 173 | +## Identified wiring gaps (actionable follow-up) |
| 174 | + |
| 175 | +[[gap]] |
| 176 | +id = "gap-codeql-to-hypatia" |
| 177 | +priority = "medium" |
| 178 | +description = "CodeQL alerts not ingested by any Hypatia rule" |
| 179 | +proposed_rule = "Hypatia.Rules.CodeqlAlerts — symmetric to DependabotAlerts; classify by severity + rule-id, dispatch via FleetDispatcher" |
| 180 | +proposed_dispatch = "{:codeql_fix, recipe, pattern}" |
| 181 | +blocked_by = "none — design work only" |
| 182 | + |
| 183 | +[[gap]] |
| 184 | +id = "gap-secret-scanning-to-cipherbot" |
| 185 | +priority = "high" |
| 186 | +description = "Secret-scanning alerts visible on GitHub but no Hypatia rule consumes them" |
| 187 | +proposed_rule = "Hypatia.Rules.SecretScanning — list alerts, classify by secret-type, always :control tier (no auto-revoke)" |
| 188 | +proposed_dispatch = "{:secret_rotation_advisory, alert, repo} → cipherbot advisory-only" |
| 189 | +blocked_by = "none — safety-critical so must be control-tier only, no auto-execute" |
| 190 | + |
| 191 | +[[gap]] |
| 192 | +id = "gap-scorecard-auto-remediation" |
| 193 | +priority = "low" |
| 194 | +description = "ScorecardCompliance reports but does not route to rhodibot/sustainabot to remediate drops" |
| 195 | +proposed_rule = "extend Hypatia.Rules.ScorecardCompliance with classify_drop/1 + to_recipe/1" |
| 196 | +proposed_dispatch = "{:scorecard_fix, drop, remediation} → rhodibot PR" |
| 197 | +blocked_by = "none" |
| 198 | + |
| 199 | +[[gap]] |
| 200 | +id = "gap-dependabot-pr-lifecycle" |
| 201 | +priority = "medium" |
| 202 | +description = "Dependabot-generated PRs accumulate unmerged; no auto-merge safe ones, no auto-prune stale ones" |
| 203 | +proposed_rule = "Hypatia.Rules.DependabotAlerts extension DA009 — classify_dependabot_pr/1" |
| 204 | +proposed_dispatch = "{:dependabot_pr_merge, pr} if safe → rhodibot | {:dependabot_pr_prune, pr} if stale → finishingbot" |
| 205 | +blocked_by = "none" |
| 206 | + |
| 207 | +[[gap]] |
| 208 | +id = "gap-mirror-side-discrepancy" |
| 209 | +priority = "deferred — PARKED" |
| 210 | +description = "Unknown delta between GitHub-side findings and GitLab+Bitbucket mirrors" |
| 211 | +proposed_rule = "Hypatia.Rules.MirrorDelta — compare source-of-truth GitHub findings against mirror-visible findings, flag discrepancies" |
| 212 | +proposed_dispatch = "{:mirror_advisory_sync, delta} → sustainabot advisory" |
| 213 | +blocked_by = "Atlassian API token refresh + GitLab PAT (see ~/Desktop/AI-WORK.md §0)" |
| 214 | + |
| 215 | +## Coverage summary |
| 216 | + |
| 217 | +[[summary]] |
| 218 | +pipeline_stage = "signal types monitored" |
| 219 | +count_wired = 3 |
| 220 | +count_gap = 4 |
| 221 | +gap_list = ["codeql", "secret_scanning", "scorecard_drop", "dependabot_pr_lifecycle"] |
| 222 | + |
| 223 | +[[summary]] |
| 224 | +pipeline_stage = "bots with directive coverage" |
| 225 | +count_wired = 11 |
| 226 | +count_gap = 0 |
| 227 | +notes = "All 11 gitbot-fleet bots have .a2ml directives post-2026-04-17 migration." |
| 228 | + |
| 229 | +[[summary]] |
| 230 | +pipeline_stage = "repos sampled" |
| 231 | +count = 5 |
| 232 | +total_open_alerts = 27 |
| 233 | +distinct_advisories = 3 |
| 234 | +auto_routable_eliminate = 23 |
| 235 | +requires_human_substitute = 4 |
| 236 | +control_only = 0 |
| 237 | + |
| 238 | +## Next-action recommendation |
| 239 | + |
| 240 | +[[next_action]] |
| 241 | +id = "verify-dedup" |
| 242 | +owner = "CLAUDE" |
| 243 | +description = "Verify fixes_from_alerts dedupes per-advisory so rhodibot opens one PR per GHSA, not one per Cargo.lock hit. Gitbot-fleet's 18 alerts / 3 advisories / 6 crates is the test case." |
| 244 | +effort = "30 min" |
| 245 | + |
| 246 | +[[next_action]] |
| 247 | +id = "draft-codeql-rule" |
| 248 | +owner = "CLAUDE" |
| 249 | +description = "Draft Hypatia.Rules.CodeqlAlerts module mirroring DependabotAlerts structure. Non-invasive — compile + test in isolation before wiring." |
| 250 | +effort = "2-3 hours" |
| 251 | + |
| 252 | +[[next_action]] |
| 253 | +id = "draft-secret-scanning-rule" |
| 254 | +owner = "CLAUDE" |
| 255 | +description = "Draft Hypatia.Rules.SecretScanning — strictly control-tier. Per user policy, no auto-revoke or auto-rotate; cipherbot advisory-only." |
| 256 | +effort = "1-2 hours" |
| 257 | + |
| 258 | +[[next_action]] |
| 259 | +id = "resume-mirror-sweep" |
| 260 | +owner = "JONATHAN" |
| 261 | +description = "Refresh Atlassian API token + obtain GitLab PAT; drop both in ~/.netrc. Unblocks the MirrorDelta rule design + closes gap-mirror-side-discrepancy." |
| 262 | +effort = "5 min after token in hand" |
0 commit comments