github-security-report 0.15.0__tar.gz → 0.16.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 (91) hide show
  1. {github_security_report-0.15.0 → github_security_report-0.16.0}/PKG-INFO +132 -13
  2. {github_security_report-0.15.0 → github_security_report-0.16.0}/README.md +129 -10
  3. {github_security_report-0.15.0 → github_security_report-0.16.0}/pyproject.toml +4 -4
  4. {github_security_report-0.15.0 → github_security_report-0.16.0}/src/github_security_report/_version.py +2 -2
  5. github_security_report-0.16.0/src/github_security_report/categories/__init__.py +92 -0
  6. github_security_report-0.16.0/src/github_security_report/categories/keys.py +76 -0
  7. github_security_report-0.16.0/src/github_security_report/categories/signals.py +113 -0
  8. github_security_report-0.16.0/src/github_security_report/categories/tables.py +244 -0
  9. {github_security_report-0.15.0 → github_security_report-0.16.0}/src/github_security_report/cli/app.py +6 -1
  10. {github_security_report-0.15.0 → github_security_report-0.16.0}/src/github_security_report/cli/modes.py +4 -0
  11. {github_security_report-0.15.0 → github_security_report-0.16.0}/src/github_security_report/cli/outputs.py +55 -12
  12. {github_security_report-0.15.0 → github_security_report-0.16.0}/src/github_security_report/cli/publish.py +4 -0
  13. {github_security_report-0.15.0 → github_security_report-0.16.0}/src/github_security_report/cli/serialise.py +4 -0
  14. github_security_report-0.16.0/src/github_security_report/client/codeql_parsers.py +119 -0
  15. {github_security_report-0.15.0 → github_security_report-0.16.0}/src/github_security_report/client/org_reads.py +5 -0
  16. {github_security_report-0.15.0 → github_security_report-0.16.0}/src/github_security_report/client/parsers.py +5 -0
  17. {github_security_report-0.15.0 → github_security_report-0.16.0}/src/github_security_report/client/queries.py +12 -3
  18. {github_security_report-0.15.0 → github_security_report-0.16.0}/src/github_security_report/client/reads.py +95 -1
  19. {github_security_report-0.15.0 → github_security_report-0.16.0}/src/github_security_report/client/writes.py +20 -0
  20. github_security_report-0.16.0/src/github_security_report/codeql/__init__.py +38 -0
  21. github_security_report-0.16.0/src/github_security_report/codeql/facts.py +189 -0
  22. github_security_report-0.16.0/src/github_security_report/codeql/tables.py +246 -0
  23. github_security_report-0.16.0/src/github_security_report/collect/codeql.py +66 -0
  24. {github_security_report-0.15.0 → github_security_report-0.16.0}/src/github_security_report/collect/extras.py +25 -15
  25. {github_security_report-0.15.0 → github_security_report-0.16.0}/src/github_security_report/collect/protocols.py +15 -0
  26. {github_security_report-0.15.0 → github_security_report-0.16.0}/src/github_security_report/config/loader.py +12 -0
  27. {github_security_report-0.15.0 → github_security_report-0.16.0}/src/github_security_report/config/models.py +27 -1
  28. {github_security_report-0.15.0 → github_security_report-0.16.0}/src/github_security_report/config/schema.py +63 -36
  29. {github_security_report-0.15.0 → github_security_report-0.16.0}/src/github_security_report/layout.py +14 -28
  30. {github_security_report-0.15.0 → github_security_report-0.16.0}/src/github_security_report/models.py +6 -0
  31. {github_security_report-0.15.0 → github_security_report-0.16.0}/src/github_security_report/ordering.py +13 -16
  32. {github_security_report-0.15.0 → github_security_report-0.16.0}/src/github_security_report/posture/__init__.py +2 -0
  33. {github_security_report-0.15.0 → github_security_report-0.16.0}/src/github_security_report/posture/enablement.py +29 -4
  34. {github_security_report-0.15.0 → github_security_report-0.16.0}/src/github_security_report/posture/facts.py +2 -0
  35. {github_security_report-0.15.0 → github_security_report-0.16.0}/src/github_security_report/remediate.py +10 -6
  36. {github_security_report-0.15.0 → github_security_report-0.16.0}/src/github_security_report/render/html.py +29 -16
  37. {github_security_report-0.15.0 → github_security_report-0.16.0}/src/github_security_report/render/markdown.py +34 -22
  38. {github_security_report-0.15.0 → github_security_report-0.16.0}/src/github_security_report/render/slack.py +45 -30
  39. {github_security_report-0.15.0 → github_security_report-0.16.0}/src/github_security_report/render/terminal.py +23 -40
  40. {github_security_report-0.15.0 → github_security_report-0.16.0}/src/github_security_report/report/__init__.py +10 -0
  41. {github_security_report-0.15.0 → github_security_report-0.16.0}/src/github_security_report/report/aggregate.py +59 -1
  42. {github_security_report-0.15.0 → github_security_report-0.16.0}/src/github_security_report/report/display.py +47 -1
  43. {github_security_report-0.15.0 → github_security_report-0.16.0}/src/github_security_report/report/tables.py +63 -5
  44. {github_security_report-0.15.0 → github_security_report-0.16.0}/src/github_security_report/summary.py +50 -3
  45. github_security_report-0.15.0/src/github_security_report/categories.py +0 -384
  46. {github_security_report-0.15.0 → github_security_report-0.16.0}/.gitignore +0 -0
  47. {github_security_report-0.15.0 → github_security_report-0.16.0}/LICENSE +0 -0
  48. {github_security_report-0.15.0 → github_security_report-0.16.0}/LICENSES/Apache-2.0.txt +0 -0
  49. {github_security_report-0.15.0 → github_security_report-0.16.0}/scripts/README.md +0 -0
  50. {github_security_report-0.15.0 → github_security_report-0.16.0}/src/github_security_report/__init__.py +0 -0
  51. {github_security_report-0.15.0 → github_security_report-0.16.0}/src/github_security_report/authors.py +0 -0
  52. {github_security_report-0.15.0 → github_security_report-0.16.0}/src/github_security_report/classify.py +0 -0
  53. {github_security_report-0.15.0 → github_security_report-0.16.0}/src/github_security_report/cli/__init__.py +0 -0
  54. {github_security_report-0.15.0 → github_security_report-0.16.0}/src/github_security_report/cli/__main__.py +0 -0
  55. {github_security_report-0.15.0 → github_security_report-0.16.0}/src/github_security_report/cli/boundary.py +0 -0
  56. {github_security_report-0.15.0 → github_security_report-0.16.0}/src/github_security_report/cli/options.py +0 -0
  57. {github_security_report-0.15.0 → github_security_report-0.16.0}/src/github_security_report/client/__init__.py +0 -0
  58. {github_security_report-0.15.0 → github_security_report-0.16.0}/src/github_security_report/client/alerts.py +0 -0
  59. {github_security_report-0.15.0 → github_security_report-0.16.0}/src/github_security_report/client/batch_errors.py +0 -0
  60. {github_security_report-0.15.0 → github_security_report-0.16.0}/src/github_security_report/client/copilot.py +0 -0
  61. {github_security_report-0.15.0 → github_security_report-0.16.0}/src/github_security_report/client/endpoints.py +0 -0
  62. {github_security_report-0.15.0 → github_security_report-0.16.0}/src/github_security_report/client/errors.py +0 -0
  63. {github_security_report-0.15.0 → github_security_report-0.16.0}/src/github_security_report/client/reviews.py +0 -0
  64. {github_security_report-0.15.0 → github_security_report-0.16.0}/src/github_security_report/client/transport.py +0 -0
  65. {github_security_report-0.15.0 → github_security_report-0.16.0}/src/github_security_report/collect/__init__.py +0 -0
  66. {github_security_report-0.15.0 → github_security_report-0.16.0}/src/github_security_report/collect/context.py +0 -0
  67. {github_security_report-0.15.0 → github_security_report-0.16.0}/src/github_security_report/collect/org.py +0 -0
  68. {github_security_report-0.15.0 → github_security_report-0.16.0}/src/github_security_report/collect/repo.py +0 -0
  69. {github_security_report-0.15.0 → github_security_report-0.16.0}/src/github_security_report/config/__init__.py +0 -0
  70. {github_security_report-0.15.0 → github_security_report-0.16.0}/src/github_security_report/config/order.py +0 -0
  71. {github_security_report-0.15.0 → github_security_report-0.16.0}/src/github_security_report/gating.py +0 -0
  72. {github_security_report-0.15.0 → github_security_report-0.16.0}/src/github_security_report/gitctx.py +0 -0
  73. {github_security_report-0.15.0 → github_security_report-0.16.0}/src/github_security_report/issues.py +0 -0
  74. {github_security_report-0.15.0 → github_security_report-0.16.0}/src/github_security_report/posture/releases.py +0 -0
  75. {github_security_report-0.15.0 → github_security_report-0.16.0}/src/github_security_report/pulls/__init__.py +0 -0
  76. {github_security_report-0.15.0 → github_security_report-0.16.0}/src/github_security_report/pulls/columns.py +0 -0
  77. {github_security_report-0.15.0 → github_security_report-0.16.0}/src/github_security_report/pulls/counting.py +0 -0
  78. {github_security_report-0.15.0 → github_security_report-0.16.0}/src/github_security_report/pulls/presentation.py +0 -0
  79. {github_security_report-0.15.0 → github_security_report-0.16.0}/src/github_security_report/pulls/table.py +0 -0
  80. {github_security_report-0.15.0 → github_security_report-0.16.0}/src/github_security_report/py.typed +0 -0
  81. {github_security_report-0.15.0 → github_security_report-0.16.0}/src/github_security_report/ranking.py +0 -0
  82. {github_security_report-0.15.0 → github_security_report-0.16.0}/src/github_security_report/render/__init__.py +0 -0
  83. {github_security_report-0.15.0 → github_security_report-0.16.0}/src/github_security_report/render/slack_limits.py +0 -0
  84. {github_security_report-0.15.0 → github_security_report-0.16.0}/src/github_security_report/report/signals.py +0 -0
  85. {github_security_report-0.15.0 → github_security_report-0.16.0}/src/github_security_report/rulesets.py +0 -0
  86. {github_security_report-0.15.0 → github_security_report-0.16.0}/src/github_security_report/runner.py +0 -0
  87. {github_security_report-0.15.0 → github_security_report-0.16.0}/src/github_security_report/scope.py +0 -0
  88. {github_security_report-0.15.0 → github_security_report-0.16.0}/src/github_security_report/secret_patterns.py +0 -0
  89. {github_security_report-0.15.0 → github_security_report-0.16.0}/src/github_security_report/severity.py +0 -0
  90. {github_security_report-0.15.0 → github_security_report-0.16.0}/src/github_security_report/templates/index.html.j2 +0 -0
  91. {github_security_report-0.15.0 → github_security_report-0.16.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.16.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
@@ -355,6 +409,23 @@ 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
410
  exclusion is visible rather than silently dropped.
357
411
 
412
+ Every category reports those exclusions beneath its counts, so an
413
+ organisation-wide list repeats under each one. `report.excluded_display`
414
+ governs that line on every surface:
415
+
416
+ | Value | Excluded line |
417
+ | ----- | ------------- |
418
+ | `always-show` (default) | Shown under every category |
419
+ | `always-hide` | Never shown |
420
+ | `conditional-hide` | Shown only for a category whose exclusions differ from the organisation's `exclude` list, and then in full |
421
+
422
+ Hiding the line changes nothing else: every count is unaffected, and the report
423
+ header's repository count already leaves excluded repositories out. So a clean
424
+ category reads `All Clean` once no Excluded line qualifies it. Today every
425
+ category reports the organisation's own list, so `conditional-hide` currently
426
+ hides the same lines `always-hide` does. It differs once a category excludes
427
+ repositories of its own. The `report.json` artifact always lists the exclusions.
428
+
358
429
  Archived and test repositories are excluded from analysis by default. Opt them
359
430
  back in with `report.include_archived` / `report.include_test` in the config, or
360
431
  for a single run with `--include-archived` / `--include-test`.
@@ -384,16 +455,17 @@ both true.
384
455
 
385
456
  The example above hides Zizmor on every surface, and keeps Releases / Tagging
386
457
  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.
458
+ Pages output. The valid category keys are: `codeql`,
459
+ `codeql_stale_configurations`, `codeql_language_coverage`, `scorecard`,
460
+ `zizmor`, `aislop`, `dependabot_alerts`, `secret_scanning`,
461
+ `dependabot_alerts_enabled`, `dependabot_updates_enabled`,
462
+ `dependabot_cooldown`, `releases`, `mutable_releases`,
463
+ `private_vulnerability_reporting`, `auto_merge`, `github_issues`,
464
+ `pull_requests`, `pull_requests_assigned`. Like the other `report` settings,
465
+ `categories` can be set globally and overridden per organisation (overrides
466
+ merge key-by-key, so flipping one output leaves the rest untouched). The
467
+ machine-readable `report.json` artifact always contains the complete dataset,
468
+ regardless of these toggles.
397
469
 
398
470
  When several organisations share one Slack channel they render into a single
399
471
  combined digest, so the per-org Slack toggles are unioned for that channel: a
@@ -454,6 +526,52 @@ Slack-style ceiling, but they still apply their own row limits — only the
454
526
  GitHub Pages report whenever `pages_url` is set and short enough to render as a
455
527
  link.
456
528
 
529
+ ### Enabled or disabled repository lists
530
+
531
+ The four boolean feature categories — `dependabot_alerts_enabled`,
532
+ `dependabot_updates_enabled`, `private_vulnerability_reporting` and `auto_merge`
533
+ — sort each repository into one of three buckets: enabled, not enabled, or
534
+ **unknown** when the feature's state could not be read. The unknown bucket is
535
+ counted but never named, and never treated as either side, so the enabled and
536
+ not-enabled counts need not sum to the repositories analysed. Beneath the counts,
537
+ one of the two known sides is named; `report.repo_list` chooses which, on every
538
+ surface:
539
+
540
+ | Value | Names |
541
+ | ----- | ----- |
542
+ | `auto` (default) | Whichever list is shorter |
543
+ | `enabled` | The repositories with the feature on |
544
+ | `disabled` | The repositories with the feature off |
545
+
546
+ `auto` keeps a footer short whichever way an organisation leans: a feature
547
+ nearly every repository has lists its few holdouts, and one almost none have
548
+ lists its few adopters. Two cases resolve towards the actionable side: a tie
549
+ names the repositories without the feature, and so does a category where
550
+ *nothing* is enabled, since naming an empty list would print nothing where a
551
+ reader wants the repositories to fix. Both sides are always counted, whichever is
552
+ named.
553
+
554
+ A category can override the global value:
555
+
556
+ ```json
557
+ {
558
+ "report": {
559
+ "repo_list": "auto",
560
+ "categories": {
561
+ "dependabot_alerts_enabled": { "repo_list": "disabled" }
562
+ }
563
+ },
564
+ "organizations": [{ "name": "lfreleng-actions" }]
565
+ }
566
+ ```
567
+
568
+ Here Dependabot alerts always name the repositories to fix, and the other three
569
+ feature categories name whichever side is shorter. `repo_list` applies only to
570
+ those four categories; setting it on any other is a configuration error, since a
571
+ table with qualitative columns has no enabled list to name. These categories
572
+ render as a name list rather than a one-column table on every surface. The
573
+ `report.json` artifact is unaffected.
574
+
457
575
  ### Per-category row ordering
458
576
 
459
577
  Each table ships a sensible default ordering — largest backlog first, stalest
@@ -1413,6 +1531,7 @@ enablement endpoint:
1413
1531
  | `dependabot_alerts_enabled` | Dependabot vulnerability alerts |
1414
1532
  | `dependabot_updates_enabled` | Dependabot security updates (plus alerts) |
1415
1533
  | `private_vulnerability_reporting` | Private vulnerability reporting |
1534
+ | `auto_merge` | The repository's "Allow auto-merge" setting |
1416
1535
 
1417
1536
  Qualitative findings (Scorecard, zizmor, open Dependabot alerts, cooldown,
1418
1537
  release freshness/mutability) are reported but not auto-remediated. Remediation
@@ -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
@@ -311,6 +365,23 @@ 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
366
  exclusion is visible rather than silently dropped.
313
367
 
368
+ Every category reports those exclusions beneath its counts, so an
369
+ organisation-wide list repeats under each one. `report.excluded_display`
370
+ governs that line on every surface:
371
+
372
+ | Value | Excluded line |
373
+ | ----- | ------------- |
374
+ | `always-show` (default) | Shown under every category |
375
+ | `always-hide` | Never shown |
376
+ | `conditional-hide` | Shown only for a category whose exclusions differ from the organisation's `exclude` list, and then in full |
377
+
378
+ Hiding the line changes nothing else: every count is unaffected, and the report
379
+ header's repository count already leaves excluded repositories out. So a clean
380
+ category reads `All Clean` once no Excluded line qualifies it. Today every
381
+ category reports the organisation's own list, so `conditional-hide` currently
382
+ hides the same lines `always-hide` does. It differs once a category excludes
383
+ repositories of its own. The `report.json` artifact always lists the exclusions.
384
+
314
385
  Archived and test repositories are excluded from analysis by default. Opt them
315
386
  back in with `report.include_archived` / `report.include_test` in the config, or
316
387
  for a single run with `--include-archived` / `--include-test`.
@@ -340,16 +411,17 @@ both true.
340
411
 
341
412
  The example above hides Zizmor on every surface, and keeps Releases / Tagging
342
413
  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.
414
+ Pages output. The valid category keys are: `codeql`,
415
+ `codeql_stale_configurations`, `codeql_language_coverage`, `scorecard`,
416
+ `zizmor`, `aislop`, `dependabot_alerts`, `secret_scanning`,
417
+ `dependabot_alerts_enabled`, `dependabot_updates_enabled`,
418
+ `dependabot_cooldown`, `releases`, `mutable_releases`,
419
+ `private_vulnerability_reporting`, `auto_merge`, `github_issues`,
420
+ `pull_requests`, `pull_requests_assigned`. Like the other `report` settings,
421
+ `categories` can be set globally and overridden per organisation (overrides
422
+ merge key-by-key, so flipping one output leaves the rest untouched). The
423
+ machine-readable `report.json` artifact always contains the complete dataset,
424
+ regardless of these toggles.
353
425
 
354
426
  When several organisations share one Slack channel they render into a single
355
427
  combined digest, so the per-org Slack toggles are unioned for that channel: a
@@ -410,6 +482,52 @@ Slack-style ceiling, but they still apply their own row limits — only the
410
482
  GitHub Pages report whenever `pages_url` is set and short enough to render as a
411
483
  link.
412
484
 
485
+ ### Enabled or disabled repository lists
486
+
487
+ The four boolean feature categories — `dependabot_alerts_enabled`,
488
+ `dependabot_updates_enabled`, `private_vulnerability_reporting` and `auto_merge`
489
+ — sort each repository into one of three buckets: enabled, not enabled, or
490
+ **unknown** when the feature's state could not be read. The unknown bucket is
491
+ counted but never named, and never treated as either side, so the enabled and
492
+ not-enabled counts need not sum to the repositories analysed. Beneath the counts,
493
+ one of the two known sides is named; `report.repo_list` chooses which, on every
494
+ surface:
495
+
496
+ | Value | Names |
497
+ | ----- | ----- |
498
+ | `auto` (default) | Whichever list is shorter |
499
+ | `enabled` | The repositories with the feature on |
500
+ | `disabled` | The repositories with the feature off |
501
+
502
+ `auto` keeps a footer short whichever way an organisation leans: a feature
503
+ nearly every repository has lists its few holdouts, and one almost none have
504
+ lists its few adopters. Two cases resolve towards the actionable side: a tie
505
+ names the repositories without the feature, and so does a category where
506
+ *nothing* is enabled, since naming an empty list would print nothing where a
507
+ reader wants the repositories to fix. Both sides are always counted, whichever is
508
+ named.
509
+
510
+ A category can override the global value:
511
+
512
+ ```json
513
+ {
514
+ "report": {
515
+ "repo_list": "auto",
516
+ "categories": {
517
+ "dependabot_alerts_enabled": { "repo_list": "disabled" }
518
+ }
519
+ },
520
+ "organizations": [{ "name": "lfreleng-actions" }]
521
+ }
522
+ ```
523
+
524
+ Here Dependabot alerts always name the repositories to fix, and the other three
525
+ feature categories name whichever side is shorter. `repo_list` applies only to
526
+ those four categories; setting it on any other is a configuration error, since a
527
+ table with qualitative columns has no enabled list to name. These categories
528
+ render as a name list rather than a one-column table on every surface. The
529
+ `report.json` artifact is unaffected.
530
+
413
531
  ### Per-category row ordering
414
532
 
415
533
  Each table ships a sensible default ordering — largest backlog first, stalest
@@ -1369,6 +1487,7 @@ enablement endpoint:
1369
1487
  | `dependabot_alerts_enabled` | Dependabot vulnerability alerts |
1370
1488
  | `dependabot_updates_enabled` | Dependabot security updates (plus alerts) |
1371
1489
  | `private_vulnerability_reporting` | Private vulnerability reporting |
1490
+ | `auto_merge` | The repository's "Allow auto-merge" setting |
1372
1491
 
1373
1492
  Qualitative findings (Scorecard, zizmor, open Dependabot alerts, cooldown,
1374
1493
  release freshness/mutability) are reported but not auto-remediated. Remediation
@@ -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.16.0'
22
+ __version_tuple__ = version_tuple = (0, 16, 0)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -0,0 +1,92 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ # SPDX-FileCopyrightText: 2026 The Linux Foundation
3
+ """Report category metadata.
4
+
5
+ A single, render-surface-agnostic registry describing every reporting category
6
+ the tool produces. Each category carries its display title, the pass/fail
7
+ vocabulary used in the standardised summary footer, a documentation URL, and a
8
+ default human description. Renderers read this registry instead of hard-coding
9
+ per-category headings, labels and explanatory text, so a wording change here
10
+ flows to the terminal, Slack, Markdown and HTML surfaces at once.
11
+
12
+ The registry deliberately holds no behaviour and imports nothing from the rest
13
+ of the package except the leaf ``severity`` and ``secret_patterns`` modules
14
+ (which themselves import nothing from the package), so both the domain models
15
+ and the renderers can depend on it without a cycle. It is split by kind --
16
+ the ranked signals in :mod:`.signals`, the tables in :mod:`.tables` -- and
17
+ merged here, so callers see one registry in render order.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ from github_security_report.categories.keys import CategoryKey, CategoryMeta
23
+ from github_security_report.categories.signals import SIGNAL_CATEGORIES
24
+ from github_security_report.categories.tables import TABLE_CATEGORIES
25
+
26
+ __all__ = [
27
+ "NESTED_CATEGORIES",
28
+ "REPO_LIST_CATEGORIES",
29
+ "CategoryKey",
30
+ "CategoryMeta",
31
+ "all_categories",
32
+ "category_meta",
33
+ "orderable_categories",
34
+ ]
35
+
36
+ _CATEGORIES: dict[CategoryKey, CategoryMeta] = {**SIGNAL_CATEGORIES, **TABLE_CATEGORIES}
37
+
38
+
39
+ def category_meta(key: CategoryKey) -> CategoryMeta:
40
+ """The :class:`CategoryMeta` for ``key`` (registry lookup)."""
41
+ return _CATEGORIES[key]
42
+
43
+
44
+ # Categories rendered as sub-tables beneath another category rather than as
45
+ # sections of their own. The three Dependabot posture tables qualify their
46
+ # parent signal -- "Alerts Enabled" means nothing adrift from "Dependabot:
47
+ # Security Alerts" -- so they travel with it and cannot be positioned
48
+ # independently, and the two CodeQL scan-health tables likewise qualify the
49
+ # CodeQL signal, whose "Clean" means nothing if the scans behind it stopped.
50
+ # Named here rather than in the layout module so the config schema can refuse to
51
+ # accept one in an ordering list, which would otherwise be a setting that
52
+ # validates and then does nothing.
53
+ NESTED_CATEGORIES: frozenset[CategoryKey] = frozenset(
54
+ {
55
+ CategoryKey.CODEQL_STALE_CONFIGURATIONS,
56
+ CategoryKey.CODEQL_LANGUAGE_COVERAGE,
57
+ CategoryKey.DEPENDABOT_ALERTS_ENABLED,
58
+ CategoryKey.DEPENDABOT_UPDATES_ENABLED,
59
+ CategoryKey.DEPENDABOT_COOLDOWN,
60
+ }
61
+ )
62
+
63
+ # The boolean feature categories: every repository is either enabled or not, and
64
+ # nothing else is known about it, so both sides are plain repository lists and
65
+ # either can be the one worth naming. These render their names inline rather
66
+ # than as a one-column table, and are the only categories the ``repo_list``
67
+ # setting applies to -- the schema refuses it anywhere else, since a table with
68
+ # qualitative columns has no "enabled" list to swap in.
69
+ REPO_LIST_CATEGORIES: frozenset[CategoryKey] = frozenset(
70
+ {
71
+ CategoryKey.DEPENDABOT_ALERTS_ENABLED,
72
+ CategoryKey.DEPENDABOT_UPDATES_ENABLED,
73
+ CategoryKey.PRIVATE_VULNERABILITY_REPORTING,
74
+ CategoryKey.AUTO_MERGE,
75
+ }
76
+ )
77
+
78
+
79
+ def orderable_categories() -> tuple[CategoryMeta, ...]:
80
+ """Categories an ordering list may name, in registry order.
81
+
82
+ Every category except the nested ones, which have no position of their own
83
+ to configure.
84
+ """
85
+ return tuple(
86
+ meta for meta in _CATEGORIES.values() if meta.key not in NESTED_CATEGORIES
87
+ )
88
+
89
+
90
+ def all_categories() -> tuple[CategoryMeta, ...]:
91
+ """Every category's metadata, in registry (render) order."""
92
+ return tuple(_CATEGORIES.values())
@@ -0,0 +1,76 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ # SPDX-FileCopyrightText: 2026 The Linux Foundation
3
+ """The category identifier and the shape of its metadata.
4
+
5
+ Leaf types shared by the registry halves in :mod:`.signals` and :mod:`.tables`.
6
+ ``CategoryKey`` values are the stable identifiers used by the per-category
7
+ configuration toggles, so treat them as part of the config contract: rename
8
+ with care.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from dataclasses import dataclass
14
+ from enum import Enum
15
+
16
+ from github_security_report.severity import Severity
17
+
18
+
19
+ class CategoryKey(str, Enum):
20
+ """Stable identifier for one reporting category (also the config key)."""
21
+
22
+ CODEQL = "codeql"
23
+ CODEQL_STALE_CONFIGURATIONS = "codeql_stale_configurations"
24
+ CODEQL_LANGUAGE_COVERAGE = "codeql_language_coverage"
25
+ SCORECARD = "scorecard"
26
+ ZIZMOR = "zizmor"
27
+ AISLOP = "aislop"
28
+ DEPENDABOT_ALERTS = "dependabot_alerts"
29
+ SECRET_SCANNING = "secret_scanning"
30
+ DEPENDABOT_ALERTS_ENABLED = "dependabot_alerts_enabled"
31
+ DEPENDABOT_UPDATES_ENABLED = "dependabot_updates_enabled"
32
+ DEPENDABOT_COOLDOWN = "dependabot_cooldown"
33
+ RELEASES = "releases"
34
+ MUTABLE_RELEASES = "mutable_releases"
35
+ PRIVATE_VULNERABILITY_REPORTING = "private_vulnerability_reporting"
36
+ AUTO_MERGE = "auto_merge"
37
+ GITHUB_ISSUES = "github_issues"
38
+ PULL_REQUESTS = "pull_requests"
39
+ PULL_REQUESTS_ASSIGNED = "pull_requests_assigned"
40
+
41
+
42
+ @dataclass(frozen=True)
43
+ class CategoryMeta:
44
+ """Display and documentation metadata for one reporting category.
45
+
46
+ ``pass_label`` names the healthy state (e.g. ``"Clean"``, ``"Immutable"``)
47
+ and is what the summary footer reports as ``All <pass_label>`` when nothing
48
+ needs attention. That collapse wants an adjectival label; a category whose
49
+ counted wording is a noun phrase ("12 No open issues") sets
50
+ ``pass_all_label`` to the word that reads correctly after "All" instead.
51
+ ``fail_label`` names the actionable state for categories
52
+ with a binary pass/fail axis (enablement, cooldown, mutability, release
53
+ freshness); it is ``None`` for the severity-ranked signals, whose offenders
54
+ are enumerated in the table itself rather than as a single failure count.
55
+ ``description`` is the default explanatory text shown beneath the table on
56
+ the Markdown and HTML surfaces; a builder may override it at runtime when
57
+ the wording depends on configuration (e.g. the release-age thresholds).
58
+ """
59
+
60
+ key: CategoryKey
61
+ title: str
62
+ pass_label: str
63
+ fail_label: str | None
64
+ url: str
65
+ description: str = ""
66
+ # Alternative pass wording for the collapsed "All <label>" footer line,
67
+ # when the counted wording would not read grammatically after "All".
68
+ pass_all_label: str | None = None
69
+ # The lowest finding severity that counts as a failure for this category.
70
+ # A repository fails (appears as an offender) only when it carries a finding
71
+ # at or above this rung; findings below it fold into the clean count. The
72
+ # global default is MEDIUM, so Low and Informational findings pass; a
73
+ # category may lower it (Zizmor uses INFORMATIONAL, so every finding
74
+ # counts). Meaningful only for the severity-ranked signals; binary
75
+ # categories ignore it. Overridable per category via the JSON config.
76
+ fail_severity: Severity = Severity.MEDIUM