github-security-report 0.15.0__tar.gz → 0.17.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. {github_security_report-0.15.0 → github_security_report-0.17.0}/PKG-INFO +209 -14
  2. {github_security_report-0.15.0 → github_security_report-0.17.0}/README.md +206 -11
  3. {github_security_report-0.15.0 → github_security_report-0.17.0}/pyproject.toml +4 -4
  4. {github_security_report-0.15.0 → github_security_report-0.17.0}/src/github_security_report/_version.py +2 -2
  5. github_security_report-0.17.0/src/github_security_report/categories/__init__.py +92 -0
  6. github_security_report-0.17.0/src/github_security_report/categories/keys.py +76 -0
  7. github_security_report-0.17.0/src/github_security_report/categories/signals.py +113 -0
  8. github_security_report-0.17.0/src/github_security_report/categories/tables.py +244 -0
  9. {github_security_report-0.15.0 → github_security_report-0.17.0}/src/github_security_report/cli/app.py +35 -5
  10. {github_security_report-0.15.0 → github_security_report-0.17.0}/src/github_security_report/cli/modes.py +97 -4
  11. {github_security_report-0.15.0 → github_security_report-0.17.0}/src/github_security_report/cli/outputs.py +55 -12
  12. {github_security_report-0.15.0 → github_security_report-0.17.0}/src/github_security_report/cli/publish.py +4 -0
  13. {github_security_report-0.15.0 → github_security_report-0.17.0}/src/github_security_report/cli/serialise.py +4 -0
  14. github_security_report-0.17.0/src/github_security_report/client/codeql_parsers.py +129 -0
  15. {github_security_report-0.15.0 → github_security_report-0.17.0}/src/github_security_report/client/org_reads.py +5 -0
  16. {github_security_report-0.15.0 → github_security_report-0.17.0}/src/github_security_report/client/parsers.py +5 -0
  17. {github_security_report-0.15.0 → github_security_report-0.17.0}/src/github_security_report/client/queries.py +12 -3
  18. {github_security_report-0.15.0 → github_security_report-0.17.0}/src/github_security_report/client/reads.py +143 -1
  19. github_security_report-0.17.0/src/github_security_report/client/writes.py +284 -0
  20. github_security_report-0.17.0/src/github_security_report/codeql/__init__.py +50 -0
  21. github_security_report-0.17.0/src/github_security_report/codeql/cleanup.py +123 -0
  22. github_security_report-0.17.0/src/github_security_report/codeql/facts.py +277 -0
  23. github_security_report-0.17.0/src/github_security_report/codeql/tables.py +253 -0
  24. {github_security_report-0.15.0 → github_security_report-0.17.0}/src/github_security_report/collect/__init__.py +6 -0
  25. github_security_report-0.17.0/src/github_security_report/collect/codeql.py +66 -0
  26. {github_security_report-0.15.0 → github_security_report-0.17.0}/src/github_security_report/collect/extras.py +28 -15
  27. {github_security_report-0.15.0 → github_security_report-0.17.0}/src/github_security_report/collect/org.py +19 -51
  28. {github_security_report-0.15.0 → github_security_report-0.17.0}/src/github_security_report/collect/protocols.py +15 -0
  29. github_security_report-0.17.0/src/github_security_report/collect/scoping.py +122 -0
  30. {github_security_report-0.15.0 → github_security_report-0.17.0}/src/github_security_report/config/loader.py +12 -0
  31. {github_security_report-0.15.0 → github_security_report-0.17.0}/src/github_security_report/config/models.py +27 -1
  32. {github_security_report-0.15.0 → github_security_report-0.17.0}/src/github_security_report/config/schema.py +63 -36
  33. {github_security_report-0.15.0 → github_security_report-0.17.0}/src/github_security_report/layout.py +14 -28
  34. {github_security_report-0.15.0 → github_security_report-0.17.0}/src/github_security_report/models.py +6 -0
  35. {github_security_report-0.15.0 → github_security_report-0.17.0}/src/github_security_report/ordering.py +13 -16
  36. {github_security_report-0.15.0 → github_security_report-0.17.0}/src/github_security_report/posture/__init__.py +2 -0
  37. {github_security_report-0.15.0 → github_security_report-0.17.0}/src/github_security_report/posture/enablement.py +29 -4
  38. {github_security_report-0.15.0 → github_security_report-0.17.0}/src/github_security_report/posture/facts.py +2 -0
  39. github_security_report-0.15.0/src/github_security_report/remediate.py → github_security_report-0.17.0/src/github_security_report/remediate/__init__.py +115 -101
  40. github_security_report-0.17.0/src/github_security_report/remediate/codeql.py +99 -0
  41. github_security_report-0.17.0/src/github_security_report/remediate/model.py +154 -0
  42. {github_security_report-0.15.0 → github_security_report-0.17.0}/src/github_security_report/render/html.py +29 -16
  43. {github_security_report-0.15.0 → github_security_report-0.17.0}/src/github_security_report/render/markdown.py +34 -22
  44. {github_security_report-0.15.0 → github_security_report-0.17.0}/src/github_security_report/render/slack.py +45 -30
  45. {github_security_report-0.15.0 → github_security_report-0.17.0}/src/github_security_report/render/terminal.py +68 -63
  46. {github_security_report-0.15.0 → github_security_report-0.17.0}/src/github_security_report/report/__init__.py +10 -0
  47. {github_security_report-0.15.0 → github_security_report-0.17.0}/src/github_security_report/report/aggregate.py +69 -1
  48. {github_security_report-0.15.0 → github_security_report-0.17.0}/src/github_security_report/report/display.py +47 -1
  49. {github_security_report-0.15.0 → github_security_report-0.17.0}/src/github_security_report/report/tables.py +63 -5
  50. {github_security_report-0.15.0 → github_security_report-0.17.0}/src/github_security_report/scope.py +69 -2
  51. {github_security_report-0.15.0 → github_security_report-0.17.0}/src/github_security_report/summary.py +50 -3
  52. github_security_report-0.15.0/src/github_security_report/categories.py +0 -384
  53. github_security_report-0.15.0/src/github_security_report/client/writes.py +0 -136
  54. {github_security_report-0.15.0 → github_security_report-0.17.0}/.gitignore +0 -0
  55. {github_security_report-0.15.0 → github_security_report-0.17.0}/LICENSE +0 -0
  56. {github_security_report-0.15.0 → github_security_report-0.17.0}/LICENSES/Apache-2.0.txt +0 -0
  57. {github_security_report-0.15.0 → github_security_report-0.17.0}/scripts/README.md +0 -0
  58. {github_security_report-0.15.0 → github_security_report-0.17.0}/src/github_security_report/__init__.py +0 -0
  59. {github_security_report-0.15.0 → github_security_report-0.17.0}/src/github_security_report/authors.py +0 -0
  60. {github_security_report-0.15.0 → github_security_report-0.17.0}/src/github_security_report/classify.py +0 -0
  61. {github_security_report-0.15.0 → github_security_report-0.17.0}/src/github_security_report/cli/__init__.py +0 -0
  62. {github_security_report-0.15.0 → github_security_report-0.17.0}/src/github_security_report/cli/__main__.py +0 -0
  63. {github_security_report-0.15.0 → github_security_report-0.17.0}/src/github_security_report/cli/boundary.py +0 -0
  64. {github_security_report-0.15.0 → github_security_report-0.17.0}/src/github_security_report/cli/options.py +0 -0
  65. {github_security_report-0.15.0 → github_security_report-0.17.0}/src/github_security_report/client/__init__.py +0 -0
  66. {github_security_report-0.15.0 → github_security_report-0.17.0}/src/github_security_report/client/alerts.py +0 -0
  67. {github_security_report-0.15.0 → github_security_report-0.17.0}/src/github_security_report/client/batch_errors.py +0 -0
  68. {github_security_report-0.15.0 → github_security_report-0.17.0}/src/github_security_report/client/copilot.py +0 -0
  69. {github_security_report-0.15.0 → github_security_report-0.17.0}/src/github_security_report/client/endpoints.py +0 -0
  70. {github_security_report-0.15.0 → github_security_report-0.17.0}/src/github_security_report/client/errors.py +0 -0
  71. {github_security_report-0.15.0 → github_security_report-0.17.0}/src/github_security_report/client/reviews.py +0 -0
  72. {github_security_report-0.15.0 → github_security_report-0.17.0}/src/github_security_report/client/transport.py +0 -0
  73. {github_security_report-0.15.0 → github_security_report-0.17.0}/src/github_security_report/collect/context.py +0 -0
  74. {github_security_report-0.15.0 → github_security_report-0.17.0}/src/github_security_report/collect/repo.py +0 -0
  75. {github_security_report-0.15.0 → github_security_report-0.17.0}/src/github_security_report/config/__init__.py +0 -0
  76. {github_security_report-0.15.0 → github_security_report-0.17.0}/src/github_security_report/config/order.py +0 -0
  77. {github_security_report-0.15.0 → github_security_report-0.17.0}/src/github_security_report/gating.py +0 -0
  78. {github_security_report-0.15.0 → github_security_report-0.17.0}/src/github_security_report/gitctx.py +0 -0
  79. {github_security_report-0.15.0 → github_security_report-0.17.0}/src/github_security_report/issues.py +0 -0
  80. {github_security_report-0.15.0 → github_security_report-0.17.0}/src/github_security_report/posture/releases.py +0 -0
  81. {github_security_report-0.15.0 → github_security_report-0.17.0}/src/github_security_report/pulls/__init__.py +0 -0
  82. {github_security_report-0.15.0 → github_security_report-0.17.0}/src/github_security_report/pulls/columns.py +0 -0
  83. {github_security_report-0.15.0 → github_security_report-0.17.0}/src/github_security_report/pulls/counting.py +0 -0
  84. {github_security_report-0.15.0 → github_security_report-0.17.0}/src/github_security_report/pulls/presentation.py +0 -0
  85. {github_security_report-0.15.0 → github_security_report-0.17.0}/src/github_security_report/pulls/table.py +0 -0
  86. {github_security_report-0.15.0 → github_security_report-0.17.0}/src/github_security_report/py.typed +0 -0
  87. {github_security_report-0.15.0 → github_security_report-0.17.0}/src/github_security_report/ranking.py +0 -0
  88. {github_security_report-0.15.0 → github_security_report-0.17.0}/src/github_security_report/render/__init__.py +0 -0
  89. {github_security_report-0.15.0 → github_security_report-0.17.0}/src/github_security_report/render/slack_limits.py +0 -0
  90. {github_security_report-0.15.0 → github_security_report-0.17.0}/src/github_security_report/report/signals.py +0 -0
  91. {github_security_report-0.15.0 → github_security_report-0.17.0}/src/github_security_report/rulesets.py +0 -0
  92. {github_security_report-0.15.0 → github_security_report-0.17.0}/src/github_security_report/runner.py +0 -0
  93. {github_security_report-0.15.0 → github_security_report-0.17.0}/src/github_security_report/secret_patterns.py +0 -0
  94. {github_security_report-0.15.0 → github_security_report-0.17.0}/src/github_security_report/severity.py +0 -0
  95. {github_security_report-0.15.0 → github_security_report-0.17.0}/src/github_security_report/templates/index.html.j2 +0 -0
  96. {github_security_report-0.15.0 → github_security_report-0.17.0}/src/github_security_report/templates/report.html.j2 +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: github-security-report
3
- Version: 0.15.0
3
+ Version: 0.17.0
4
4
  Summary: Security and quality reporting across GitHub organisations
5
5
  Project-URL: Homepage, https://github.com/lfreleng-actions/github-security-report-action
6
6
  Project-URL: Repository, https://github.com/lfreleng-actions/github-security-report-action
@@ -36,8 +36,8 @@ Requires-Dist: pytest-asyncio==1.4.0; extra == 'dev'
36
36
  Requires-Dist: pytest-cov==7.1.0; extra == 'dev'
37
37
  Requires-Dist: pytest==9.1.1; extra == 'dev'
38
38
  Requires-Dist: respx==0.23.1; extra == 'dev'
39
- Requires-Dist: ruff==0.16.5; extra == 'dev'
40
- Requires-Dist: syrupy==6.0.0; extra == 'dev'
39
+ Requires-Dist: ruff==0.16.8; extra == 'dev'
40
+ Requires-Dist: syrupy==6.1.1; extra == 'dev'
41
41
  Requires-Dist: types-jsonschema==4.26.0.20260518; extra == 'dev'
42
42
  Requires-Dist: types-pyyaml==6.0.12.20250915; extra == 'dev'
43
43
  Description-Content-Type: text/markdown
@@ -101,6 +101,33 @@ if you want to probe everything regardless.
101
101
  Further sections report **configuration posture** and **freshness** as plain
102
102
  tables (org mode):
103
103
 
104
+ - **CodeQL scan health** — two tables beneath **CodeQL: Results/Findings** (the
105
+ CodeQL alerts table), whose "Clean" says nothing about whether CodeQL is
106
+ still running:
107
+ - **Stale Configurations** lists every CodeQL configuration whose last scan
108
+ trails the default branch's newest commit by more than
109
+ `codeql_stale_days` (default 30) — the condition GitHub's tool status page
110
+ flags as *"Code Scanning results may be out of date"*. Each row names the
111
+ setup type (**Default**, GitHub-managed; or **Advanced**, a workflow in the
112
+ repository), the language, the last *successful* scan, and the cause:
113
+ analyses failing (a run that errors still uploads, but its results do not
114
+ count as a scan), default setup switched off, a workflow removed or
115
+ disabled, or an advanced workflow
116
+ **superseded** by default setup (GitHub rejects advanced CodeQL uploads
117
+ while default setup is on). An *orphaned* configuration will never scan
118
+ again; delete it from the tool status page once the live setup covers its
119
+ language.
120
+ - **Language Coverage** lists repositories where GitHub detects a
121
+ CodeQL-supported language that no current configuration scans — typically
122
+ an advanced workflow whose language matrix omits one the repository
123
+ contains, such as `actions` for its own workflows. On default setup GitHub
124
+ reports only the languages it is set to scan, so a detected language
125
+ deliberately left out of default setup is not visible to this check.
126
+
127
+ Both consider only repositories where CodeQL has run at least once; the rest
128
+ already appear in the Results/Findings not-enabled list. Finding a stale
129
+ configuration means reading each repository's whole CodeQL analysis history,
130
+ since GitHub cannot filter it by configuration.
104
131
  - **Dependabot** — three tables: repositories with vulnerability **alerts not
105
132
  enabled**, repositories with **security updates not enabled**, and ecosystems
106
133
  with no update `cooldown` configured (mandatory; any value passes).
@@ -118,6 +145,20 @@ tables (org mode):
118
145
  exposes no org-wide or GraphQL equivalent) and, like every other category,
119
146
  always collected; hide it with the `private_vulnerability_reporting` render
120
147
  toggle.
148
+ - **Auto-merge** — repositories where the **Allow auto-merge** setting is off,
149
+ so a pull request cannot be queued to merge itself once its requirements are
150
+ met. The setting only offers the option: an auto-merging pull request still
151
+ waits for the required checks, reviews and branch protections the repository
152
+ already enforces, so enabling it relaxes nothing. What it removes is the
153
+ interval between a change becoming mergeable and somebody noticing — the
154
+ window a reviewed dependency update sits in while the vulnerability it fixes
155
+ stays unpatched. Read from the batched GraphQL prefetch, so it costs no extra
156
+ request; hide it with the `auto_merge` render toggle.
157
+
158
+ The four enablement categories count the repositories with and without the
159
+ feature (plus any whose state could not be read), and by default name whichever
160
+ known list is shorter — see
161
+ [Enabled or disabled repository lists](#enabled-or-disabled-repository-lists).
121
162
 
122
163
  ## Operating modes
123
164
 
@@ -182,6 +223,7 @@ organisation and **Repository access** set to *All repositories*, then grant:
182
223
  | Secret scanning alerts | Open secret-scanning alerts, across every GitHub pattern category |
183
224
  | Issues | Open issues and their labels (GitHub Issues table) |
184
225
  | Administration | Dependabot enablement + security-updates status, and effective branch rules |
226
+ | Actions | Workflow state behind a stale CodeQL configuration |
185
227
 
186
228
  **Organization permissions:**
187
229
 
@@ -302,6 +344,7 @@ environment-variable name, never embedded.
302
344
  "include_test": false,
303
345
  "repo_min_age_days": 28,
304
346
  "release_max_age_days": 60,
347
+ "codeql_stale_days": 30,
305
348
  "graph_batch": 10,
306
349
  "order": { "style": "auto" }
307
350
  },
@@ -326,6 +369,17 @@ individual output. Set a value to `0` to remove the limit entirely and show
326
369
  every offender. Each can also be set at the CLI with `--top-n`,
327
370
  `--top-n-report`, `--top-n-cli`, and `--top-n-slack`.
328
371
 
372
+ `report.codeql_stale_days` (default `30`, minimum `1`) is the CodeQL
373
+ stale-configuration threshold: a configuration is stale once its last scan
374
+ trails the default branch's newest commit by more than that many days. It is
375
+ measured against the branch head rather than the clock, so a repository nobody
376
+ has pushed to is not flagged just for being quiet. GitHub does not publish the
377
+ threshold behind its own "may be out of date" warning; across the
378
+ `lfreleng-actions` estate healthy configurations trailed their head by at most
379
+ five days and abandoned ones by more than eighty, so the default sits well
380
+ clear of both. The same threshold decides which configurations count as current
381
+ for Language Coverage.
382
+
329
383
  The Releases / Tagging section has two independent freshness levers:
330
384
 
331
385
  - `report.repo_min_age_days` (default `28`, `0` = include all) is a grace
@@ -353,7 +407,25 @@ organisation, as `--top-n` does.
353
407
 
354
408
  The per-org `exclude` list removes repositories from analysis entirely; they are
355
409
  reported as **excluded** (distinct from "not enabled"), so an intentional
356
- exclusion is visible rather than silently dropped.
410
+ exclusion is visible rather than silently dropped. Entries match repository
411
+ names case-insensitively, as GitHub does.
412
+
413
+ Every category reports those exclusions beneath its counts, so an
414
+ organisation-wide list repeats under each one. `report.excluded_display`
415
+ governs that line on every surface:
416
+
417
+ | Value | Excluded line |
418
+ | ----- | ------------- |
419
+ | `always-show` (default) | Shown under every category |
420
+ | `always-hide` | Never shown |
421
+ | `conditional-hide` | Shown only for a category whose exclusions differ from the organisation's `exclude` list, and then in full |
422
+
423
+ Hiding the line changes nothing else: every count is unaffected, and the report
424
+ header's repository count already leaves excluded repositories out. So a clean
425
+ category reads `All Clean` once no Excluded line qualifies it. Today every
426
+ category reports the organisation's own list, so `conditional-hide` currently
427
+ hides the same lines `always-hide` does. It differs once a category excludes
428
+ repositories of its own. The `report.json` artifact always lists the exclusions.
357
429
 
358
430
  Archived and test repositories are excluded from analysis by default. Opt them
359
431
  back in with `report.include_archived` / `report.include_test` in the config, or
@@ -384,16 +456,17 @@ both true.
384
456
 
385
457
  The example above hides Zizmor on every surface, and keeps Releases / Tagging
386
458
  out of the terminal and Slack while still publishing it to the Markdown and HTML
387
- Pages output. The valid category keys are: `codeql`, `scorecard`, `zizmor`,
388
- `aislop`, `dependabot_alerts`, `secret_scanning`, `dependabot_alerts_enabled`,
389
- `dependabot_updates_enabled`, `dependabot_cooldown`, `releases`,
390
- `mutable_releases`, `private_vulnerability_reporting`, `github_issues`. Like the
391
- other `report`
392
- settings, `categories` can be set
393
- globally and overridden per organisation (overrides merge key-by-key, so
394
- flipping one output leaves the rest untouched). The machine-readable
395
- `report.json` artifact always contains the complete dataset, regardless of these
396
- toggles.
459
+ Pages output. The valid category keys are: `codeql`,
460
+ `codeql_stale_configurations`, `codeql_language_coverage`, `scorecard`,
461
+ `zizmor`, `aislop`, `dependabot_alerts`, `secret_scanning`,
462
+ `dependabot_alerts_enabled`, `dependabot_updates_enabled`,
463
+ `dependabot_cooldown`, `releases`, `mutable_releases`,
464
+ `private_vulnerability_reporting`, `auto_merge`, `github_issues`,
465
+ `pull_requests`, `pull_requests_assigned`. Like the other `report` settings,
466
+ `categories` can be set globally and overridden per organisation (overrides
467
+ merge key-by-key, so flipping one output leaves the rest untouched). The
468
+ machine-readable `report.json` artifact always contains the complete dataset,
469
+ regardless of these toggles.
397
470
 
398
471
  When several organisations share one Slack channel they render into a single
399
472
  combined digest, so the per-org Slack toggles are unioned for that channel: a
@@ -454,6 +527,52 @@ Slack-style ceiling, but they still apply their own row limits — only the
454
527
  GitHub Pages report whenever `pages_url` is set and short enough to render as a
455
528
  link.
456
529
 
530
+ ### Enabled or disabled repository lists
531
+
532
+ The four boolean feature categories — `dependabot_alerts_enabled`,
533
+ `dependabot_updates_enabled`, `private_vulnerability_reporting` and `auto_merge`
534
+ — sort each repository into one of three buckets: enabled, not enabled, or
535
+ **unknown** when the feature's state could not be read. The unknown bucket is
536
+ counted but never named, and never treated as either side, so the enabled and
537
+ not-enabled counts need not sum to the repositories analysed. Beneath the counts,
538
+ one of the two known sides is named; `report.repo_list` chooses which, on every
539
+ surface:
540
+
541
+ | Value | Names |
542
+ | ----- | ----- |
543
+ | `auto` (default) | Whichever list is shorter |
544
+ | `enabled` | The repositories with the feature on |
545
+ | `disabled` | The repositories with the feature off |
546
+
547
+ `auto` keeps a footer short whichever way an organisation leans: a feature
548
+ nearly every repository has lists its few holdouts, and one almost none have
549
+ lists its few adopters. Two cases resolve towards the actionable side: a tie
550
+ names the repositories without the feature, and so does a category where
551
+ *nothing* is enabled, since naming an empty list would print nothing where a
552
+ reader wants the repositories to fix. Both sides are always counted, whichever is
553
+ named.
554
+
555
+ A category can override the global value:
556
+
557
+ ```json
558
+ {
559
+ "report": {
560
+ "repo_list": "auto",
561
+ "categories": {
562
+ "dependabot_alerts_enabled": { "repo_list": "disabled" }
563
+ }
564
+ },
565
+ "organizations": [{ "name": "lfreleng-actions" }]
566
+ }
567
+ ```
568
+
569
+ Here Dependabot alerts always name the repositories to fix, and the other three
570
+ feature categories name whichever side is shorter. `repo_list` applies only to
571
+ those four categories; setting it on any other is a configuration error, since a
572
+ table with qualitative columns has no enabled list to name. These categories
573
+ render as a name list rather than a one-column table on every surface. The
574
+ `report.json` artifact is unaffected.
575
+
457
576
  ### Per-category row ordering
458
577
 
459
578
  Each table ships a sensible default ordering — largest backlog first, stalest
@@ -1401,8 +1520,30 @@ uvx github-security-report remediate \
1401
1520
  # Limit to specific categories (repeatable).
1402
1521
  uvx github-security-report remediate --org lfreleng-actions \
1403
1522
  --category codeql --category private_vulnerability_reporting --apply
1523
+
1524
+ # Limit to specific repositories, for any category (comma-separated and/or
1525
+ # repeatable; `name` in any configured org, or `owner/name`).
1526
+ uvx github-security-report remediate --org lfreleng-actions \
1527
+ --repos dependamerge,python-nss-ng --apply
1404
1528
  ```
1405
1529
 
1530
+ `--repos` narrows the run itself, not just its output. Everything done per
1531
+ repository (the feature probes, the batched prefetch, the CodeQL history walk
1532
+ and every write) covers only the named repositories, so a targeted run skips
1533
+ the bulk of a full one. The organisation-wide alert sweeps are the exception:
1534
+ each is a single org-bulk request, so its cost follows the organisation's open
1535
+ alert backlog rather than its repository count. It combines with
1536
+ `--category`. Every name is checked against every configured organisation
1537
+ before anything is written: a name that matches no repository (almost always
1538
+ a typo), or one that matches only repositories the configuration excludes
1539
+ (the `exclude` list, archived, fork, template or test), stops the run with
1540
+ exit code 2 and says which. A bare name excluded in one organisation but in
1541
+ scope in another runs against the in-scope one only; naming a repository never
1542
+ brings an excluded one back. A `--repos` value that names nothing also stops
1543
+ the run, rather than falling back to every repository. The run then collects
1544
+ exactly the repositories that check validated, without listing the
1545
+ organisation again, and prints `Limited to:` beneath its heading.
1546
+
1406
1547
  The remediable categories are the simple on/off features with a documented
1407
1548
  enablement endpoint:
1408
1549
 
@@ -1413,11 +1554,65 @@ enablement endpoint:
1413
1554
  | `dependabot_alerts_enabled` | Dependabot vulnerability alerts |
1414
1555
  | `dependabot_updates_enabled` | Dependabot security updates (plus alerts) |
1415
1556
  | `private_vulnerability_reporting` | Private vulnerability reporting |
1557
+ | `auto_merge` | The repository's "Allow auto-merge" setting |
1416
1558
 
1417
1559
  Qualitative findings (Scorecard, zizmor, open Dependabot alerts, cooldown,
1418
1560
  release freshness/mutability) are reported but not auto-remediated. Remediation
1419
1561
  is organisation-scoped (`--scope org`, the default and only supported scope).
1420
1562
 
1563
+ ### Cleaning up stale CodeQL configurations
1564
+
1565
+ One category is **destructive**, so it runs only when named. The no-argument
1566
+ run never includes it:
1567
+
1568
+ ```bash
1569
+ # Preview: which configurations would go, and which are refused and why.
1570
+ uvx github-security-report remediate --org lfreleng-actions \
1571
+ --category codeql_stale_configurations
1572
+
1573
+ # Apply.
1574
+ uvx github-security-report remediate --org lfreleng-actions \
1575
+ --category codeql_stale_configurations --apply
1576
+ ```
1577
+
1578
+ It acts on the rows of **CodeQL: Stale Configurations**, according to their
1579
+ cause:
1580
+
1581
+ <!-- markdownlint-disable MD013 -->
1582
+
1583
+ | Cause | Action |
1584
+ | ----- | ------ |
1585
+ | Default setup disabled; workflow removed; superseded by default setup; language removed from default setup | **Delete** the configuration: it can never upload again |
1586
+ | Workflow disabled after inactivity | **Re-enable** the workflow, restoring the scan |
1587
+ | Analyses failing, default setup changing state or not uploading, uploaded outside GitHub Actions, workflow disabled by hand, active but not uploading, or state unreadable | Reported only; needs a person |
1588
+
1589
+ <!-- markdownlint-enable MD013 -->
1590
+
1591
+ Deleting removes a configuration's analyses, which clears GitHub's *"Code
1592
+ Scanning results may be out of date"* warning but also removes its alert
1593
+ history. A deletion is therefore **refused**, in a dry run as in an apply,
1594
+ when:
1595
+
1596
+ - no current configuration scans its language: the stale results are then
1597
+ the only record of it, so add scanning for the language first (the refusal
1598
+ names it, and **CodeQL: Language Coverage** lists the gap);
1599
+ - it would help close an open alert. This is judged across **every deletion
1600
+ planned in the repository**, not one at a time: an alert held by two stale
1601
+ configurations would close if both went, so both are refused, although
1602
+ neither holds it alone. An alert also reported by a live configuration
1603
+ blocks nothing;
1604
+ - the repository's open alerts cannot be read.
1605
+
1606
+ Refusals are listed beside the work done and do not fail the run. Deletion is
1607
+ paced at one request a second across the whole run, as GitHub asks of
1608
+ mutating requests, and is resumable: an interrupted run's next pass picks up
1609
+ the remaining analyses. GitHub answers requests it will not authorise with
1610
+ `404`, so a deletion's `404` counts as already done only once the analysis
1611
+ also reads back as gone; a token that may not delete fails instead of
1612
+ reporting a cleanup that never happened. The token needs the classic `repo`
1613
+ scope. See [ADR-0005](docs/adr/0005-codeql-configuration-cleanup.md) for the
1614
+ reasoning.
1615
+
1421
1616
  ## Bulk Remediation Scripts
1422
1617
 
1423
1618
  The standalone scripts below predate the `remediate` subcommand and remain for
@@ -57,6 +57,33 @@ if you want to probe everything regardless.
57
57
  Further sections report **configuration posture** and **freshness** as plain
58
58
  tables (org mode):
59
59
 
60
+ - **CodeQL scan health** — two tables beneath **CodeQL: Results/Findings** (the
61
+ CodeQL alerts table), whose "Clean" says nothing about whether CodeQL is
62
+ still running:
63
+ - **Stale Configurations** lists every CodeQL configuration whose last scan
64
+ trails the default branch's newest commit by more than
65
+ `codeql_stale_days` (default 30) — the condition GitHub's tool status page
66
+ flags as *"Code Scanning results may be out of date"*. Each row names the
67
+ setup type (**Default**, GitHub-managed; or **Advanced**, a workflow in the
68
+ repository), the language, the last *successful* scan, and the cause:
69
+ analyses failing (a run that errors still uploads, but its results do not
70
+ count as a scan), default setup switched off, a workflow removed or
71
+ disabled, or an advanced workflow
72
+ **superseded** by default setup (GitHub rejects advanced CodeQL uploads
73
+ while default setup is on). An *orphaned* configuration will never scan
74
+ again; delete it from the tool status page once the live setup covers its
75
+ language.
76
+ - **Language Coverage** lists repositories where GitHub detects a
77
+ CodeQL-supported language that no current configuration scans — typically
78
+ an advanced workflow whose language matrix omits one the repository
79
+ contains, such as `actions` for its own workflows. On default setup GitHub
80
+ reports only the languages it is set to scan, so a detected language
81
+ deliberately left out of default setup is not visible to this check.
82
+
83
+ Both consider only repositories where CodeQL has run at least once; the rest
84
+ already appear in the Results/Findings not-enabled list. Finding a stale
85
+ configuration means reading each repository's whole CodeQL analysis history,
86
+ since GitHub cannot filter it by configuration.
60
87
  - **Dependabot** — three tables: repositories with vulnerability **alerts not
61
88
  enabled**, repositories with **security updates not enabled**, and ecosystems
62
89
  with no update `cooldown` configured (mandatory; any value passes).
@@ -74,6 +101,20 @@ tables (org mode):
74
101
  exposes no org-wide or GraphQL equivalent) and, like every other category,
75
102
  always collected; hide it with the `private_vulnerability_reporting` render
76
103
  toggle.
104
+ - **Auto-merge** — repositories where the **Allow auto-merge** setting is off,
105
+ so a pull request cannot be queued to merge itself once its requirements are
106
+ met. The setting only offers the option: an auto-merging pull request still
107
+ waits for the required checks, reviews and branch protections the repository
108
+ already enforces, so enabling it relaxes nothing. What it removes is the
109
+ interval between a change becoming mergeable and somebody noticing — the
110
+ window a reviewed dependency update sits in while the vulnerability it fixes
111
+ stays unpatched. Read from the batched GraphQL prefetch, so it costs no extra
112
+ request; hide it with the `auto_merge` render toggle.
113
+
114
+ The four enablement categories count the repositories with and without the
115
+ feature (plus any whose state could not be read), and by default name whichever
116
+ known list is shorter — see
117
+ [Enabled or disabled repository lists](#enabled-or-disabled-repository-lists).
77
118
 
78
119
  ## Operating modes
79
120
 
@@ -138,6 +179,7 @@ organisation and **Repository access** set to *All repositories*, then grant:
138
179
  | Secret scanning alerts | Open secret-scanning alerts, across every GitHub pattern category |
139
180
  | Issues | Open issues and their labels (GitHub Issues table) |
140
181
  | Administration | Dependabot enablement + security-updates status, and effective branch rules |
182
+ | Actions | Workflow state behind a stale CodeQL configuration |
141
183
 
142
184
  **Organization permissions:**
143
185
 
@@ -258,6 +300,7 @@ environment-variable name, never embedded.
258
300
  "include_test": false,
259
301
  "repo_min_age_days": 28,
260
302
  "release_max_age_days": 60,
303
+ "codeql_stale_days": 30,
261
304
  "graph_batch": 10,
262
305
  "order": { "style": "auto" }
263
306
  },
@@ -282,6 +325,17 @@ individual output. Set a value to `0` to remove the limit entirely and show
282
325
  every offender. Each can also be set at the CLI with `--top-n`,
283
326
  `--top-n-report`, `--top-n-cli`, and `--top-n-slack`.
284
327
 
328
+ `report.codeql_stale_days` (default `30`, minimum `1`) is the CodeQL
329
+ stale-configuration threshold: a configuration is stale once its last scan
330
+ trails the default branch's newest commit by more than that many days. It is
331
+ measured against the branch head rather than the clock, so a repository nobody
332
+ has pushed to is not flagged just for being quiet. GitHub does not publish the
333
+ threshold behind its own "may be out of date" warning; across the
334
+ `lfreleng-actions` estate healthy configurations trailed their head by at most
335
+ five days and abandoned ones by more than eighty, so the default sits well
336
+ clear of both. The same threshold decides which configurations count as current
337
+ for Language Coverage.
338
+
285
339
  The Releases / Tagging section has two independent freshness levers:
286
340
 
287
341
  - `report.repo_min_age_days` (default `28`, `0` = include all) is a grace
@@ -309,7 +363,25 @@ organisation, as `--top-n` does.
309
363
 
310
364
  The per-org `exclude` list removes repositories from analysis entirely; they are
311
365
  reported as **excluded** (distinct from "not enabled"), so an intentional
312
- exclusion is visible rather than silently dropped.
366
+ exclusion is visible rather than silently dropped. Entries match repository
367
+ names case-insensitively, as GitHub does.
368
+
369
+ Every category reports those exclusions beneath its counts, so an
370
+ organisation-wide list repeats under each one. `report.excluded_display`
371
+ governs that line on every surface:
372
+
373
+ | Value | Excluded line |
374
+ | ----- | ------------- |
375
+ | `always-show` (default) | Shown under every category |
376
+ | `always-hide` | Never shown |
377
+ | `conditional-hide` | Shown only for a category whose exclusions differ from the organisation's `exclude` list, and then in full |
378
+
379
+ Hiding the line changes nothing else: every count is unaffected, and the report
380
+ header's repository count already leaves excluded repositories out. So a clean
381
+ category reads `All Clean` once no Excluded line qualifies it. Today every
382
+ category reports the organisation's own list, so `conditional-hide` currently
383
+ hides the same lines `always-hide` does. It differs once a category excludes
384
+ repositories of its own. The `report.json` artifact always lists the exclusions.
313
385
 
314
386
  Archived and test repositories are excluded from analysis by default. Opt them
315
387
  back in with `report.include_archived` / `report.include_test` in the config, or
@@ -340,16 +412,17 @@ both true.
340
412
 
341
413
  The example above hides Zizmor on every surface, and keeps Releases / Tagging
342
414
  out of the terminal and Slack while still publishing it to the Markdown and HTML
343
- Pages output. The valid category keys are: `codeql`, `scorecard`, `zizmor`,
344
- `aislop`, `dependabot_alerts`, `secret_scanning`, `dependabot_alerts_enabled`,
345
- `dependabot_updates_enabled`, `dependabot_cooldown`, `releases`,
346
- `mutable_releases`, `private_vulnerability_reporting`, `github_issues`. Like the
347
- other `report`
348
- settings, `categories` can be set
349
- globally and overridden per organisation (overrides merge key-by-key, so
350
- flipping one output leaves the rest untouched). The machine-readable
351
- `report.json` artifact always contains the complete dataset, regardless of these
352
- toggles.
415
+ Pages output. The valid category keys are: `codeql`,
416
+ `codeql_stale_configurations`, `codeql_language_coverage`, `scorecard`,
417
+ `zizmor`, `aislop`, `dependabot_alerts`, `secret_scanning`,
418
+ `dependabot_alerts_enabled`, `dependabot_updates_enabled`,
419
+ `dependabot_cooldown`, `releases`, `mutable_releases`,
420
+ `private_vulnerability_reporting`, `auto_merge`, `github_issues`,
421
+ `pull_requests`, `pull_requests_assigned`. Like the other `report` settings,
422
+ `categories` can be set globally and overridden per organisation (overrides
423
+ merge key-by-key, so flipping one output leaves the rest untouched). The
424
+ machine-readable `report.json` artifact always contains the complete dataset,
425
+ regardless of these toggles.
353
426
 
354
427
  When several organisations share one Slack channel they render into a single
355
428
  combined digest, so the per-org Slack toggles are unioned for that channel: a
@@ -410,6 +483,52 @@ Slack-style ceiling, but they still apply their own row limits — only the
410
483
  GitHub Pages report whenever `pages_url` is set and short enough to render as a
411
484
  link.
412
485
 
486
+ ### Enabled or disabled repository lists
487
+
488
+ The four boolean feature categories — `dependabot_alerts_enabled`,
489
+ `dependabot_updates_enabled`, `private_vulnerability_reporting` and `auto_merge`
490
+ — sort each repository into one of three buckets: enabled, not enabled, or
491
+ **unknown** when the feature's state could not be read. The unknown bucket is
492
+ counted but never named, and never treated as either side, so the enabled and
493
+ not-enabled counts need not sum to the repositories analysed. Beneath the counts,
494
+ one of the two known sides is named; `report.repo_list` chooses which, on every
495
+ surface:
496
+
497
+ | Value | Names |
498
+ | ----- | ----- |
499
+ | `auto` (default) | Whichever list is shorter |
500
+ | `enabled` | The repositories with the feature on |
501
+ | `disabled` | The repositories with the feature off |
502
+
503
+ `auto` keeps a footer short whichever way an organisation leans: a feature
504
+ nearly every repository has lists its few holdouts, and one almost none have
505
+ lists its few adopters. Two cases resolve towards the actionable side: a tie
506
+ names the repositories without the feature, and so does a category where
507
+ *nothing* is enabled, since naming an empty list would print nothing where a
508
+ reader wants the repositories to fix. Both sides are always counted, whichever is
509
+ named.
510
+
511
+ A category can override the global value:
512
+
513
+ ```json
514
+ {
515
+ "report": {
516
+ "repo_list": "auto",
517
+ "categories": {
518
+ "dependabot_alerts_enabled": { "repo_list": "disabled" }
519
+ }
520
+ },
521
+ "organizations": [{ "name": "lfreleng-actions" }]
522
+ }
523
+ ```
524
+
525
+ Here Dependabot alerts always name the repositories to fix, and the other three
526
+ feature categories name whichever side is shorter. `repo_list` applies only to
527
+ those four categories; setting it on any other is a configuration error, since a
528
+ table with qualitative columns has no enabled list to name. These categories
529
+ render as a name list rather than a one-column table on every surface. The
530
+ `report.json` artifact is unaffected.
531
+
413
532
  ### Per-category row ordering
414
533
 
415
534
  Each table ships a sensible default ordering — largest backlog first, stalest
@@ -1357,8 +1476,30 @@ uvx github-security-report remediate \
1357
1476
  # Limit to specific categories (repeatable).
1358
1477
  uvx github-security-report remediate --org lfreleng-actions \
1359
1478
  --category codeql --category private_vulnerability_reporting --apply
1479
+
1480
+ # Limit to specific repositories, for any category (comma-separated and/or
1481
+ # repeatable; `name` in any configured org, or `owner/name`).
1482
+ uvx github-security-report remediate --org lfreleng-actions \
1483
+ --repos dependamerge,python-nss-ng --apply
1360
1484
  ```
1361
1485
 
1486
+ `--repos` narrows the run itself, not just its output. Everything done per
1487
+ repository (the feature probes, the batched prefetch, the CodeQL history walk
1488
+ and every write) covers only the named repositories, so a targeted run skips
1489
+ the bulk of a full one. The organisation-wide alert sweeps are the exception:
1490
+ each is a single org-bulk request, so its cost follows the organisation's open
1491
+ alert backlog rather than its repository count. It combines with
1492
+ `--category`. Every name is checked against every configured organisation
1493
+ before anything is written: a name that matches no repository (almost always
1494
+ a typo), or one that matches only repositories the configuration excludes
1495
+ (the `exclude` list, archived, fork, template or test), stops the run with
1496
+ exit code 2 and says which. A bare name excluded in one organisation but in
1497
+ scope in another runs against the in-scope one only; naming a repository never
1498
+ brings an excluded one back. A `--repos` value that names nothing also stops
1499
+ the run, rather than falling back to every repository. The run then collects
1500
+ exactly the repositories that check validated, without listing the
1501
+ organisation again, and prints `Limited to:` beneath its heading.
1502
+
1362
1503
  The remediable categories are the simple on/off features with a documented
1363
1504
  enablement endpoint:
1364
1505
 
@@ -1369,11 +1510,65 @@ enablement endpoint:
1369
1510
  | `dependabot_alerts_enabled` | Dependabot vulnerability alerts |
1370
1511
  | `dependabot_updates_enabled` | Dependabot security updates (plus alerts) |
1371
1512
  | `private_vulnerability_reporting` | Private vulnerability reporting |
1513
+ | `auto_merge` | The repository's "Allow auto-merge" setting |
1372
1514
 
1373
1515
  Qualitative findings (Scorecard, zizmor, open Dependabot alerts, cooldown,
1374
1516
  release freshness/mutability) are reported but not auto-remediated. Remediation
1375
1517
  is organisation-scoped (`--scope org`, the default and only supported scope).
1376
1518
 
1519
+ ### Cleaning up stale CodeQL configurations
1520
+
1521
+ One category is **destructive**, so it runs only when named. The no-argument
1522
+ run never includes it:
1523
+
1524
+ ```bash
1525
+ # Preview: which configurations would go, and which are refused and why.
1526
+ uvx github-security-report remediate --org lfreleng-actions \
1527
+ --category codeql_stale_configurations
1528
+
1529
+ # Apply.
1530
+ uvx github-security-report remediate --org lfreleng-actions \
1531
+ --category codeql_stale_configurations --apply
1532
+ ```
1533
+
1534
+ It acts on the rows of **CodeQL: Stale Configurations**, according to their
1535
+ cause:
1536
+
1537
+ <!-- markdownlint-disable MD013 -->
1538
+
1539
+ | Cause | Action |
1540
+ | ----- | ------ |
1541
+ | Default setup disabled; workflow removed; superseded by default setup; language removed from default setup | **Delete** the configuration: it can never upload again |
1542
+ | Workflow disabled after inactivity | **Re-enable** the workflow, restoring the scan |
1543
+ | Analyses failing, default setup changing state or not uploading, uploaded outside GitHub Actions, workflow disabled by hand, active but not uploading, or state unreadable | Reported only; needs a person |
1544
+
1545
+ <!-- markdownlint-enable MD013 -->
1546
+
1547
+ Deleting removes a configuration's analyses, which clears GitHub's *"Code
1548
+ Scanning results may be out of date"* warning but also removes its alert
1549
+ history. A deletion is therefore **refused**, in a dry run as in an apply,
1550
+ when:
1551
+
1552
+ - no current configuration scans its language: the stale results are then
1553
+ the only record of it, so add scanning for the language first (the refusal
1554
+ names it, and **CodeQL: Language Coverage** lists the gap);
1555
+ - it would help close an open alert. This is judged across **every deletion
1556
+ planned in the repository**, not one at a time: an alert held by two stale
1557
+ configurations would close if both went, so both are refused, although
1558
+ neither holds it alone. An alert also reported by a live configuration
1559
+ blocks nothing;
1560
+ - the repository's open alerts cannot be read.
1561
+
1562
+ Refusals are listed beside the work done and do not fail the run. Deletion is
1563
+ paced at one request a second across the whole run, as GitHub asks of
1564
+ mutating requests, and is resumable: an interrupted run's next pass picks up
1565
+ the remaining analyses. GitHub answers requests it will not authorise with
1566
+ `404`, so a deletion's `404` counts as already done only once the analysis
1567
+ also reads back as gone; a token that may not delete fails instead of
1568
+ reporting a cleanup that never happened. The token needs the classic `repo`
1569
+ scope. See [ADR-0005](docs/adr/0005-codeql-configuration-cleanup.md) for the
1570
+ reasoning.
1571
+
1377
1572
  ## Bulk Remediation Scripts
1378
1573
 
1379
1574
  The standalone scripts below predate the `remediate` subcommand and remain for
@@ -63,9 +63,9 @@ dev = [
63
63
  "pytest-asyncio==1.4.0",
64
64
  "pytest-cov==7.1.0",
65
65
  "respx==0.23.1",
66
- "syrupy==6.0.0",
66
+ "syrupy==6.1.1",
67
67
  "mypy==2.3.1",
68
- "ruff==0.16.5",
68
+ "ruff==0.16.8",
69
69
  "types-jsonschema==4.26.0.20260518",
70
70
  "types-PyYAML==6.0.12.20250915",
71
71
  ]
@@ -95,9 +95,9 @@ dev = [
95
95
  "pytest-asyncio==1.4.0",
96
96
  "pytest-cov==7.1.0",
97
97
  "respx==0.23.1",
98
- "syrupy==6.0.0",
98
+ "syrupy==6.1.1",
99
99
  "mypy==2.3.1",
100
- "ruff==0.16.5",
100
+ "ruff==0.16.8",
101
101
  "types-jsonschema==4.26.0.20260518",
102
102
  "types-PyYAML==6.0.12.20250915",
103
103
  ]
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '0.15.0'
22
- __version_tuple__ = version_tuple = (0, 15, 0)
21
+ __version__ = version = '0.17.0'
22
+ __version_tuple__ = version_tuple = (0, 17, 0)
23
23
 
24
24
  __commit_id__ = commit_id = None