|
2 | 2 | ;; SPDX-FileCopyrightText: 2025 Jonathan D.A. Jewell |
3 | 3 | ;; ERROR-CATALOG.scm - Machine-readable catalog of common repo security errors |
4 | 4 | ;; Format: Guile Scheme (homoiconic, parseable by robot-repo-cleaner) |
5 | | -;; Updated: 2025-12-15 |
| 5 | +;; Updated: 2026-07-29 |
6 | 6 |
|
7 | 7 | (define error-catalog |
8 | 8 | '((metadata |
9 | 9 | (format-version . "1.0") |
10 | | - (schema-version . "2025-12-15") |
| 10 | + (schema-version . "2026-07-29") |
11 | 11 | (purpose . "Learning rules for propagating fixes across repositories") |
12 | 12 | (generator . "robot-repo-bot/Claude analysis")) |
13 | 13 |
|
|
309 | 309 | (permissions . "~114 workflows") |
310 | 310 | (spdx-headers . "~114 workflows"))))) |
311 | 311 |
|
| 312 | + ;;========================================================================= |
| 313 | + ;; ERROR CATEGORY: UNSATISFIABLE MERGE GATES |
| 314 | + ;; Added 2026-07-29. Every entry below was MEASURED across all 418 active |
| 315 | + ;; estate repos, not inferred. They share one shape: the board reads green |
| 316 | + ;; or blocked for a reason no workflow edit can address, because the check |
| 317 | + ;; never ran at all. |
| 318 | + ;;========================================================================= |
| 319 | + (error-type |
| 320 | + (id . "ERR-GATE-001") |
| 321 | + (name . "phantom-required-context") |
| 322 | + (severity . "critical") |
| 323 | + (category . "unsatisfiable-gate") |
| 324 | + (description . "A required status-check context that no job ever emits. Every PR sits permanently BLOCKED, and because an absent check produces NO check-run, `gh pr checks` shows nothing wrong.") |
| 325 | + |
| 326 | + (detection |
| 327 | + (method . "set-difference") |
| 328 | + (sources . ("classic branch protection" "ACTIVE branch-target RULESETS")) |
| 329 | + (sample . "PR HEAD SHAs, not the default branch") |
| 330 | + (condition . "required-context absent from observed check-runs AND commit statuses")) |
| 331 | + |
| 332 | + (measured |
| 333 | + (date . "2026-07-29") |
| 334 | + (ruleset-context-rows . 236) |
| 335 | + (note . "Rulesets hold most estate requirements; classic protection alone sees almost none.")) |
| 336 | + |
| 337 | + (variants |
| 338 | + ("invented name -- hypatia-scan, emitted by nothing" |
| 339 | + "case mismatch -- required `codeql`, real job is `CodeQL`" |
| 340 | + "filename as context -- required `.github/dependabot.yml`")) |
| 341 | + |
| 342 | + (remediation |
| 343 | + (action . "remove-context-or-rename-to-match-a-real-job") |
| 344 | + (api . "DELETE /repos/{o}/{r}/branches/{b}/protection/required_status_checks/contexts") |
| 345 | + (warning . "Use that surgical sub-resource. A full PUT .../protection REPLACES the object and silently drops required_signatures / enforce_admins.") |
| 346 | + (warning-2 . "In a ruleset, emptying required_status_checks returns HTTP 422 -- the whole rule must be dropped. A ruleset PUT also replaces, so rebuild from a live GET and preserve bypass_actors."))) |
| 347 | + |
| 348 | + (error-type |
| 349 | + (id . "ERR-GATE-002") |
| 350 | + (name . "required-context-never-runs-on-pull-request") |
| 351 | + (severity . "critical") |
| 352 | + (category . "unsatisfiable-gate") |
| 353 | + (description . "The producing job exists and is healthy, but is push-, schedule- or dynamic-triggered and never runs on pull_request. It therefore cannot satisfy a PR gate however green it looks on main.") |
| 354 | + |
| 355 | + (detection |
| 356 | + (method . "compare-two-surfaces") |
| 357 | + (condition . "context PRESENT on default-branch HEAD but ABSENT from every sampled PR head")) |
| 358 | + |
| 359 | + (measured |
| 360 | + (date . "2026-07-29") |
| 361 | + (canonical-case . "Dependabot") |
| 362 | + (emitter . "GitHub's managed dynamic/dependabot/dependabot-updates runner") |
| 363 | + (check-runs-on-main . 10) |
| 364 | + (check-runs-on-pr-head . 0) |
| 365 | + (repos-affected . 25) |
| 366 | + (note . "Sampling main scores this HEALTHY -- a false negative. It is the likely root of the estate's --admin merge drift.")) |
| 367 | + |
| 368 | + (remediation |
| 369 | + (action . "add-pull_request-trigger-or-stop-requiring-on-prs"))) |
| 370 | + |
| 371 | + (error-type |
| 372 | + (id . "ERR-GATE-003") |
| 373 | + (name . "dead-action-pin") |
| 374 | + (severity . "critical") |
| 375 | + (category . "unsatisfiable-gate") |
| 376 | + (description . "A `uses:` ref pointing at a DELETED action repository. Actions resolves refs at RUN time, and an unresolvable ref produces NO check run -- not a red one. The repo can be fully green with its scanning entirely absent.") |
| 377 | + |
| 378 | + (detection |
| 379 | + (method . "resolve-every-uses-ref-against-the-api") |
| 380 | + (condition . "repository 404, or SHA not found")) |
| 381 | + |
| 382 | + (measured |
| 383 | + (date . "2026-07-29") |
| 384 | + (repos . 69) |
| 385 | + (references . 135) |
| 386 | + (dead . ("hyperpolymath/a2ml-validate-action" "hyperpolymath/k9-validate-action")) |
| 387 | + (live . ("hyperpolymath/a2ml-ecosystem/validate-action" "hyperpolymath/k9-ecosystem/validate-action"))) |
| 388 | + |
| 389 | + (remediation |
| 390 | + (action . "REPOINT-not-vendor") |
| 391 | + (rationale . "Vendoring the script into each consumer creates one drifting copy per repo -- this estate's most recurring failure mode. Repointing is one line per file.") |
| 392 | + (warning . "Actions does NOT follow repository renames in `uses:`; a rename is a genuine break."))) |
| 393 | + |
| 394 | + (error-type |
| 395 | + (id . "ERR-GATE-004") |
| 396 | + (name . "actions-policy-refuses-every-reusable") |
| 397 | + (severity . "critical") |
| 398 | + (category . "unsatisfiable-gate") |
| 399 | + (description . "allowed_actions=selected with an EMPTY patterns_allowed permits only GitHub-owned and verified actions. Every org reusable is refused at parse time, so runs die as startup_failure with ZERO jobs and no check run.") |
| 400 | + |
| 401 | + (detection |
| 402 | + (method . "repository-settings") |
| 403 | + (api . "GET /repos/{o}/{r}/actions/permissions and .../selected-actions") |
| 404 | + (condition . "allowed_actions == selected AND patterns_allowed is empty")) |
| 405 | + |
| 406 | + (measured |
| 407 | + (date . "2026-07-29") |
| 408 | + (scanned . 418) |
| 409 | + (broken . 2) |
| 410 | + (proof . "governance.yml went startup_failure/0 jobs -> success/10 jobs with NO file change -- settings only.")) |
| 411 | + |
| 412 | + (remediation |
| 413 | + (action . "populate-patterns_allowed-or-set-allowed_actions-all") |
| 414 | + (warning . "This is a SETTINGS fault. No workflow edit can fix it, and a startup_failure run cannot be re-run -- it has no jobs. Verify by dispatching a fresh run and asserting jobs > 0."))) |
| 415 | + |
| 416 | + (error-type |
| 417 | + (id . "ERR-GATE-005") |
| 418 | + (name . "empty-jobs-map") |
| 419 | + (severity . "high") |
| 420 | + (category . "unsatisfiable-gate") |
| 421 | + (description . "A workflow whose `jobs:` key is present but has no uncommented job. An empty jobs map is invalid, so the run fails with zero jobs, permanently, on every matching event.") |
| 422 | + |
| 423 | + (detection |
| 424 | + (method . "parse-workflow") |
| 425 | + (condition . "`jobs:` present AND no uncommented ` <name>:` beneath it")) |
| 426 | + |
| 427 | + (measured |
| 428 | + (date . "2026-07-29") |
| 429 | + (repos . 20) |
| 430 | + (file . "e2e.yml -- an un-instantiated scaffold, every job block commented out")) |
| 431 | + |
| 432 | + (remediation |
| 433 | + (action . "instantiate-a-real-job-or-delete-the-workflow") |
| 434 | + (warning . "Do NOT write a job that passes without testing anything -- that converts a dead gate into a fake one, which is worse because it is read as evidence."))) |
| 435 | + |
| 436 | + |
312 | 437 | ;; End of ERROR-CATALOG.scm |
0 commit comments