github-security-report 0.18.0__tar.gz → 0.19.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 (100) hide show
  1. {github_security_report-0.18.0 → github_security_report-0.19.0}/PKG-INFO +167 -31
  2. {github_security_report-0.18.0 → github_security_report-0.19.0}/README.md +162 -26
  3. {github_security_report-0.18.0 → github_security_report-0.19.0}/pyproject.toml +7 -7
  4. {github_security_report-0.18.0 → github_security_report-0.19.0}/src/github_security_report/_version.py +2 -2
  5. {github_security_report-0.18.0 → github_security_report-0.19.0}/src/github_security_report/authors.py +31 -0
  6. {github_security_report-0.18.0 → github_security_report-0.19.0}/src/github_security_report/categories/keys.py +4 -0
  7. {github_security_report-0.18.0 → github_security_report-0.19.0}/src/github_security_report/categories/tables.py +21 -0
  8. {github_security_report-0.18.0 → github_security_report-0.19.0}/src/github_security_report/cli/app.py +20 -6
  9. {github_security_report-0.18.0 → github_security_report-0.19.0}/src/github_security_report/cli/boundary.py +8 -2
  10. {github_security_report-0.18.0 → github_security_report-0.19.0}/src/github_security_report/cli/modes.py +52 -22
  11. {github_security_report-0.18.0 → github_security_report-0.19.0}/src/github_security_report/cli/options.py +12 -7
  12. {github_security_report-0.18.0 → github_security_report-0.19.0}/src/github_security_report/cli/serialise.py +3 -0
  13. {github_security_report-0.18.0 → github_security_report-0.19.0}/src/github_security_report/client/__init__.py +10 -7
  14. github_security_report-0.19.0/src/github_security_report/client/accounting.py +161 -0
  15. {github_security_report-0.18.0 → github_security_report-0.19.0}/src/github_security_report/client/endpoints.py +8 -0
  16. github_security_report-0.19.0/src/github_security_report/client/failures.py +115 -0
  17. {github_security_report-0.18.0 → github_security_report-0.19.0}/src/github_security_report/client/org_reads.py +71 -47
  18. {github_security_report-0.18.0 → github_security_report-0.19.0}/src/github_security_report/client/parsers.py +2 -0
  19. {github_security_report-0.18.0 → github_security_report-0.19.0}/src/github_security_report/client/queries.py +12 -3
  20. {github_security_report-0.18.0 → github_security_report-0.19.0}/src/github_security_report/client/reads.py +20 -0
  21. {github_security_report-0.18.0 → github_security_report-0.19.0}/src/github_security_report/client/transport.py +41 -96
  22. {github_security_report-0.18.0 → github_security_report-0.19.0}/src/github_security_report/client/writes.py +32 -0
  23. github_security_report-0.19.0/src/github_security_report/code_quality.py +164 -0
  24. {github_security_report-0.18.0 → github_security_report-0.19.0}/src/github_security_report/collect/extras.py +88 -43
  25. github_security_report-0.19.0/src/github_security_report/collect/graph.py +106 -0
  26. {github_security_report-0.18.0 → github_security_report-0.19.0}/src/github_security_report/collect/org.py +17 -61
  27. {github_security_report-0.18.0 → github_security_report-0.19.0}/src/github_security_report/collect/protocols.py +11 -4
  28. {github_security_report-0.18.0 → github_security_report-0.19.0}/src/github_security_report/config/__init__.py +2 -0
  29. {github_security_report-0.18.0 → github_security_report-0.19.0}/src/github_security_report/config/loader.py +1 -0
  30. {github_security_report-0.18.0 → github_security_report-0.19.0}/src/github_security_report/config/models.py +7 -0
  31. {github_security_report-0.18.0 → github_security_report-0.19.0}/src/github_security_report/config/schema.py +11 -0
  32. {github_security_report-0.18.0 → github_security_report-0.19.0}/src/github_security_report/models.py +4 -0
  33. {github_security_report-0.18.0 → github_security_report-0.19.0}/src/github_security_report/posture/__init__.py +10 -3
  34. {github_security_report-0.18.0 → github_security_report-0.19.0}/src/github_security_report/posture/facts.py +8 -0
  35. github_security_report-0.19.0/src/github_security_report/posture/quality.py +96 -0
  36. {github_security_report-0.18.0 → github_security_report-0.19.0}/src/github_security_report/remediate/__init__.py +42 -3
  37. {github_security_report-0.18.0 → github_security_report-0.19.0}/src/github_security_report/remediate/model.py +7 -1
  38. {github_security_report-0.18.0 → github_security_report-0.19.0}/src/github_security_report/render/terminal.py +12 -0
  39. github_security_report-0.19.0/src/github_security_report/render/terminal_notices.py +48 -0
  40. {github_security_report-0.18.0 → github_security_report-0.19.0}/src/github_security_report/report/aggregate.py +14 -0
  41. {github_security_report-0.18.0 → github_security_report-0.19.0}/src/github_security_report/report/tables.py +13 -2
  42. {github_security_report-0.18.0 → github_security_report-0.19.0}/src/github_security_report/summary.py +22 -6
  43. {github_security_report-0.18.0 → github_security_report-0.19.0}/src/github_security_report/templates/report.html.j2 +2 -0
  44. {github_security_report-0.18.0 → github_security_report-0.19.0}/.gitignore +0 -0
  45. {github_security_report-0.18.0 → github_security_report-0.19.0}/LICENSE +0 -0
  46. {github_security_report-0.18.0 → github_security_report-0.19.0}/LICENSES/Apache-2.0.txt +0 -0
  47. {github_security_report-0.18.0 → github_security_report-0.19.0}/scripts/README.md +0 -0
  48. {github_security_report-0.18.0 → github_security_report-0.19.0}/src/github_security_report/__init__.py +0 -0
  49. {github_security_report-0.18.0 → github_security_report-0.19.0}/src/github_security_report/categories/__init__.py +0 -0
  50. {github_security_report-0.18.0 → github_security_report-0.19.0}/src/github_security_report/categories/signals.py +0 -0
  51. {github_security_report-0.18.0 → github_security_report-0.19.0}/src/github_security_report/classify.py +0 -0
  52. {github_security_report-0.18.0 → github_security_report-0.19.0}/src/github_security_report/cli/__init__.py +0 -0
  53. {github_security_report-0.18.0 → github_security_report-0.19.0}/src/github_security_report/cli/__main__.py +0 -0
  54. {github_security_report-0.18.0 → github_security_report-0.19.0}/src/github_security_report/cli/outputs.py +0 -0
  55. {github_security_report-0.18.0 → github_security_report-0.19.0}/src/github_security_report/cli/publish.py +0 -0
  56. {github_security_report-0.18.0 → github_security_report-0.19.0}/src/github_security_report/client/alerts.py +0 -0
  57. {github_security_report-0.18.0 → github_security_report-0.19.0}/src/github_security_report/client/batch_errors.py +0 -0
  58. {github_security_report-0.18.0 → github_security_report-0.19.0}/src/github_security_report/client/codeql_parsers.py +0 -0
  59. {github_security_report-0.18.0 → github_security_report-0.19.0}/src/github_security_report/client/copilot.py +0 -0
  60. {github_security_report-0.18.0 → github_security_report-0.19.0}/src/github_security_report/client/errors.py +0 -0
  61. {github_security_report-0.18.0 → github_security_report-0.19.0}/src/github_security_report/client/reviews.py +0 -0
  62. {github_security_report-0.18.0 → github_security_report-0.19.0}/src/github_security_report/codeql/__init__.py +0 -0
  63. {github_security_report-0.18.0 → github_security_report-0.19.0}/src/github_security_report/codeql/cleanup.py +0 -0
  64. {github_security_report-0.18.0 → github_security_report-0.19.0}/src/github_security_report/codeql/facts.py +0 -0
  65. {github_security_report-0.18.0 → github_security_report-0.19.0}/src/github_security_report/codeql/tables.py +0 -0
  66. {github_security_report-0.18.0 → github_security_report-0.19.0}/src/github_security_report/collect/__init__.py +0 -0
  67. {github_security_report-0.18.0 → github_security_report-0.19.0}/src/github_security_report/collect/codeql.py +0 -0
  68. {github_security_report-0.18.0 → github_security_report-0.19.0}/src/github_security_report/collect/context.py +0 -0
  69. {github_security_report-0.18.0 → github_security_report-0.19.0}/src/github_security_report/collect/repo.py +0 -0
  70. {github_security_report-0.18.0 → github_security_report-0.19.0}/src/github_security_report/collect/scoping.py +0 -0
  71. {github_security_report-0.18.0 → github_security_report-0.19.0}/src/github_security_report/config/order.py +0 -0
  72. {github_security_report-0.18.0 → github_security_report-0.19.0}/src/github_security_report/gating.py +0 -0
  73. {github_security_report-0.18.0 → github_security_report-0.19.0}/src/github_security_report/gitctx.py +0 -0
  74. {github_security_report-0.18.0 → github_security_report-0.19.0}/src/github_security_report/issues.py +0 -0
  75. {github_security_report-0.18.0 → github_security_report-0.19.0}/src/github_security_report/layout.py +0 -0
  76. {github_security_report-0.18.0 → github_security_report-0.19.0}/src/github_security_report/ordering.py +0 -0
  77. {github_security_report-0.18.0 → github_security_report-0.19.0}/src/github_security_report/posture/enablement.py +0 -0
  78. {github_security_report-0.18.0 → github_security_report-0.19.0}/src/github_security_report/posture/releases.py +0 -0
  79. {github_security_report-0.18.0 → github_security_report-0.19.0}/src/github_security_report/pulls/__init__.py +0 -0
  80. {github_security_report-0.18.0 → github_security_report-0.19.0}/src/github_security_report/pulls/columns.py +0 -0
  81. {github_security_report-0.18.0 → github_security_report-0.19.0}/src/github_security_report/pulls/counting.py +0 -0
  82. {github_security_report-0.18.0 → github_security_report-0.19.0}/src/github_security_report/pulls/presentation.py +0 -0
  83. {github_security_report-0.18.0 → github_security_report-0.19.0}/src/github_security_report/pulls/table.py +0 -0
  84. {github_security_report-0.18.0 → github_security_report-0.19.0}/src/github_security_report/py.typed +0 -0
  85. {github_security_report-0.18.0 → github_security_report-0.19.0}/src/github_security_report/ranking.py +0 -0
  86. {github_security_report-0.18.0 → github_security_report-0.19.0}/src/github_security_report/remediate/codeql.py +0 -0
  87. {github_security_report-0.18.0 → github_security_report-0.19.0}/src/github_security_report/render/__init__.py +0 -0
  88. {github_security_report-0.18.0 → github_security_report-0.19.0}/src/github_security_report/render/html.py +0 -0
  89. {github_security_report-0.18.0 → github_security_report-0.19.0}/src/github_security_report/render/markdown.py +0 -0
  90. {github_security_report-0.18.0 → github_security_report-0.19.0}/src/github_security_report/render/slack.py +0 -0
  91. {github_security_report-0.18.0 → github_security_report-0.19.0}/src/github_security_report/render/slack_limits.py +0 -0
  92. {github_security_report-0.18.0 → github_security_report-0.19.0}/src/github_security_report/report/__init__.py +0 -0
  93. {github_security_report-0.18.0 → github_security_report-0.19.0}/src/github_security_report/report/display.py +0 -0
  94. {github_security_report-0.18.0 → github_security_report-0.19.0}/src/github_security_report/report/signals.py +0 -0
  95. {github_security_report-0.18.0 → github_security_report-0.19.0}/src/github_security_report/rulesets.py +0 -0
  96. {github_security_report-0.18.0 → github_security_report-0.19.0}/src/github_security_report/runner.py +0 -0
  97. {github_security_report-0.18.0 → github_security_report-0.19.0}/src/github_security_report/scope.py +0 -0
  98. {github_security_report-0.18.0 → github_security_report-0.19.0}/src/github_security_report/secret_patterns.py +0 -0
  99. {github_security_report-0.18.0 → github_security_report-0.19.0}/src/github_security_report/severity.py +0 -0
  100. {github_security_report-0.18.0 → github_security_report-0.19.0}/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.0
3
+ Version: 0.19.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
@@ -29,16 +29,16 @@ Requires-Dist: jinja2==3.1.6
29
29
  Requires-Dist: jsonschema==4.26.0
30
30
  Requires-Dist: pyyaml==6.0.3
31
31
  Requires-Dist: rich==15.0.0
32
- Requires-Dist: typer==0.27.2
32
+ Requires-Dist: typer==0.27.3
33
33
  Provides-Extra: dev
34
- Requires-Dist: mypy==2.3.1; extra == 'dev'
34
+ Requires-Dist: mypy==2.4.0; extra == 'dev'
35
35
  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.9; extra == 'dev'
39
+ Requires-Dist: ruff==0.17.0; extra == 'dev'
40
40
  Requires-Dist: syrupy==6.1.1; extra == 'dev'
41
- Requires-Dist: types-jsonschema==4.26.0.20260518; extra == 'dev'
41
+ Requires-Dist: types-jsonschema==4.26.0.20261006; extra == 'dev'
42
42
  Requires-Dist: types-pyyaml==6.0.12.20250915; extra == 'dev'
43
43
  Description-Content-Type: text/markdown
44
44
 
@@ -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
@@ -348,6 +376,7 @@ environment-variable name, never embedded.
348
376
  "issue_age_warn_days": 30,
349
377
  "issue_age_error_days": 60,
350
378
  "graph_batch": 10,
379
+ "concurrency": 9,
351
380
  "order": { "style": "auto" }
352
381
  },
353
382
  "organizations": [
@@ -463,10 +492,11 @@ Pages output. The valid category keys are: `codeql`,
463
492
  `zizmor`, `aislop`, `dependabot_alerts`, `secret_scanning`,
464
493
  `dependabot_alerts_enabled`, `dependabot_updates_enabled`,
465
494
  `dependabot_cooldown`, `releases`, `mutable_releases`,
466
- `private_vulnerability_reporting`, `auto_merge`, `github_issues`,
467
- `pull_requests`, `pull_requests_assigned`. Like the other `report` settings,
468
- `categories` can be set globally and overridden per organisation (overrides
469
- 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
470
500
  machine-readable `report.json` artifact always contains the complete dataset,
471
501
  regardless of these toggles.
472
502
 
@@ -1280,10 +1310,13 @@ see the relationship reports it), but one naming an outsider proves nothing,
1280
1310
  because that is exactly how a private member appears to an under-privileged
1281
1311
  token. Those authors are reported as indeterminate rather than external, so
1282
1312
  `Ext` **undercounts and should be read as a lower bound** — the table says so in
1283
- its description whenever this happens, and the run logs a warning. Membership is
1284
- never used partially: a failed page or an organisation beyond the pagination
1285
- guard is treated as unreadable rather than as a member list with people missing
1286
- 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.
1287
1320
 
1288
1321
  **Automation is never counted as external.** Bots are outsiders by association —
1289
1322
  `dependabot[bot]` genuinely reports `CONTRIBUTOR` or `NONE` — so counting on
@@ -1296,8 +1329,9 @@ association value GitHub has newly introduced — is **not** counted as external
1296
1329
  The column understates rather than inventing an outsider.
1297
1330
 
1298
1331
  **Permissions.** Reading organisation membership needs `read:org` (classic) or
1299
- **Members: read** (fine-grained). Without it the tool logs a warning and
1300
- 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
1301
1335
  `authorAssociation` alone.
1302
1336
 
1303
1337
  ### Organisation feature gating
@@ -1345,16 +1379,25 @@ size is bounded by *latency*, not by rate-limit cost: 25 repositories measured
1345
1379
  at ~9 s against `lfreleng-actions` and failed intermittently; 10 measured at
1346
1380
  ~3.5 s.
1347
1381
 
1382
+ Up to four of these queries run at once. Each takes a few seconds on GitHub's
1383
+ side, so a prefetch issued one query at a time left most of the client's
1384
+ concurrency idle; against `lfreleng-actions` it took about 50 s of a 100 s run.
1385
+
1348
1386
  `report.graph_batch` (default `10`; minimum `1`) sets the starting batch size,
1349
1387
  globally or per organisation. It is a lever for a slow day rather than a hard
1350
1388
  limit: a batch that still fails on size after the transport's own retries is
1351
- re-issued at **half** the size, and the smaller size is kept for the rest of
1352
- that organisation's collection (each organisation in a multi-org run starts
1353
- from its own configured size, since organisations differ in how much data a
1354
- repository carries). Only a single-repository query GitHub still cannot
1355
- answer, or a failure that a smaller query could not fix (a `403`, an
1356
- exhausted rate limit, or a `500`/`503` outage rather than a `502`/`504`
1357
- timeout), aborts the run with exit code `3`.
1389
+ re-issued at **half** the size, and every batch issued after that failure is
1390
+ seen uses the smaller size for the rest of that organisation's collection
1391
+ (each organisation in a multi-org run starts from its own configured size,
1392
+ since organisations differ in how much data a repository carries). The
1393
+ learned size only shrinks. Because queries overlap, the batches already in
1394
+ flight when the first failure arrives were issued at the old size, so on a
1395
+ slow day up to four oversized batches, rather than one, may fail before the
1396
+ smaller size takes effect; that bounded cost buys the overlap. Only a
1397
+ single-repository query GitHub still cannot answer, or a failure that a
1398
+ smaller query could not fix (a `403`, an exhausted rate limit, or a
1399
+ `500`/`503` outage rather than a `502`/`504` timeout), aborts the run with
1400
+ exit code `3`, and the queries still in flight are cancelled first.
1358
1401
 
1359
1402
  The value can be set three ways, in this order of precedence:
1360
1403
 
@@ -1379,6 +1422,65 @@ a larger value buys nothing except a longer first failure.
1379
1422
  > predating the flag will reject a set one with `No such option`, so set
1380
1423
  > `GSR_GRAPH_BATCH` only once the pinned release supports it.
1381
1424
 
1425
+ ### Client concurrency
1426
+
1427
+ Each organisation's run holds at most `report.concurrency` requests to the
1428
+ GitHub API open at once (default `9`; minimum `1`, maximum `32`), globally or
1429
+ per organisation. Reads from the external Scorecard API queue on a separate
1430
+ limit of four, so a slow third-party endpoint never holds a slot that a
1431
+ GitHub request is waiting for.
1432
+
1433
+ The value can be set three ways, in this order of precedence:
1434
+
1435
+ 1. `--concurrency N` on the command line (the action's `concurrency` input).
1436
+ 2. A **repository or organisation variable** passed to that input. The
1437
+ bundled `reporting.yaml` passes `vars.GSR_CONCURRENCY`, so the value can be
1438
+ adjusted from repository settings without a workflow edit or a release.
1439
+ 3. `report.concurrency` in the configuration; otherwise the built-in `9`.
1440
+
1441
+ `9` is a deliberate halfway point. GitHub documents its secondary rate limits
1442
+ as no more than 100 concurrent requests across REST and GraphQL, and no more
1443
+ than 900 points per minute for REST endpoints; this project has hit those
1444
+ limits before, so the default stays well clear of them. Measured against
1445
+ `lfreleng-actions` (143 repositories, about 1,400 requests; one run each in
1446
+ the same hour, so read ±10–15 % as GitHub latency noise), before the GraphQL
1447
+ prefetch ran concurrently:
1448
+
1449
+ <!-- markdownlint-disable MD013 -->
1450
+
1451
+ | Concurrency | Wall | Throughput | Mean time per request | Average slots busy | GitHub pushback |
1452
+ | ----------- | ---: | ---------: | --------------------: | -----------------: | --------------- |
1453
+ | 6 (former default) | 102.5 s | 13.7 req/s | 0.283 s | 3.9 / 6 | none |
1454
+ | 12 | 75.9 s | 18.5 req/s | 0.286 s | 5.3 / 12 | none |
1455
+
1456
+ <!-- markdownlint-enable MD013 -->
1457
+
1458
+ Raise it once a run shows no pushback; lower it if GitHub starts answering
1459
+ with secondary rate-limit `403`/`429` responses. To measure a change, run the
1460
+ CLI with `--verbose`: when each organisation's collection finishes it logs
1461
+ one line of what the run cost, for example
1462
+
1463
+ <!-- markdownlint-disable MD013 -->
1464
+
1465
+ ```text
1466
+ GitHub API for lfreleng-actions: 1402 requests (0 retries), 822 core points, 64 graphql points, 41.3 MiB received, 102.5 s
1467
+ ```
1468
+
1469
+ <!-- markdownlint-enable MD013 -->
1470
+
1471
+ The request count is every request the tool issued, including retries and
1472
+ attempts that never got an answer. The points come from GitHub's own
1473
+ `x-ratelimit-*` response headers rather than from counting: GitHub meters a
1474
+ token across more than one bucket per resource (alike requests seconds apart
1475
+ can carry different `x-ratelimit-reset` values with independent counters), so
1476
+ each bucket's spread between its lowest and highest reading is summed per
1477
+ resource. The lowest reading already includes that request's own cost, so the
1478
+ figure is short by the first request of each bucket — a measurement aid, not a
1479
+ bill. Nothing of this line reaches the rendered report or the step summary.
1480
+
1481
+ > As with `graph_batch`, an empty input is not passed to the tool, so set
1482
+ > `GSR_CONCURRENCY` only once the pinned release supports `--concurrency`.
1483
+
1382
1484
  ### Pass/fail severity cutoff
1383
1485
 
1384
1486
  The severity-ranked signals (CodeQL, Scorecard, Zizmor, aislop, Dependabot
@@ -1457,6 +1559,7 @@ and the Slack **bot token** is consumed by the workflow, not the CLI.
1457
1559
  | `force_notify` | No | `false` | Post to Slack regardless of `report_day` |
1458
1560
  | `hide` | No | `""` | Category keys to suppress on every output (space- or comma-separated). Overrides config, and is one-way: it cannot re-enable a disabled category |
1459
1561
  | `graph_batch` | No | `""` | Repositories per batched GraphQL prefetch query (minimum `1`; default: config, else `10`). Pass a repository or organisation variable such as `${{ vars.GSR_GRAPH_BATCH }}` to adjust it from settings. A batch that still fails is halved automatically, so this sets the starting size (see [GraphQL prefetch batch size](#graphql-prefetch-batch-size)) |
1562
+ | `concurrency` | No | `""` | Concurrent GitHub API requests per organisation (`1` to `32`; default: config, else `9`). Pass a repository or organisation variable such as `${{ vars.GSR_CONCURRENCY }}` to adjust it from settings (see [Client concurrency](#client-concurrency)) |
1460
1563
  | `tool_version` | No | `""` | Published PyPI version to install. Empty (the default) uses the Dependabot-managed pin in `.github/runtime-pin/requirements.txt`; set a specific version to override. Ignored on pull requests or when `use_local_source` is `true` (both run from source) |
1461
1564
  | `use_local_source` | No | `false` | Run from the checked-out source instead of PyPI (for testing unreleased code from any event) |
1462
1565
 
@@ -1578,8 +1681,8 @@ the run, rather than falling back to every repository. The run then collects
1578
1681
  exactly the repositories that check validated, without listing the
1579
1682
  organisation again, and prints `Limited to:` beneath its heading.
1580
1683
 
1581
- The remediable categories are the simple on/off features with a documented
1582
- enablement endpoint:
1684
+ The remediable categories are the features with a documented enablement
1685
+ endpoint:
1583
1686
 
1584
1687
  | `--category` | Enables |
1585
1688
  |---|---|
@@ -1589,15 +1692,48 @@ enablement endpoint:
1589
1692
  | `dependabot_updates_enabled` | Dependabot security updates (plus alerts) |
1590
1693
  | `private_vulnerability_reporting` | Private vulnerability reporting |
1591
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) |
1592
1697
 
1593
1698
  Qualitative findings (Scorecard, zizmor, open Dependabot alerts, cooldown,
1594
1699
  release freshness/mutability) are reported but not auto-remediated. Remediation
1595
1700
  is organisation-scoped (`--scope org`, the default and only supported scope).
1596
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
+
1597
1734
  ### Cleaning up stale CodeQL configurations
1598
1735
 
1599
- One category is **destructive**, so it runs only when named. The no-argument
1600
- run never includes it:
1736
+ This category is **destructive**, so it runs only when named:
1601
1737
 
1602
1738
  ```bash
1603
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
@@ -304,6 +332,7 @@ environment-variable name, never embedded.
304
332
  "issue_age_warn_days": 30,
305
333
  "issue_age_error_days": 60,
306
334
  "graph_batch": 10,
335
+ "concurrency": 9,
307
336
  "order": { "style": "auto" }
308
337
  },
309
338
  "organizations": [
@@ -419,10 +448,11 @@ Pages output. The valid category keys are: `codeql`,
419
448
  `zizmor`, `aislop`, `dependabot_alerts`, `secret_scanning`,
420
449
  `dependabot_alerts_enabled`, `dependabot_updates_enabled`,
421
450
  `dependabot_cooldown`, `releases`, `mutable_releases`,
422
- `private_vulnerability_reporting`, `auto_merge`, `github_issues`,
423
- `pull_requests`, `pull_requests_assigned`. Like the other `report` settings,
424
- `categories` can be set globally and overridden per organisation (overrides
425
- 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
426
456
  machine-readable `report.json` artifact always contains the complete dataset,
427
457
  regardless of these toggles.
428
458
 
@@ -1236,10 +1266,13 @@ see the relationship reports it), but one naming an outsider proves nothing,
1236
1266
  because that is exactly how a private member appears to an under-privileged
1237
1267
  token. Those authors are reported as indeterminate rather than external, so
1238
1268
  `Ext` **undercounts and should be read as a lower bound** — the table says so in
1239
- its description whenever this happens, and the run logs a warning. Membership is
1240
- never used partially: a failed page or an organisation beyond the pagination
1241
- guard is treated as unreadable rather than as a member list with people missing
1242
- 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.
1243
1276
 
1244
1277
  **Automation is never counted as external.** Bots are outsiders by association —
1245
1278
  `dependabot[bot]` genuinely reports `CONTRIBUTOR` or `NONE` — so counting on
@@ -1252,8 +1285,9 @@ association value GitHub has newly introduced — is **not** counted as external
1252
1285
  The column understates rather than inventing an outsider.
1253
1286
 
1254
1287
  **Permissions.** Reading organisation membership needs `read:org` (classic) or
1255
- **Members: read** (fine-grained). Without it the tool logs a warning and
1256
- 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
1257
1291
  `authorAssociation` alone.
1258
1292
 
1259
1293
  ### Organisation feature gating
@@ -1301,16 +1335,25 @@ size is bounded by *latency*, not by rate-limit cost: 25 repositories measured
1301
1335
  at ~9 s against `lfreleng-actions` and failed intermittently; 10 measured at
1302
1336
  ~3.5 s.
1303
1337
 
1338
+ Up to four of these queries run at once. Each takes a few seconds on GitHub's
1339
+ side, so a prefetch issued one query at a time left most of the client's
1340
+ concurrency idle; against `lfreleng-actions` it took about 50 s of a 100 s run.
1341
+
1304
1342
  `report.graph_batch` (default `10`; minimum `1`) sets the starting batch size,
1305
1343
  globally or per organisation. It is a lever for a slow day rather than a hard
1306
1344
  limit: a batch that still fails on size after the transport's own retries is
1307
- re-issued at **half** the size, and the smaller size is kept for the rest of
1308
- that organisation's collection (each organisation in a multi-org run starts
1309
- from its own configured size, since organisations differ in how much data a
1310
- repository carries). Only a single-repository query GitHub still cannot
1311
- answer, or a failure that a smaller query could not fix (a `403`, an
1312
- exhausted rate limit, or a `500`/`503` outage rather than a `502`/`504`
1313
- timeout), aborts the run with exit code `3`.
1345
+ re-issued at **half** the size, and every batch issued after that failure is
1346
+ seen uses the smaller size for the rest of that organisation's collection
1347
+ (each organisation in a multi-org run starts from its own configured size,
1348
+ since organisations differ in how much data a repository carries). The
1349
+ learned size only shrinks. Because queries overlap, the batches already in
1350
+ flight when the first failure arrives were issued at the old size, so on a
1351
+ slow day up to four oversized batches, rather than one, may fail before the
1352
+ smaller size takes effect; that bounded cost buys the overlap. Only a
1353
+ single-repository query GitHub still cannot answer, or a failure that a
1354
+ smaller query could not fix (a `403`, an exhausted rate limit, or a
1355
+ `500`/`503` outage rather than a `502`/`504` timeout), aborts the run with
1356
+ exit code `3`, and the queries still in flight are cancelled first.
1314
1357
 
1315
1358
  The value can be set three ways, in this order of precedence:
1316
1359
 
@@ -1335,6 +1378,65 @@ a larger value buys nothing except a longer first failure.
1335
1378
  > predating the flag will reject a set one with `No such option`, so set
1336
1379
  > `GSR_GRAPH_BATCH` only once the pinned release supports it.
1337
1380
 
1381
+ ### Client concurrency
1382
+
1383
+ Each organisation's run holds at most `report.concurrency` requests to the
1384
+ GitHub API open at once (default `9`; minimum `1`, maximum `32`), globally or
1385
+ per organisation. Reads from the external Scorecard API queue on a separate
1386
+ limit of four, so a slow third-party endpoint never holds a slot that a
1387
+ GitHub request is waiting for.
1388
+
1389
+ The value can be set three ways, in this order of precedence:
1390
+
1391
+ 1. `--concurrency N` on the command line (the action's `concurrency` input).
1392
+ 2. A **repository or organisation variable** passed to that input. The
1393
+ bundled `reporting.yaml` passes `vars.GSR_CONCURRENCY`, so the value can be
1394
+ adjusted from repository settings without a workflow edit or a release.
1395
+ 3. `report.concurrency` in the configuration; otherwise the built-in `9`.
1396
+
1397
+ `9` is a deliberate halfway point. GitHub documents its secondary rate limits
1398
+ as no more than 100 concurrent requests across REST and GraphQL, and no more
1399
+ than 900 points per minute for REST endpoints; this project has hit those
1400
+ limits before, so the default stays well clear of them. Measured against
1401
+ `lfreleng-actions` (143 repositories, about 1,400 requests; one run each in
1402
+ the same hour, so read ±10–15 % as GitHub latency noise), before the GraphQL
1403
+ prefetch ran concurrently:
1404
+
1405
+ <!-- markdownlint-disable MD013 -->
1406
+
1407
+ | Concurrency | Wall | Throughput | Mean time per request | Average slots busy | GitHub pushback |
1408
+ | ----------- | ---: | ---------: | --------------------: | -----------------: | --------------- |
1409
+ | 6 (former default) | 102.5 s | 13.7 req/s | 0.283 s | 3.9 / 6 | none |
1410
+ | 12 | 75.9 s | 18.5 req/s | 0.286 s | 5.3 / 12 | none |
1411
+
1412
+ <!-- markdownlint-enable MD013 -->
1413
+
1414
+ Raise it once a run shows no pushback; lower it if GitHub starts answering
1415
+ with secondary rate-limit `403`/`429` responses. To measure a change, run the
1416
+ CLI with `--verbose`: when each organisation's collection finishes it logs
1417
+ one line of what the run cost, for example
1418
+
1419
+ <!-- markdownlint-disable MD013 -->
1420
+
1421
+ ```text
1422
+ GitHub API for lfreleng-actions: 1402 requests (0 retries), 822 core points, 64 graphql points, 41.3 MiB received, 102.5 s
1423
+ ```
1424
+
1425
+ <!-- markdownlint-enable MD013 -->
1426
+
1427
+ The request count is every request the tool issued, including retries and
1428
+ attempts that never got an answer. The points come from GitHub's own
1429
+ `x-ratelimit-*` response headers rather than from counting: GitHub meters a
1430
+ token across more than one bucket per resource (alike requests seconds apart
1431
+ can carry different `x-ratelimit-reset` values with independent counters), so
1432
+ each bucket's spread between its lowest and highest reading is summed per
1433
+ resource. The lowest reading already includes that request's own cost, so the
1434
+ figure is short by the first request of each bucket — a measurement aid, not a
1435
+ bill. Nothing of this line reaches the rendered report or the step summary.
1436
+
1437
+ > As with `graph_batch`, an empty input is not passed to the tool, so set
1438
+ > `GSR_CONCURRENCY` only once the pinned release supports `--concurrency`.
1439
+
1338
1440
  ### Pass/fail severity cutoff
1339
1441
 
1340
1442
  The severity-ranked signals (CodeQL, Scorecard, Zizmor, aislop, Dependabot
@@ -1413,6 +1515,7 @@ and the Slack **bot token** is consumed by the workflow, not the CLI.
1413
1515
  | `force_notify` | No | `false` | Post to Slack regardless of `report_day` |
1414
1516
  | `hide` | No | `""` | Category keys to suppress on every output (space- or comma-separated). Overrides config, and is one-way: it cannot re-enable a disabled category |
1415
1517
  | `graph_batch` | No | `""` | Repositories per batched GraphQL prefetch query (minimum `1`; default: config, else `10`). Pass a repository or organisation variable such as `${{ vars.GSR_GRAPH_BATCH }}` to adjust it from settings. A batch that still fails is halved automatically, so this sets the starting size (see [GraphQL prefetch batch size](#graphql-prefetch-batch-size)) |
1518
+ | `concurrency` | No | `""` | Concurrent GitHub API requests per organisation (`1` to `32`; default: config, else `9`). Pass a repository or organisation variable such as `${{ vars.GSR_CONCURRENCY }}` to adjust it from settings (see [Client concurrency](#client-concurrency)) |
1416
1519
  | `tool_version` | No | `""` | Published PyPI version to install. Empty (the default) uses the Dependabot-managed pin in `.github/runtime-pin/requirements.txt`; set a specific version to override. Ignored on pull requests or when `use_local_source` is `true` (both run from source) |
1417
1520
  | `use_local_source` | No | `false` | Run from the checked-out source instead of PyPI (for testing unreleased code from any event) |
1418
1521
 
@@ -1534,8 +1637,8 @@ the run, rather than falling back to every repository. The run then collects
1534
1637
  exactly the repositories that check validated, without listing the
1535
1638
  organisation again, and prints `Limited to:` beneath its heading.
1536
1639
 
1537
- The remediable categories are the simple on/off features with a documented
1538
- enablement endpoint:
1640
+ The remediable categories are the features with a documented enablement
1641
+ endpoint:
1539
1642
 
1540
1643
  | `--category` | Enables |
1541
1644
  |---|---|
@@ -1545,15 +1648,48 @@ enablement endpoint:
1545
1648
  | `dependabot_updates_enabled` | Dependabot security updates (plus alerts) |
1546
1649
  | `private_vulnerability_reporting` | Private vulnerability reporting |
1547
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) |
1548
1653
 
1549
1654
  Qualitative findings (Scorecard, zizmor, open Dependabot alerts, cooldown,
1550
1655
  release freshness/mutability) are reported but not auto-remediated. Remediation
1551
1656
  is organisation-scoped (`--scope org`, the default and only supported scope).
1552
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
+
1553
1690
  ### Cleaning up stale CodeQL configurations
1554
1691
 
1555
- One category is **destructive**, so it runs only when named. The no-argument
1556
- run never includes it:
1692
+ This category is **destructive**, so it runs only when named:
1557
1693
 
1558
1694
  ```bash
1559
1695
  # Preview: which configurations would go, and which are refused and why.