github-security-report 0.18.1__tar.gz → 0.19.1__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 (100) hide show
  1. {github_security_report-0.18.1 → github_security_report-0.19.1}/PKG-INFO +87 -21
  2. {github_security_report-0.18.1 → github_security_report-0.19.1}/README.md +85 -19
  3. {github_security_report-0.18.1 → github_security_report-0.19.1}/pyproject.toml +2 -2
  4. {github_security_report-0.18.1 → github_security_report-0.19.1}/src/github_security_report/_version.py +2 -2
  5. {github_security_report-0.18.1 → github_security_report-0.19.1}/src/github_security_report/authors.py +31 -0
  6. {github_security_report-0.18.1 → github_security_report-0.19.1}/src/github_security_report/categories/keys.py +4 -0
  7. {github_security_report-0.18.1 → github_security_report-0.19.1}/src/github_security_report/categories/tables.py +21 -0
  8. {github_security_report-0.18.1 → github_security_report-0.19.1}/src/github_security_report/cli/app.py +9 -6
  9. {github_security_report-0.18.1 → github_security_report-0.19.1}/src/github_security_report/cli/modes.py +27 -12
  10. {github_security_report-0.18.1 → github_security_report-0.19.1}/src/github_security_report/cli/options.py +3 -0
  11. {github_security_report-0.18.1 → github_security_report-0.19.1}/src/github_security_report/cli/serialise.py +3 -0
  12. {github_security_report-0.18.1 → github_security_report-0.19.1}/src/github_security_report/client/org_reads.py +71 -47
  13. {github_security_report-0.18.1 → github_security_report-0.19.1}/src/github_security_report/client/parsers.py +2 -0
  14. {github_security_report-0.18.1 → github_security_report-0.19.1}/src/github_security_report/client/queries.py +12 -3
  15. {github_security_report-0.18.1 → github_security_report-0.19.1}/src/github_security_report/client/reads.py +20 -0
  16. {github_security_report-0.18.1 → github_security_report-0.19.1}/src/github_security_report/client/writes.py +32 -0
  17. github_security_report-0.19.1/src/github_security_report/code_quality.py +164 -0
  18. {github_security_report-0.18.1 → github_security_report-0.19.1}/src/github_security_report/collect/extras.py +88 -43
  19. {github_security_report-0.18.1 → github_security_report-0.19.1}/src/github_security_report/collect/org.py +13 -1
  20. {github_security_report-0.18.1 → github_security_report-0.19.1}/src/github_security_report/collect/protocols.py +11 -4
  21. {github_security_report-0.18.1 → github_security_report-0.19.1}/src/github_security_report/models.py +4 -0
  22. {github_security_report-0.18.1 → github_security_report-0.19.1}/src/github_security_report/posture/__init__.py +10 -3
  23. {github_security_report-0.18.1 → github_security_report-0.19.1}/src/github_security_report/posture/facts.py +8 -0
  24. github_security_report-0.19.1/src/github_security_report/posture/quality.py +96 -0
  25. {github_security_report-0.18.1 → github_security_report-0.19.1}/src/github_security_report/remediate/__init__.py +42 -3
  26. {github_security_report-0.18.1 → github_security_report-0.19.1}/src/github_security_report/remediate/model.py +7 -1
  27. {github_security_report-0.18.1 → github_security_report-0.19.1}/src/github_security_report/render/terminal.py +12 -0
  28. github_security_report-0.19.1/src/github_security_report/render/terminal_notices.py +48 -0
  29. {github_security_report-0.18.1 → github_security_report-0.19.1}/src/github_security_report/report/aggregate.py +14 -0
  30. {github_security_report-0.18.1 → github_security_report-0.19.1}/src/github_security_report/report/tables.py +13 -2
  31. {github_security_report-0.18.1 → github_security_report-0.19.1}/src/github_security_report/summary.py +22 -6
  32. {github_security_report-0.18.1 → github_security_report-0.19.1}/src/github_security_report/templates/report.html.j2 +2 -0
  33. {github_security_report-0.18.1 → github_security_report-0.19.1}/.gitignore +0 -0
  34. {github_security_report-0.18.1 → github_security_report-0.19.1}/LICENSE +0 -0
  35. {github_security_report-0.18.1 → github_security_report-0.19.1}/LICENSES/Apache-2.0.txt +0 -0
  36. {github_security_report-0.18.1 → github_security_report-0.19.1}/scripts/README.md +0 -0
  37. {github_security_report-0.18.1 → github_security_report-0.19.1}/src/github_security_report/__init__.py +0 -0
  38. {github_security_report-0.18.1 → github_security_report-0.19.1}/src/github_security_report/categories/__init__.py +0 -0
  39. {github_security_report-0.18.1 → github_security_report-0.19.1}/src/github_security_report/categories/signals.py +0 -0
  40. {github_security_report-0.18.1 → github_security_report-0.19.1}/src/github_security_report/classify.py +0 -0
  41. {github_security_report-0.18.1 → github_security_report-0.19.1}/src/github_security_report/cli/__init__.py +0 -0
  42. {github_security_report-0.18.1 → github_security_report-0.19.1}/src/github_security_report/cli/__main__.py +0 -0
  43. {github_security_report-0.18.1 → github_security_report-0.19.1}/src/github_security_report/cli/boundary.py +0 -0
  44. {github_security_report-0.18.1 → github_security_report-0.19.1}/src/github_security_report/cli/outputs.py +0 -0
  45. {github_security_report-0.18.1 → github_security_report-0.19.1}/src/github_security_report/cli/publish.py +0 -0
  46. {github_security_report-0.18.1 → github_security_report-0.19.1}/src/github_security_report/client/__init__.py +0 -0
  47. {github_security_report-0.18.1 → github_security_report-0.19.1}/src/github_security_report/client/accounting.py +0 -0
  48. {github_security_report-0.18.1 → github_security_report-0.19.1}/src/github_security_report/client/alerts.py +0 -0
  49. {github_security_report-0.18.1 → github_security_report-0.19.1}/src/github_security_report/client/batch_errors.py +0 -0
  50. {github_security_report-0.18.1 → github_security_report-0.19.1}/src/github_security_report/client/codeql_parsers.py +0 -0
  51. {github_security_report-0.18.1 → github_security_report-0.19.1}/src/github_security_report/client/copilot.py +0 -0
  52. {github_security_report-0.18.1 → github_security_report-0.19.1}/src/github_security_report/client/endpoints.py +0 -0
  53. {github_security_report-0.18.1 → github_security_report-0.19.1}/src/github_security_report/client/errors.py +0 -0
  54. {github_security_report-0.18.1 → github_security_report-0.19.1}/src/github_security_report/client/failures.py +0 -0
  55. {github_security_report-0.18.1 → github_security_report-0.19.1}/src/github_security_report/client/reviews.py +0 -0
  56. {github_security_report-0.18.1 → github_security_report-0.19.1}/src/github_security_report/client/transport.py +0 -0
  57. {github_security_report-0.18.1 → github_security_report-0.19.1}/src/github_security_report/codeql/__init__.py +0 -0
  58. {github_security_report-0.18.1 → github_security_report-0.19.1}/src/github_security_report/codeql/cleanup.py +0 -0
  59. {github_security_report-0.18.1 → github_security_report-0.19.1}/src/github_security_report/codeql/facts.py +0 -0
  60. {github_security_report-0.18.1 → github_security_report-0.19.1}/src/github_security_report/codeql/tables.py +0 -0
  61. {github_security_report-0.18.1 → github_security_report-0.19.1}/src/github_security_report/collect/__init__.py +0 -0
  62. {github_security_report-0.18.1 → github_security_report-0.19.1}/src/github_security_report/collect/codeql.py +0 -0
  63. {github_security_report-0.18.1 → github_security_report-0.19.1}/src/github_security_report/collect/context.py +0 -0
  64. {github_security_report-0.18.1 → github_security_report-0.19.1}/src/github_security_report/collect/graph.py +0 -0
  65. {github_security_report-0.18.1 → github_security_report-0.19.1}/src/github_security_report/collect/repo.py +0 -0
  66. {github_security_report-0.18.1 → github_security_report-0.19.1}/src/github_security_report/collect/scoping.py +0 -0
  67. {github_security_report-0.18.1 → github_security_report-0.19.1}/src/github_security_report/config/__init__.py +0 -0
  68. {github_security_report-0.18.1 → github_security_report-0.19.1}/src/github_security_report/config/loader.py +0 -0
  69. {github_security_report-0.18.1 → github_security_report-0.19.1}/src/github_security_report/config/models.py +0 -0
  70. {github_security_report-0.18.1 → github_security_report-0.19.1}/src/github_security_report/config/order.py +0 -0
  71. {github_security_report-0.18.1 → github_security_report-0.19.1}/src/github_security_report/config/schema.py +0 -0
  72. {github_security_report-0.18.1 → github_security_report-0.19.1}/src/github_security_report/gating.py +0 -0
  73. {github_security_report-0.18.1 → github_security_report-0.19.1}/src/github_security_report/gitctx.py +0 -0
  74. {github_security_report-0.18.1 → github_security_report-0.19.1}/src/github_security_report/issues.py +0 -0
  75. {github_security_report-0.18.1 → github_security_report-0.19.1}/src/github_security_report/layout.py +0 -0
  76. {github_security_report-0.18.1 → github_security_report-0.19.1}/src/github_security_report/ordering.py +0 -0
  77. {github_security_report-0.18.1 → github_security_report-0.19.1}/src/github_security_report/posture/enablement.py +0 -0
  78. {github_security_report-0.18.1 → github_security_report-0.19.1}/src/github_security_report/posture/releases.py +0 -0
  79. {github_security_report-0.18.1 → github_security_report-0.19.1}/src/github_security_report/pulls/__init__.py +0 -0
  80. {github_security_report-0.18.1 → github_security_report-0.19.1}/src/github_security_report/pulls/columns.py +0 -0
  81. {github_security_report-0.18.1 → github_security_report-0.19.1}/src/github_security_report/pulls/counting.py +0 -0
  82. {github_security_report-0.18.1 → github_security_report-0.19.1}/src/github_security_report/pulls/presentation.py +0 -0
  83. {github_security_report-0.18.1 → github_security_report-0.19.1}/src/github_security_report/pulls/table.py +0 -0
  84. {github_security_report-0.18.1 → github_security_report-0.19.1}/src/github_security_report/py.typed +0 -0
  85. {github_security_report-0.18.1 → github_security_report-0.19.1}/src/github_security_report/ranking.py +0 -0
  86. {github_security_report-0.18.1 → github_security_report-0.19.1}/src/github_security_report/remediate/codeql.py +0 -0
  87. {github_security_report-0.18.1 → github_security_report-0.19.1}/src/github_security_report/render/__init__.py +0 -0
  88. {github_security_report-0.18.1 → github_security_report-0.19.1}/src/github_security_report/render/html.py +0 -0
  89. {github_security_report-0.18.1 → github_security_report-0.19.1}/src/github_security_report/render/markdown.py +0 -0
  90. {github_security_report-0.18.1 → github_security_report-0.19.1}/src/github_security_report/render/slack.py +0 -0
  91. {github_security_report-0.18.1 → github_security_report-0.19.1}/src/github_security_report/render/slack_limits.py +0 -0
  92. {github_security_report-0.18.1 → github_security_report-0.19.1}/src/github_security_report/report/__init__.py +0 -0
  93. {github_security_report-0.18.1 → github_security_report-0.19.1}/src/github_security_report/report/display.py +0 -0
  94. {github_security_report-0.18.1 → github_security_report-0.19.1}/src/github_security_report/report/signals.py +0 -0
  95. {github_security_report-0.18.1 → github_security_report-0.19.1}/src/github_security_report/rulesets.py +0 -0
  96. {github_security_report-0.18.1 → github_security_report-0.19.1}/src/github_security_report/runner.py +0 -0
  97. {github_security_report-0.18.1 → github_security_report-0.19.1}/src/github_security_report/scope.py +0 -0
  98. {github_security_report-0.18.1 → github_security_report-0.19.1}/src/github_security_report/secret_patterns.py +0 -0
  99. {github_security_report-0.18.1 → github_security_report-0.19.1}/src/github_security_report/severity.py +0 -0
  100. {github_security_report-0.18.1 → github_security_report-0.19.1}/src/github_security_report/templates/index.html.j2 +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: github-security-report
3
- Version: 0.18.1
3
+ Version: 0.19.1
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,7 +36,7 @@ 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.17.0; extra == 'dev'
39
+ Requires-Dist: ruff==0.17.1; extra == 'dev'
40
40
  Requires-Dist: syrupy==6.1.1; extra == 'dev'
41
41
  Requires-Dist: types-jsonschema==4.26.0.20261006; extra == 'dev'
42
42
  Requires-Dist: types-pyyaml==6.0.12.20250915; extra == 'dev'
@@ -76,7 +76,9 @@ states and rendered worst-first:
76
76
 
77
77
  Every category renders the same **standardised summary footer** beneath its
78
78
  table: remediation-first count lines (failures, disabled, unknown, then the
79
- healthy pass line, then excluded). The pass line reads **"All <state>"** when
79
+ healthy pass line, then excluded). A category may add a note qualifying the
80
+ pass line (Code Quality's per-language breakdown) and a **Not applicable**
81
+ count, both shown on every surface. The pass line reads **"All <state>"** when
80
82
  nothing needs attention, or **"N <state>"** otherwise. The terminal and Slack
81
83
  stay brevity-first; the explanatory per-category description and documentation
82
84
  link are shown only on the richer Markdown and HTML (GitHub Pages) outputs.
@@ -154,6 +156,26 @@ tables (org mode):
154
156
  window a reviewed dependency update sits in while the vulnerability it fixes
155
157
  stays unpatched. Read from the batched GraphQL prefetch, so it costs no extra
156
158
  request; hide it with the `auto_merge` render toggle.
159
+ - **Code Quality** — repositories where
160
+ [GitHub Code Quality](https://docs.github.com/en/code-security/concepts/code-quality/code-quality)
161
+ is **Disabled**, or **Partial**: configured, but without a language the
162
+ repository holds. Code Quality analyses six language families, so each
163
+ repository is first classified from its GitHub Linguist languages, read
164
+ from the batched GraphQL prefetch: Python (`python`), Go (`go`), Java and
165
+ Kotlin (`java-kotlin`), JavaScript and TypeScript
166
+ (`javascript-typescript`), Ruby (`ruby`) and C# (`csharp`). A repository
167
+ with none of them, such as one holding only shell scripts and workflow
168
+ files, is **Not applicable**: counted on a line of its own and never
169
+ reported as off. Only applicable repositories have their setup read, with
170
+ `GET /repos/{owner}/{repo}/code-quality/setup`; one whose setup or languages
171
+ cannot be read, or that holds more than the twenty languages the prefetch
172
+ reads, is counted as **Unreadable**, never as Disabled. The
173
+ **Missing** column names the languages each listed repository still needs,
174
+ and a footer line on every surface, the terminal included, gives the
175
+ enabled count out of the applicable count per language, such as
176
+ `python 30/32 · go 5/5 · javascript-typescript 10/12`. See
177
+ [ADR-0006](docs/adr/0006-github-code-quality.md); hide it with the
178
+ `code_quality` render toggle.
157
179
 
158
180
  The four enablement categories count the repositories with and without the
159
181
  feature (plus any whose state could not be read), and by default name whichever
@@ -192,7 +214,13 @@ signal is skipped and every other part of the report is unaffected, so the
192
214
  minimal tokens below omit it. Grant the org-admin permission only if you want
193
215
  ruleset-based tool coverage.
194
216
 
195
- A token without that permission gets a `404` from the endpoint, which the tool
217
+ GitHub Code Quality is gated the same way: a fine-grained token needs
218
+ repository Administration **write** to read a repository's Code Quality setup,
219
+ even though the report only reads it. Without it every repository Code Quality
220
+ applies to is reported as **Unreadable** in that one category; a classic PAT's
221
+ `repo` scope already covers the read.
222
+
223
+ A token without the org-admin permission gets a `404` from the endpoint, which the tool
196
224
  reports at **INFO** — so it is invisible unless you pass `--verbose`:
197
225
 
198
226
  ```text
@@ -212,7 +240,7 @@ A fine-grained PAT is bound to one resource owner, so it works for a report
212
240
  covering a **single** organisation. Create it with **Resource owner** set to the
213
241
  organisation and **Repository access** set to *All repositories*, then grant:
214
242
 
215
- **Repository permissions** (all Read-only):
243
+ **Repository permissions** (Read-only unless noted):
216
244
 
217
245
  | Permission | Used for |
218
246
  | ---------- | -------- |
@@ -222,7 +250,7 @@ organisation and **Repository access** set to *All repositories*, then grant:
222
250
  | Code scanning alerts | CodeQL / Scorecard / zizmor / aislop findings |
223
251
  | Secret scanning alerts | Open secret-scanning alerts, across every GitHub pattern category |
224
252
  | Issues | Open issues and their labels (GitHub Issues table) |
225
- | Administration | Dependabot enablement + security-updates status, and effective branch rules |
253
+ | Administration | Dependabot enablement + security-updates status, and effective branch rules. *Optional* **write** reads the Code Quality setup, which GitHub gates behind write |
226
254
  | Actions | Workflow state behind a stale CodeQL configuration |
227
255
 
228
256
  **Organization permissions:**
@@ -232,7 +260,7 @@ organisation and **Repository access** set to *All repositories*, then grant:
232
260
  | Administration | Read (write for rulesets) | *Optional*, and the only permission here with a write variant. **Read** keeps the token read-only and unlocks GitHub's secret-scanning pattern inventory (see below). **Write** additionally unlocks organisation rulesets (detecting tools enforced through an org ruleset), which GitHub gates behind Administration write. Omit the permission entirely to skip both; everything else is unaffected. |
233
261
 
234
262
  > Read-only is enough for everything except the optional ruleset coverage
235
- > above. A fine-grained token cannot span organisations. For a report covering
263
+ > and the Code Quality setup read above. A fine-grained token cannot span organisations. For a report covering
236
264
  > more than one org, use a classic PAT (below).
237
265
 
238
266
  ### Multiple organisations — classic PAT
@@ -464,10 +492,11 @@ Pages output. The valid category keys are: `codeql`,
464
492
  `zizmor`, `aislop`, `dependabot_alerts`, `secret_scanning`,
465
493
  `dependabot_alerts_enabled`, `dependabot_updates_enabled`,
466
494
  `dependabot_cooldown`, `releases`, `mutable_releases`,
467
- `private_vulnerability_reporting`, `auto_merge`, `github_issues`,
468
- `pull_requests`, `pull_requests_assigned`. Like the other `report` settings,
469
- `categories` can be set globally and overridden per organisation (overrides
470
- merge key-by-key, so flipping one output leaves the rest untouched). The
495
+ `private_vulnerability_reporting`, `auto_merge`, `code_quality`,
496
+ `github_issues`, `pull_requests`, `pull_requests_assigned`. Like the other
497
+ `report` settings, `categories` can be set globally and overridden per
498
+ organisation (overrides merge key-by-key, so flipping one output leaves the
499
+ rest untouched). The
471
500
  machine-readable `report.json` artifact always contains the complete dataset,
472
501
  regardless of these toggles.
473
502
 
@@ -1281,10 +1310,13 @@ see the relationship reports it), but one naming an outsider proves nothing,
1281
1310
  because that is exactly how a private member appears to an under-privileged
1282
1311
  token. Those authors are reported as indeterminate rather than external, so
1283
1312
  `Ext` **undercounts and should be read as a lower bound** — the table says so in
1284
- its description whenever this happens, and the run logs a warning. Membership is
1285
- never used partially: a failed page or an organisation beyond the pagination
1286
- guard is treated as unreadable rather than as a member list with people missing
1287
- from it, since every absent member would otherwise read as an outsider.
1313
+ its description whenever this happens, and the terminal report prints one
1314
+ warning line under each affected organisation's heading, naming the cause: a
1315
+ token without the membership permission, a token whose account is not a member,
1316
+ or a read that failed. Membership is never used partially: a failed page or an
1317
+ organisation beyond the pagination guard is treated as unreadable rather than as
1318
+ a member list with people missing from it, since every absent member would
1319
+ otherwise read as an outsider.
1288
1320
 
1289
1321
  **Automation is never counted as external.** Bots are outsiders by association —
1290
1322
  `dependabot[bot]` genuinely reports `CONTRIBUTOR` or `NONE` — so counting on
@@ -1297,8 +1329,9 @@ association value GitHub has newly introduced — is **not** counted as external
1297
1329
  The column understates rather than inventing an outsider.
1298
1330
 
1299
1331
  **Permissions.** Reading organisation membership needs `read:org` (classic) or
1300
- **Members: read** (fine-grained). Without it the tool logs a warning and
1301
- reports externally-raised counts as a lower bound rather than guessing from
1332
+ **Members: read** (fine-grained). Without it the terminal report prints
1333
+ `⚠️ Token lacks read/organisation permission; results incomplete for <org>`
1334
+ and reports externally-raised counts as a lower bound rather than guessing from
1302
1335
  `authorAssociation` alone.
1303
1336
 
1304
1337
  ### Organisation feature gating
@@ -1648,8 +1681,8 @@ the run, rather than falling back to every repository. The run then collects
1648
1681
  exactly the repositories that check validated, without listing the
1649
1682
  organisation again, and prints `Limited to:` beneath its heading.
1650
1683
 
1651
- The remediable categories are the simple on/off features with a documented
1652
- enablement endpoint:
1684
+ The remediable categories are the features with a documented enablement
1685
+ endpoint:
1653
1686
 
1654
1687
  | `--category` | Enables |
1655
1688
  |---|---|
@@ -1659,15 +1692,48 @@ enablement endpoint:
1659
1692
  | `dependabot_updates_enabled` | Dependabot security updates (plus alerts) |
1660
1693
  | `private_vulnerability_reporting` | Private vulnerability reporting |
1661
1694
  | `auto_merge` | The repository's "Allow auto-merge" setting |
1695
+ | `code_quality` | GitHub Code Quality, per language (named only; billed) |
1696
+ | `codeql_stale_configurations` | Stale CodeQL cleanup (named only; deletes) |
1662
1697
 
1663
1698
  Qualitative findings (Scorecard, zizmor, open Dependabot alerts, cooldown,
1664
1699
  release freshness/mutability) are reported but not auto-remediated. Remediation
1665
1700
  is organisation-scoped (`--scope org`, the default and only supported scope).
1666
1701
 
1702
+ The last two run **only when named** with `--category`, and the no-argument
1703
+ run never includes them: `code_quality`, because GitHub bills it,
1704
+ and `codeql_stale_configurations`, because it deletes data.
1705
+
1706
+ ### Enabling GitHub Code Quality
1707
+
1708
+ GitHub Code Quality is billed per active committer and uses GitHub Actions
1709
+ minutes, so switching it on is a spending decision a routine run must not
1710
+ make:
1711
+
1712
+ ```bash
1713
+ # Preview: each repository with the languages it would get.
1714
+ uvx github-security-report remediate --org lfreleng-actions \
1715
+ --category code_quality
1716
+
1717
+ # Apply.
1718
+ uvx github-security-report remediate --org lfreleng-actions \
1719
+ --category code_quality --apply
1720
+ ```
1721
+
1722
+ It acts on the rows of **Code Quality**, the Disabled and Partial
1723
+ repositories. Each one is configured for the union of the languages its
1724
+ setup already analyses and the ones its Linguist languages make applicable,
1725
+ so a write never drops a configured language; the dry run names them, as in
1726
+ `dependamerge (python, javascript-typescript)`. Not applicable and Unreadable
1727
+ repositories are never written. GitHub provisions the first analysis
1728
+ asynchronously. A `409` (an update already in progress), a `403` (an archived
1729
+ repository, or Code Quality not allowed for it) or a `422` fails that
1730
+ repository only and names the cause. The token needs the classic `repo`
1731
+ scope, or a fine-grained token's Administration: write. See
1732
+ [ADR-0006](docs/adr/0006-github-code-quality.md) for the reasoning.
1733
+
1667
1734
  ### Cleaning up stale CodeQL configurations
1668
1735
 
1669
- One category is **destructive**, so it runs only when named. The no-argument
1670
- run never includes it:
1736
+ This category is **destructive**, so it runs only when named:
1671
1737
 
1672
1738
  ```bash
1673
1739
  # Preview: which configurations would go, and which are refused and why.
@@ -32,7 +32,9 @@ states and rendered worst-first:
32
32
 
33
33
  Every category renders the same **standardised summary footer** beneath its
34
34
  table: remediation-first count lines (failures, disabled, unknown, then the
35
- healthy pass line, then excluded). The pass line reads **"All <state>"** when
35
+ healthy pass line, then excluded). A category may add a note qualifying the
36
+ pass line (Code Quality's per-language breakdown) and a **Not applicable**
37
+ count, both shown on every surface. The pass line reads **"All <state>"** when
36
38
  nothing needs attention, or **"N <state>"** otherwise. The terminal and Slack
37
39
  stay brevity-first; the explanatory per-category description and documentation
38
40
  link are shown only on the richer Markdown and HTML (GitHub Pages) outputs.
@@ -110,6 +112,26 @@ tables (org mode):
110
112
  window a reviewed dependency update sits in while the vulnerability it fixes
111
113
  stays unpatched. Read from the batched GraphQL prefetch, so it costs no extra
112
114
  request; hide it with the `auto_merge` render toggle.
115
+ - **Code Quality** — repositories where
116
+ [GitHub Code Quality](https://docs.github.com/en/code-security/concepts/code-quality/code-quality)
117
+ is **Disabled**, or **Partial**: configured, but without a language the
118
+ repository holds. Code Quality analyses six language families, so each
119
+ repository is first classified from its GitHub Linguist languages, read
120
+ from the batched GraphQL prefetch: Python (`python`), Go (`go`), Java and
121
+ Kotlin (`java-kotlin`), JavaScript and TypeScript
122
+ (`javascript-typescript`), Ruby (`ruby`) and C# (`csharp`). A repository
123
+ with none of them, such as one holding only shell scripts and workflow
124
+ files, is **Not applicable**: counted on a line of its own and never
125
+ reported as off. Only applicable repositories have their setup read, with
126
+ `GET /repos/{owner}/{repo}/code-quality/setup`; one whose setup or languages
127
+ cannot be read, or that holds more than the twenty languages the prefetch
128
+ reads, is counted as **Unreadable**, never as Disabled. The
129
+ **Missing** column names the languages each listed repository still needs,
130
+ and a footer line on every surface, the terminal included, gives the
131
+ enabled count out of the applicable count per language, such as
132
+ `python 30/32 · go 5/5 · javascript-typescript 10/12`. See
133
+ [ADR-0006](docs/adr/0006-github-code-quality.md); hide it with the
134
+ `code_quality` render toggle.
113
135
 
114
136
  The four enablement categories count the repositories with and without the
115
137
  feature (plus any whose state could not be read), and by default name whichever
@@ -148,7 +170,13 @@ signal is skipped and every other part of the report is unaffected, so the
148
170
  minimal tokens below omit it. Grant the org-admin permission only if you want
149
171
  ruleset-based tool coverage.
150
172
 
151
- A token without that permission gets a `404` from the endpoint, which the tool
173
+ GitHub Code Quality is gated the same way: a fine-grained token needs
174
+ repository Administration **write** to read a repository's Code Quality setup,
175
+ even though the report only reads it. Without it every repository Code Quality
176
+ applies to is reported as **Unreadable** in that one category; a classic PAT's
177
+ `repo` scope already covers the read.
178
+
179
+ A token without the org-admin permission gets a `404` from the endpoint, which the tool
152
180
  reports at **INFO** — so it is invisible unless you pass `--verbose`:
153
181
 
154
182
  ```text
@@ -168,7 +196,7 @@ A fine-grained PAT is bound to one resource owner, so it works for a report
168
196
  covering a **single** organisation. Create it with **Resource owner** set to the
169
197
  organisation and **Repository access** set to *All repositories*, then grant:
170
198
 
171
- **Repository permissions** (all Read-only):
199
+ **Repository permissions** (Read-only unless noted):
172
200
 
173
201
  | Permission | Used for |
174
202
  | ---------- | -------- |
@@ -178,7 +206,7 @@ organisation and **Repository access** set to *All repositories*, then grant:
178
206
  | Code scanning alerts | CodeQL / Scorecard / zizmor / aislop findings |
179
207
  | Secret scanning alerts | Open secret-scanning alerts, across every GitHub pattern category |
180
208
  | Issues | Open issues and their labels (GitHub Issues table) |
181
- | Administration | Dependabot enablement + security-updates status, and effective branch rules |
209
+ | Administration | Dependabot enablement + security-updates status, and effective branch rules. *Optional* **write** reads the Code Quality setup, which GitHub gates behind write |
182
210
  | Actions | Workflow state behind a stale CodeQL configuration |
183
211
 
184
212
  **Organization permissions:**
@@ -188,7 +216,7 @@ organisation and **Repository access** set to *All repositories*, then grant:
188
216
  | Administration | Read (write for rulesets) | *Optional*, and the only permission here with a write variant. **Read** keeps the token read-only and unlocks GitHub's secret-scanning pattern inventory (see below). **Write** additionally unlocks organisation rulesets (detecting tools enforced through an org ruleset), which GitHub gates behind Administration write. Omit the permission entirely to skip both; everything else is unaffected. |
189
217
 
190
218
  > Read-only is enough for everything except the optional ruleset coverage
191
- > above. A fine-grained token cannot span organisations. For a report covering
219
+ > and the Code Quality setup read above. A fine-grained token cannot span organisations. For a report covering
192
220
  > more than one org, use a classic PAT (below).
193
221
 
194
222
  ### Multiple organisations — classic PAT
@@ -420,10 +448,11 @@ Pages output. The valid category keys are: `codeql`,
420
448
  `zizmor`, `aislop`, `dependabot_alerts`, `secret_scanning`,
421
449
  `dependabot_alerts_enabled`, `dependabot_updates_enabled`,
422
450
  `dependabot_cooldown`, `releases`, `mutable_releases`,
423
- `private_vulnerability_reporting`, `auto_merge`, `github_issues`,
424
- `pull_requests`, `pull_requests_assigned`. Like the other `report` settings,
425
- `categories` can be set globally and overridden per organisation (overrides
426
- merge key-by-key, so flipping one output leaves the rest untouched). The
451
+ `private_vulnerability_reporting`, `auto_merge`, `code_quality`,
452
+ `github_issues`, `pull_requests`, `pull_requests_assigned`. Like the other
453
+ `report` settings, `categories` can be set globally and overridden per
454
+ organisation (overrides merge key-by-key, so flipping one output leaves the
455
+ rest untouched). The
427
456
  machine-readable `report.json` artifact always contains the complete dataset,
428
457
  regardless of these toggles.
429
458
 
@@ -1237,10 +1266,13 @@ see the relationship reports it), but one naming an outsider proves nothing,
1237
1266
  because that is exactly how a private member appears to an under-privileged
1238
1267
  token. Those authors are reported as indeterminate rather than external, so
1239
1268
  `Ext` **undercounts and should be read as a lower bound** — the table says so in
1240
- its description whenever this happens, and the run logs a warning. Membership is
1241
- never used partially: a failed page or an organisation beyond the pagination
1242
- guard is treated as unreadable rather than as a member list with people missing
1243
- from it, since every absent member would otherwise read as an outsider.
1269
+ its description whenever this happens, and the terminal report prints one
1270
+ warning line under each affected organisation's heading, naming the cause: a
1271
+ token without the membership permission, a token whose account is not a member,
1272
+ or a read that failed. Membership is never used partially: a failed page or an
1273
+ organisation beyond the pagination guard is treated as unreadable rather than as
1274
+ a member list with people missing from it, since every absent member would
1275
+ otherwise read as an outsider.
1244
1276
 
1245
1277
  **Automation is never counted as external.** Bots are outsiders by association —
1246
1278
  `dependabot[bot]` genuinely reports `CONTRIBUTOR` or `NONE` — so counting on
@@ -1253,8 +1285,9 @@ association value GitHub has newly introduced — is **not** counted as external
1253
1285
  The column understates rather than inventing an outsider.
1254
1286
 
1255
1287
  **Permissions.** Reading organisation membership needs `read:org` (classic) or
1256
- **Members: read** (fine-grained). Without it the tool logs a warning and
1257
- reports externally-raised counts as a lower bound rather than guessing from
1288
+ **Members: read** (fine-grained). Without it the terminal report prints
1289
+ `⚠️ Token lacks read/organisation permission; results incomplete for <org>`
1290
+ and reports externally-raised counts as a lower bound rather than guessing from
1258
1291
  `authorAssociation` alone.
1259
1292
 
1260
1293
  ### Organisation feature gating
@@ -1604,8 +1637,8 @@ the run, rather than falling back to every repository. The run then collects
1604
1637
  exactly the repositories that check validated, without listing the
1605
1638
  organisation again, and prints `Limited to:` beneath its heading.
1606
1639
 
1607
- The remediable categories are the simple on/off features with a documented
1608
- enablement endpoint:
1640
+ The remediable categories are the features with a documented enablement
1641
+ endpoint:
1609
1642
 
1610
1643
  | `--category` | Enables |
1611
1644
  |---|---|
@@ -1615,15 +1648,48 @@ enablement endpoint:
1615
1648
  | `dependabot_updates_enabled` | Dependabot security updates (plus alerts) |
1616
1649
  | `private_vulnerability_reporting` | Private vulnerability reporting |
1617
1650
  | `auto_merge` | The repository's "Allow auto-merge" setting |
1651
+ | `code_quality` | GitHub Code Quality, per language (named only; billed) |
1652
+ | `codeql_stale_configurations` | Stale CodeQL cleanup (named only; deletes) |
1618
1653
 
1619
1654
  Qualitative findings (Scorecard, zizmor, open Dependabot alerts, cooldown,
1620
1655
  release freshness/mutability) are reported but not auto-remediated. Remediation
1621
1656
  is organisation-scoped (`--scope org`, the default and only supported scope).
1622
1657
 
1658
+ The last two run **only when named** with `--category`, and the no-argument
1659
+ run never includes them: `code_quality`, because GitHub bills it,
1660
+ and `codeql_stale_configurations`, because it deletes data.
1661
+
1662
+ ### Enabling GitHub Code Quality
1663
+
1664
+ GitHub Code Quality is billed per active committer and uses GitHub Actions
1665
+ minutes, so switching it on is a spending decision a routine run must not
1666
+ make:
1667
+
1668
+ ```bash
1669
+ # Preview: each repository with the languages it would get.
1670
+ uvx github-security-report remediate --org lfreleng-actions \
1671
+ --category code_quality
1672
+
1673
+ # Apply.
1674
+ uvx github-security-report remediate --org lfreleng-actions \
1675
+ --category code_quality --apply
1676
+ ```
1677
+
1678
+ It acts on the rows of **Code Quality**, the Disabled and Partial
1679
+ repositories. Each one is configured for the union of the languages its
1680
+ setup already analyses and the ones its Linguist languages make applicable,
1681
+ so a write never drops a configured language; the dry run names them, as in
1682
+ `dependamerge (python, javascript-typescript)`. Not applicable and Unreadable
1683
+ repositories are never written. GitHub provisions the first analysis
1684
+ asynchronously. A `409` (an update already in progress), a `403` (an archived
1685
+ repository, or Code Quality not allowed for it) or a `422` fails that
1686
+ repository only and names the cause. The token needs the classic `repo`
1687
+ scope, or a fine-grained token's Administration: write. See
1688
+ [ADR-0006](docs/adr/0006-github-code-quality.md) for the reasoning.
1689
+
1623
1690
  ### Cleaning up stale CodeQL configurations
1624
1691
 
1625
- One category is **destructive**, so it runs only when named. The no-argument
1626
- run never includes it:
1692
+ This category is **destructive**, so it runs only when named:
1627
1693
 
1628
1694
  ```bash
1629
1695
  # Preview: which configurations would go, and which are refused and why.
@@ -65,7 +65,7 @@ dev = [
65
65
  "respx==0.23.1",
66
66
  "syrupy==6.1.1",
67
67
  "mypy==2.4.0",
68
- "ruff==0.17.0",
68
+ "ruff==0.17.1",
69
69
  "types-jsonschema==4.26.0.20261006",
70
70
  "types-PyYAML==6.0.12.20250915",
71
71
  ]
@@ -97,7 +97,7 @@ dev = [
97
97
  "respx==0.23.1",
98
98
  "syrupy==6.1.1",
99
99
  "mypy==2.4.0",
100
- "ruff==0.17.0",
100
+ "ruff==0.17.1",
101
101
  "types-jsonschema==4.26.0.20261006",
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.18.1'
22
- __version_tuple__ = version_tuple = (0, 18, 1)
21
+ __version__ = version = '0.19.1'
22
+ __version_tuple__ = version_tuple = (0, 19, 1)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -32,7 +32,9 @@ contributors. Automation is reported in its own column instead.
32
32
 
33
33
  from __future__ import annotations
34
34
 
35
+ import enum
35
36
  from collections.abc import Set
37
+ from dataclasses import dataclass
36
38
 
37
39
  # --------------------------------------------------------------------------- #
38
40
  # Automation authors
@@ -218,3 +220,32 @@ def normalise_members(logins: object) -> frozenset[str]:
218
220
  for login in logins
219
221
  if isinstance(login, str) and (normalised := normalise_login(login))
220
222
  )
223
+
224
+
225
+ class MembershipGap(enum.Enum):
226
+ """Why organisation membership could not be read in full.
227
+
228
+ Each names a different fix, so the run reports which one applies rather
229
+ than folding them into one "could not be read".
230
+ """
231
+
232
+ # The token lacks the organisation-membership permission (classic
233
+ # ``read:org``, fine-grained Members: read).
234
+ PERMISSION = "permission"
235
+ # The token's account is outside the organisation, so GitHub served only
236
+ # its public members.
237
+ NOT_MEMBER = "not_member"
238
+ # Anything else: a failed page, a malformed response, the page limit.
239
+ UNREADABLE = "unreadable"
240
+
241
+
242
+ @dataclass(frozen=True)
243
+ class OrgMembership:
244
+ """An organisation membership read: the members, or why there are none.
245
+
246
+ Exactly one of the two is set. ``members`` of ``None`` carries the meaning
247
+ :func:`is_external_author` gives it, and ``gap`` says why.
248
+ """
249
+
250
+ members: frozenset[str] | None
251
+ gap: MembershipGap | None = None
@@ -34,6 +34,7 @@ class CategoryKey(str, Enum):
34
34
  MUTABLE_RELEASES = "mutable_releases"
35
35
  PRIVATE_VULNERABILITY_REPORTING = "private_vulnerability_reporting"
36
36
  AUTO_MERGE = "auto_merge"
37
+ CODE_QUALITY = "code_quality"
37
38
  GITHUB_ISSUES = "github_issues"
38
39
  PULL_REQUESTS = "pull_requests"
39
40
  PULL_REQUESTS_ASSIGNED = "pull_requests_assigned"
@@ -66,6 +67,9 @@ class CategoryMeta:
66
67
  # Alternative pass wording for the collapsed "All <label>" footer line,
67
68
  # when the counted wording would not read grammatically after "All".
68
69
  pass_all_label: str | None = None
70
+ # Wording for the footer's unknown line, for a category whose unknowns have
71
+ # one specific cause worth naming.
72
+ unknown_label: str = "Unknown"
69
73
  # The lowest finding severity that counts as a failure for this category.
70
74
  # A repository fails (appears as an offender) only when it carries a finding
71
75
  # at or above this rung; findings below it fold into the clean count. The
@@ -170,6 +170,27 @@ TABLE_CATEGORIES: dict[CategoryKey, CategoryMeta] = {
170
170
  "vulnerability it fixes stays unpatched."
171
171
  ),
172
172
  ),
173
+ CategoryKey.CODE_QUALITY: CategoryMeta(
174
+ key=CategoryKey.CODE_QUALITY,
175
+ title="Code Quality",
176
+ pass_label="Enabled",
177
+ fail_label="Disabled or partial",
178
+ unknown_label="Unreadable",
179
+ url=(
180
+ "https://docs.github.com/en/code-security/concepts/code-quality/"
181
+ "code-quality"
182
+ ),
183
+ description=(
184
+ "Repositories holding a language GitHub Code Quality analyses "
185
+ "(python, go, java-kotlin, javascript-typescript, ruby, csharp, as "
186
+ "GitHub's Linguist classifies them) where Code Quality is off "
187
+ "(Disabled) or configured without one of those languages "
188
+ "(Partial). Missing names the languages to add. A repository with "
189
+ "none of them -- shell scripts and workflow files alone, say -- is "
190
+ "Not applicable and never counted as off. Unreadable counts the "
191
+ "repositories whose setup, or whose languages, could not be read."
192
+ ),
193
+ ),
173
194
  CategoryKey.GITHUB_ISSUES: CategoryMeta(
174
195
  key=CategoryKey.GITHUB_ISSUES,
175
196
  title="GitHub Issues",
@@ -199,7 +199,7 @@ def report(
199
199
  )
200
200
  hidden = boundary.resolve_hidden(console, hide)
201
201
 
202
- cfg = _load_config(config_file, config_data, org, token_env, console=console)
202
+ cfg, config_path = _load_config(config_file, config_data, org, token_env)
203
203
  detected = boundary.detect_target(console, repo, scope)
204
204
  mode = boundary.resolve_mode(console, scope, cfg=cfg, detected=detected)
205
205
 
@@ -230,6 +230,7 @@ def report(
230
230
  concurrency=concurrency,
231
231
  ),
232
232
  hidden=hidden,
233
+ config_path=config_path,
233
234
  )
234
235
  code = boundary.run_guarded(console, _run_org(cfg, options, console=console))
235
236
  else:
@@ -271,6 +272,7 @@ def report(
271
272
  report_cfg=cfg.report if cfg is not None else None,
272
273
  limits=TopNLimits(shared=top_n, report=top_n_report, cli=top_n_cli),
273
274
  hidden=hidden,
275
+ config_path=config_path,
274
276
  ),
275
277
  )
276
278
  raise typer.Exit(code)
@@ -301,7 +303,7 @@ def remediate(
301
303
  category: list[str] | None = typer.Option(
302
304
  None,
303
305
  "--category",
304
- help=f"Remediable category to act on (repeatable; default: all except the destructive {_EXPLICIT_HELP}, which run only when named). One of: {_REMEDIABLE_HELP}.",
306
+ help=f"Remediable category to act on (repeatable; default: all except the explicit-only {_EXPLICIT_HELP}, which delete data or incur charges and run only when named). One of: {_REMEDIABLE_HELP}.",
305
307
  ),
306
308
  token_env: str | None = typer.Option(
307
309
  None,
@@ -325,9 +327,9 @@ def remediate(
325
327
  Runs the same collection the report uses, then switches on each selected
326
328
  remediable feature wherever a repository has it confirmed off. Named
327
329
  explicitly, codeql_stale_configurations also deletes orphaned CodeQL
328
- configurations. Dry run by default: pass --apply to make changes. Requires
329
- a write-capable token (org admin), distinct from the read-only reporting
330
- PAT.
330
+ configurations, and code_quality enables billed GitHub Code Quality. Dry
331
+ run by default: pass --apply to make changes. Requires a write-capable
332
+ token (org admin), distinct from the read-only reporting PAT.
331
333
  """
332
334
  console = _console(no_color)
333
335
 
@@ -363,7 +365,7 @@ def remediate(
363
365
  )
364
366
  raise typer.Exit(2)
365
367
 
366
- cfg = _load_config(config_file, config_data, org, token_env, console=console)
368
+ cfg, config_path = _load_config(config_file, config_data, org, token_env)
367
369
  if cfg is None:
368
370
  console.print(
369
371
  "[red]No configuration: provide --config, --config-data or --org.[/red]"
@@ -390,6 +392,7 @@ def remediate(
390
392
  categories=categories,
391
393
  apply=apply,
392
394
  repos=selection,
395
+ config_path=config_path,
393
396
  ),
394
397
  )
395
398
  raise typer.Exit(code)