github-security-report 0.12.0__tar.gz → 0.13.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 (78) hide show
  1. {github_security_report-0.12.0 → github_security_report-0.13.0}/PKG-INFO +121 -10
  2. {github_security_report-0.12.0 → github_security_report-0.13.0}/README.md +118 -7
  3. {github_security_report-0.12.0 → github_security_report-0.13.0}/pyproject.toml +4 -4
  4. {github_security_report-0.12.0 → github_security_report-0.13.0}/src/github_security_report/_version.py +2 -2
  5. {github_security_report-0.12.0 → github_security_report-0.13.0}/src/github_security_report/categories.py +27 -0
  6. {github_security_report-0.12.0 → github_security_report-0.13.0}/src/github_security_report/cli/__init__.py +13 -6
  7. {github_security_report-0.12.0 → github_security_report-0.13.0}/src/github_security_report/cli/app.py +97 -130
  8. github_security_report-0.13.0/src/github_security_report/cli/boundary.py +172 -0
  9. github_security_report-0.13.0/src/github_security_report/cli/modes.py +287 -0
  10. github_security_report-0.13.0/src/github_security_report/cli/options.py +93 -0
  11. {github_security_report-0.12.0 → github_security_report-0.13.0}/src/github_security_report/cli/outputs.py +13 -9
  12. github_security_report-0.13.0/src/github_security_report/cli/publish.py +147 -0
  13. {github_security_report-0.12.0 → github_security_report-0.13.0}/src/github_security_report/cli/serialise.py +10 -0
  14. {github_security_report-0.12.0 → github_security_report-0.13.0}/src/github_security_report/client/transport.py +186 -105
  15. {github_security_report-0.12.0 → github_security_report-0.13.0}/src/github_security_report/collect/extras.py +7 -1
  16. {github_security_report-0.12.0 → github_security_report-0.13.0}/src/github_security_report/config/__init__.py +12 -0
  17. {github_security_report-0.12.0 → github_security_report-0.13.0}/src/github_security_report/config/loader.py +3 -0
  18. {github_security_report-0.12.0 → github_security_report-0.13.0}/src/github_security_report/config/models.py +4 -0
  19. github_security_report-0.13.0/src/github_security_report/config/order.py +175 -0
  20. {github_security_report-0.12.0 → github_security_report-0.13.0}/src/github_security_report/config/schema.py +35 -1
  21. github_security_report-0.13.0/src/github_security_report/layout.py +272 -0
  22. {github_security_report-0.12.0 → github_security_report-0.13.0}/src/github_security_report/ordering.py +4 -0
  23. github_security_report-0.13.0/src/github_security_report/pulls/__init__.py +148 -0
  24. github_security_report-0.13.0/src/github_security_report/pulls/columns.py +53 -0
  25. github_security_report-0.13.0/src/github_security_report/pulls/counting.py +140 -0
  26. github_security_report-0.13.0/src/github_security_report/pulls/presentation.py +133 -0
  27. github_security_report-0.13.0/src/github_security_report/pulls/table.py +218 -0
  28. {github_security_report-0.12.0 → github_security_report-0.13.0}/src/github_security_report/render/html.py +25 -28
  29. {github_security_report-0.12.0 → github_security_report-0.13.0}/src/github_security_report/render/markdown.py +9 -13
  30. {github_security_report-0.12.0 → github_security_report-0.13.0}/src/github_security_report/render/slack.py +10 -12
  31. {github_security_report-0.12.0 → github_security_report-0.13.0}/src/github_security_report/render/terminal.py +12 -12
  32. github_security_report-0.13.0/src/github_security_report/report/__init__.py +103 -0
  33. github_security_report-0.13.0/src/github_security_report/report/aggregate.py +135 -0
  34. github_security_report-0.13.0/src/github_security_report/report/display.py +181 -0
  35. github_security_report-0.13.0/src/github_security_report/report/signals.py +95 -0
  36. github_security_report-0.13.0/src/github_security_report/report/tables.py +148 -0
  37. {github_security_report-0.12.0 → github_security_report-0.13.0}/src/github_security_report/templates/report.html.j2 +0 -42
  38. github_security_report-0.12.0/src/github_security_report/cli/modes.py +0 -418
  39. github_security_report-0.12.0/src/github_security_report/pulls.py +0 -499
  40. github_security_report-0.12.0/src/github_security_report/report.py +0 -531
  41. {github_security_report-0.12.0 → github_security_report-0.13.0}/.gitignore +0 -0
  42. {github_security_report-0.12.0 → github_security_report-0.13.0}/LICENSE +0 -0
  43. {github_security_report-0.12.0 → github_security_report-0.13.0}/LICENSES/Apache-2.0.txt +0 -0
  44. {github_security_report-0.12.0 → github_security_report-0.13.0}/scripts/README.md +0 -0
  45. {github_security_report-0.12.0 → github_security_report-0.13.0}/src/github_security_report/__init__.py +0 -0
  46. {github_security_report-0.12.0 → github_security_report-0.13.0}/src/github_security_report/authors.py +0 -0
  47. {github_security_report-0.12.0 → github_security_report-0.13.0}/src/github_security_report/classify.py +0 -0
  48. {github_security_report-0.12.0 → github_security_report-0.13.0}/src/github_security_report/cli/__main__.py +0 -0
  49. {github_security_report-0.12.0 → github_security_report-0.13.0}/src/github_security_report/client/__init__.py +0 -0
  50. {github_security_report-0.12.0 → github_security_report-0.13.0}/src/github_security_report/client/endpoints.py +0 -0
  51. {github_security_report-0.12.0 → github_security_report-0.13.0}/src/github_security_report/client/org_reads.py +0 -0
  52. {github_security_report-0.12.0 → github_security_report-0.13.0}/src/github_security_report/client/parsers.py +0 -0
  53. {github_security_report-0.12.0 → github_security_report-0.13.0}/src/github_security_report/client/queries.py +0 -0
  54. {github_security_report-0.12.0 → github_security_report-0.13.0}/src/github_security_report/client/reads.py +0 -0
  55. {github_security_report-0.12.0 → github_security_report-0.13.0}/src/github_security_report/client/writes.py +0 -0
  56. {github_security_report-0.12.0 → github_security_report-0.13.0}/src/github_security_report/collect/__init__.py +0 -0
  57. {github_security_report-0.12.0 → github_security_report-0.13.0}/src/github_security_report/collect/context.py +0 -0
  58. {github_security_report-0.12.0 → github_security_report-0.13.0}/src/github_security_report/collect/org.py +0 -0
  59. {github_security_report-0.12.0 → github_security_report-0.13.0}/src/github_security_report/collect/protocols.py +0 -0
  60. {github_security_report-0.12.0 → github_security_report-0.13.0}/src/github_security_report/collect/repo.py +0 -0
  61. {github_security_report-0.12.0 → github_security_report-0.13.0}/src/github_security_report/gating.py +0 -0
  62. {github_security_report-0.12.0 → github_security_report-0.13.0}/src/github_security_report/gitctx.py +0 -0
  63. {github_security_report-0.12.0 → github_security_report-0.13.0}/src/github_security_report/issues.py +0 -0
  64. {github_security_report-0.12.0 → github_security_report-0.13.0}/src/github_security_report/models.py +0 -0
  65. {github_security_report-0.12.0 → github_security_report-0.13.0}/src/github_security_report/posture/__init__.py +0 -0
  66. {github_security_report-0.12.0 → github_security_report-0.13.0}/src/github_security_report/posture/enablement.py +0 -0
  67. {github_security_report-0.12.0 → github_security_report-0.13.0}/src/github_security_report/posture/facts.py +0 -0
  68. {github_security_report-0.12.0 → github_security_report-0.13.0}/src/github_security_report/posture/releases.py +0 -0
  69. {github_security_report-0.12.0 → github_security_report-0.13.0}/src/github_security_report/py.typed +0 -0
  70. {github_security_report-0.12.0 → github_security_report-0.13.0}/src/github_security_report/remediate.py +0 -0
  71. {github_security_report-0.12.0 → github_security_report-0.13.0}/src/github_security_report/render/__init__.py +0 -0
  72. {github_security_report-0.12.0 → github_security_report-0.13.0}/src/github_security_report/render/slack_limits.py +0 -0
  73. {github_security_report-0.12.0 → github_security_report-0.13.0}/src/github_security_report/rulesets.py +0 -0
  74. {github_security_report-0.12.0 → github_security_report-0.13.0}/src/github_security_report/runner.py +0 -0
  75. {github_security_report-0.12.0 → github_security_report-0.13.0}/src/github_security_report/scope.py +0 -0
  76. {github_security_report-0.12.0 → github_security_report-0.13.0}/src/github_security_report/severity.py +0 -0
  77. {github_security_report-0.12.0 → github_security_report-0.13.0}/src/github_security_report/summary.py +0 -0
  78. {github_security_report-0.12.0 → github_security_report-0.13.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.12.0
3
+ Version: 0.13.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
@@ -31,12 +31,12 @@ Requires-Dist: pyyaml==6.0.3
31
31
  Requires-Dist: rich==15.0.0
32
32
  Requires-Dist: typer==0.27.1
33
33
  Provides-Extra: dev
34
- Requires-Dist: mypy==2.3.0; extra == 'dev'
34
+ Requires-Dist: mypy==2.3.1; 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.2; extra == 'dev'
39
+ Requires-Dist: ruff==0.16.3; extra == 'dev'
40
40
  Requires-Dist: syrupy==5.5.3; extra == 'dev'
41
41
  Requires-Dist: types-jsonschema==4.26.0.20260518; extra == 'dev'
42
42
  Requires-Dist: types-pyyaml==6.0.12.20250915; extra == 'dev'
@@ -95,8 +95,8 @@ no alerts, no analyses on a sample of repositories — gets a single
95
95
  `⏩ Skipping feature: organisation support missing` line for that section
96
96
  instead of a nag list. See the
97
97
  [organisation scan setup guide](docs/org-scan-setup.md) for the required
98
- workflows, and disable the check with `report.gating: false` if you want to
99
- probe everything regardless.
98
+ workflows, and disable the check with `report.gating: false` (or `--no-gating`)
99
+ if you want to probe everything regardless.
100
100
 
101
101
  Further sections report **configuration posture** and **freshness** as plain
102
102
  tables (org mode):
@@ -223,7 +223,8 @@ organisations. Grant these scopes:
223
223
  scope: "org"
224
224
  config: "${{ secrets.GSR_CONFIG || vars.GSR_CONFIG }}"
225
225
  token: "${{ secrets.LFRELENG_ACTIONS_REPORT_PAT }}"
226
- # Must match the per-org "token_env" in your config (below).
226
+ # Exports the token under this name, and overrides the per-org
227
+ # "token_env" in the config (below) so the two cannot drift apart.
227
228
  token_env: "LFRELENG_ACTIONS_REPORT_PAT"
228
229
  output_dir: "site"
229
230
  pages_url: "https://lfreleng-actions.github.io/github-security-report-action/"
@@ -246,6 +247,15 @@ digest only on the configured `report_day` (default Tuesday).
246
247
  # requires: permissions: { security-events: read }
247
248
  ```
248
249
 
250
+ Repo mode renders one repository to the terminal and the job summary. It
251
+ honours `top_n`, `top_n_report`, `top_n_cli`, `hide`, and the `report` block of
252
+ a supplied `config` (including every `report.categories.*` toggle). The inputs
253
+ that only shape an organisation-wide run — `output_dir`, `pages_url`,
254
+ `slack_channel`, `force_notify`, `top_n_slack`, and the Releases/Tagging levers
255
+ — are **rejected** with exit code 2 rather than accepted and discarded, so a
256
+ misconfigured workflow says so instead of quietly producing a report that
257
+ ignores them.
258
+
249
259
  ## Configuration
250
260
 
251
261
  Configuration is JSON, supplied as a plain `vars.` entry or base64-encoded in a
@@ -264,7 +274,8 @@ environment-variable name, never embedded.
264
274
  "include_archived": false,
265
275
  "include_test": false,
266
276
  "repo_min_age_days": 28,
267
- "release_max_age_days": 60
277
+ "release_max_age_days": 60,
278
+ "order": { "style": "auto" }
268
279
  },
269
280
  "organizations": [
270
281
  {
@@ -300,7 +311,12 @@ The Releases / Tagging section has two independent freshness levers:
300
311
  repositories drop out of the table. CLI: `--release-max-age-days`.
301
312
 
302
313
  The per-org `releases_exclude` (CLI `--releases-exclude`, repeatable) drops
303
- named repositories from the section entirely.
314
+ named repositories from the section entirely. The flag *replaces* the
315
+ configured list rather than adding to it, and cannot say which organisation it
316
+ means, so it is refused when the run covers more than one — set
317
+ `releases_exclude` per organisation in the configuration for that case. The two
318
+ age levers above are scalar policy and do apply to every configured
319
+ organisation, as `--top-n` does.
304
320
 
305
321
  > The former `release_min_age_days` key was a misleading name for
306
322
  > `repo_min_age_days` (it gates *repository* age, not *release* age). It is
@@ -311,6 +327,10 @@ The per-org `exclude` list removes repositories from analysis entirely; they are
311
327
  reported as **excluded** (distinct from "not enabled"), so an intentional
312
328
  exclusion is visible rather than silently dropped.
313
329
 
330
+ Archived and test repositories are excluded from analysis by default. Opt them
331
+ back in with `report.include_archived` / `report.include_test` in the config, or
332
+ for a single run with `--include-archived` / `--include-test`.
333
+
314
334
  ### Per-category render toggles
315
335
 
316
336
  Every reporting category can be switched on or off, globally and per output
@@ -487,6 +507,92 @@ a column list — Scorecard cascades through the worst severity rung any offende
487
507
  actually carries, so a lone Critical is never buried by a weaker repository with
488
508
  a lower score.
489
509
 
510
+ ### Output order
511
+
512
+ `sort` above orders the rows *within* a table. `report.order` orders the
513
+ **sections themselves** — which category a reader meets first.
514
+
515
+ By default the report sorts its sections into three bands:
516
+
517
+ | Band | Contains | Why |
518
+ | ---- | -------- | --- |
519
+ | **Priority** | Secret Scanning, Dependabot: Security Alerts, CodeQL, Mutable Releases | Findings worth acting on today |
520
+ | **Middle** | Everything else | Neither urgent nor constant |
521
+ | **BAU** | OpenSSF Scorecard, GitHub Issues, Pull Requests, Assigned to Me | Carry data every run, so none of it is news |
522
+
523
+ The bands are not fixed slots. **A band member with nothing to report moves
524
+ into the middle**: a clean priority category is noise at the top of a page, and
525
+ a clean BAU category has stopped being background. Demoted priority categories
526
+ sit at the top of the middle band and demoted BAU ones at the bottom, so
527
+ whatever survives in each band still bounds the middle from its own side.
528
+
529
+ "Nothing to report" means no rows and no named repositories — no offenders, and
530
+ nobody nagged for having the tool disabled. The bare counts every category
531
+ prints regardless (`All Clean`, an unknown tally) do not hold a band position. A
532
+ section skipped by [feature gating](#organisation-feature-gating) renders a
533
+ notice rather than results, so it demotes too.
534
+
535
+ The order is resolved **once**, when the report is assembled, and every surface
536
+ draws it — the terminal, the Slack digest, the Markdown artifact and the Pages
537
+ HTML cannot disagree. `report.json` publishes the resolved sequence as
538
+ `section_order`, listing each nested posture table after the signal it renders
539
+ beneath, so a machine consumer can reproduce the same layout.
540
+
541
+ Four styles are available via `report.order.style`:
542
+
543
+ | Style | Reads | Behaviour |
544
+ | ----- | ----- | --------- |
545
+ | `auto` (default) | — | The bands above, with the built-in membership. `automatic` is accepted as the same thing |
546
+ | `dual` | `priority`, `bau` | The same algorithm over your own band lists. Omit either to keep the built-in one |
547
+ | `single` | `sequence` | A strict hierarchy, applied verbatim with **no** demotion. Categories the list omits keep their assembly order behind it |
548
+ | `fixed` | — | No reordering at all — the order this tool produced before `report.order` existed |
549
+
550
+ Two bands of your own:
551
+
552
+ ```json
553
+ {
554
+ "report": {
555
+ "order": {
556
+ "style": "dual",
557
+ "priority": ["secret_scanning", "codeql"],
558
+ "bau": ["github_issues", "pull_requests"]
559
+ }
560
+ },
561
+ "organizations": [{ "name": "lfreleng-actions" }]
562
+ }
563
+ ```
564
+
565
+ Or one strict hierarchy, honoured whatever the data says:
566
+
567
+ ```json
568
+ {
569
+ "report": {
570
+ "order": {
571
+ "style": "single",
572
+ "sequence": ["secret_scanning", "dependabot_alerts", "codeql"]
573
+ }
574
+ },
575
+ "organizations": [{ "name": "lfreleng-actions" }]
576
+ }
577
+ ```
578
+
579
+ A list key paired with a style that does not read it is an **error** rather than
580
+ a silent no-op: writing a `priority` band and leaving the style at `auto` asks
581
+ for a custom band and would otherwise quietly get the built-in one. Listing a
582
+ category twice, or in both bands, is rejected for the same reason — a category
583
+ holds exactly one position.
584
+
585
+ An organisation inherits the global `order` block and may override it. Setting
586
+ `style` back to `"auto"` restores the built-in bands rather than keeping the
587
+ ones a global `dual` block supplied, since the built-in bands are what `auto`
588
+ means.
589
+
590
+ The three Dependabot posture tables (alerts enabled, security updates enabled,
591
+ cooldown) are not independently placeable. They render beneath the Dependabot
592
+ Alerts signal on every surface and travel with it, since three near-identical
593
+ headings adrift in the report would not say which signal they qualified.
594
+ Naming one in an ordering list is rejected rather than accepted and ignored.
595
+
490
596
  ### GitHub Issues
491
597
 
492
598
  The `github_issues` category counts each repository's **open issues**, split by
@@ -942,7 +1048,8 @@ code-scanning alerts from the tool, analyses on a sample of repositories, or
942
1048
  **skipped** — not probed per repository, not classified — and its section
943
1049
  shows a single `⏩ Skipping feature: organisation support missing` line
944
1050
  linking the setup guide, on every output surface. Set `report.gating` to
945
- `false` (globally or per organisation) to always probe everything:
1051
+ `false` (globally or per organisation), or pass `--no-gating` for a single run,
1052
+ to always probe everything:
946
1053
 
947
1054
  ```json
948
1055
  {
@@ -951,6 +1058,10 @@ linking the setup guide, on every output surface. Set `report.gating` to
951
1058
  }
952
1059
  ```
953
1060
 
1061
+ Because a skipped section is the most likely prompt for "why is zizmor missing
1062
+ from my report?", `--no-gating` exists so that question can be answered without
1063
+ writing a configuration file to set one boolean.
1064
+
954
1065
  Gating decides **collection**; the per-category render toggles above decide
955
1066
  **presentation**. A skipped section still renders (as the one-line notice)
956
1067
  unless its category is also disabled.
@@ -1021,7 +1132,7 @@ and the Slack **bot token** is consumed by the workflow, not the CLI.
1021
1132
  | `org` | No | — | Single organisation (shorthand for org mode) |
1022
1133
  | `repo` | No | detected | `owner/name` for repo mode |
1023
1134
  | `token` | No | `${{ github.token }}` | PAT (org mode) or `GITHUB_TOKEN` (repo mode) |
1024
- | `token_env` | No | `GITHUB_TOKEN` | Env var name the token is exported under. In org mode it **must match** the per-org `token_env` in your config (e.g. `LFRELENG_ACTIONS_REPORT_PAT`), otherwise the tool looks up an unset variable and reports no token. |
1135
+ | `token_env` | No | `GITHUB_TOKEN` | Env var name the token is exported under. When set it **overrides** the per-org `token_env` in your config, so the two no longer have to be kept in step; leave it unset to let each organisation use its own configured variable. |
1025
1136
  | `output_dir` | No | — | Directory for Pages output (org mode) |
1026
1137
  | `pages_url` | No | — | Published Pages URL (used in the Slack link) |
1027
1138
  | `slack_channel` | No | — | Slack channel ID; overrides the config `slack.channel` (e.g. the `SLACK_CHANNEL_ID` variable) |
@@ -51,8 +51,8 @@ no alerts, no analyses on a sample of repositories — gets a single
51
51
  `⏩ Skipping feature: organisation support missing` line for that section
52
52
  instead of a nag list. See the
53
53
  [organisation scan setup guide](docs/org-scan-setup.md) for the required
54
- workflows, and disable the check with `report.gating: false` if you want to
55
- probe everything regardless.
54
+ workflows, and disable the check with `report.gating: false` (or `--no-gating`)
55
+ if you want to probe everything regardless.
56
56
 
57
57
  Further sections report **configuration posture** and **freshness** as plain
58
58
  tables (org mode):
@@ -179,7 +179,8 @@ organisations. Grant these scopes:
179
179
  scope: "org"
180
180
  config: "${{ secrets.GSR_CONFIG || vars.GSR_CONFIG }}"
181
181
  token: "${{ secrets.LFRELENG_ACTIONS_REPORT_PAT }}"
182
- # Must match the per-org "token_env" in your config (below).
182
+ # Exports the token under this name, and overrides the per-org
183
+ # "token_env" in the config (below) so the two cannot drift apart.
183
184
  token_env: "LFRELENG_ACTIONS_REPORT_PAT"
184
185
  output_dir: "site"
185
186
  pages_url: "https://lfreleng-actions.github.io/github-security-report-action/"
@@ -202,6 +203,15 @@ digest only on the configured `report_day` (default Tuesday).
202
203
  # requires: permissions: { security-events: read }
203
204
  ```
204
205
 
206
+ Repo mode renders one repository to the terminal and the job summary. It
207
+ honours `top_n`, `top_n_report`, `top_n_cli`, `hide`, and the `report` block of
208
+ a supplied `config` (including every `report.categories.*` toggle). The inputs
209
+ that only shape an organisation-wide run — `output_dir`, `pages_url`,
210
+ `slack_channel`, `force_notify`, `top_n_slack`, and the Releases/Tagging levers
211
+ — are **rejected** with exit code 2 rather than accepted and discarded, so a
212
+ misconfigured workflow says so instead of quietly producing a report that
213
+ ignores them.
214
+
205
215
  ## Configuration
206
216
 
207
217
  Configuration is JSON, supplied as a plain `vars.` entry or base64-encoded in a
@@ -220,7 +230,8 @@ environment-variable name, never embedded.
220
230
  "include_archived": false,
221
231
  "include_test": false,
222
232
  "repo_min_age_days": 28,
223
- "release_max_age_days": 60
233
+ "release_max_age_days": 60,
234
+ "order": { "style": "auto" }
224
235
  },
225
236
  "organizations": [
226
237
  {
@@ -256,7 +267,12 @@ The Releases / Tagging section has two independent freshness levers:
256
267
  repositories drop out of the table. CLI: `--release-max-age-days`.
257
268
 
258
269
  The per-org `releases_exclude` (CLI `--releases-exclude`, repeatable) drops
259
- named repositories from the section entirely.
270
+ named repositories from the section entirely. The flag *replaces* the
271
+ configured list rather than adding to it, and cannot say which organisation it
272
+ means, so it is refused when the run covers more than one — set
273
+ `releases_exclude` per organisation in the configuration for that case. The two
274
+ age levers above are scalar policy and do apply to every configured
275
+ organisation, as `--top-n` does.
260
276
 
261
277
  > The former `release_min_age_days` key was a misleading name for
262
278
  > `repo_min_age_days` (it gates *repository* age, not *release* age). It is
@@ -267,6 +283,10 @@ The per-org `exclude` list removes repositories from analysis entirely; they are
267
283
  reported as **excluded** (distinct from "not enabled"), so an intentional
268
284
  exclusion is visible rather than silently dropped.
269
285
 
286
+ Archived and test repositories are excluded from analysis by default. Opt them
287
+ back in with `report.include_archived` / `report.include_test` in the config, or
288
+ for a single run with `--include-archived` / `--include-test`.
289
+
270
290
  ### Per-category render toggles
271
291
 
272
292
  Every reporting category can be switched on or off, globally and per output
@@ -443,6 +463,92 @@ a column list — Scorecard cascades through the worst severity rung any offende
443
463
  actually carries, so a lone Critical is never buried by a weaker repository with
444
464
  a lower score.
445
465
 
466
+ ### Output order
467
+
468
+ `sort` above orders the rows *within* a table. `report.order` orders the
469
+ **sections themselves** — which category a reader meets first.
470
+
471
+ By default the report sorts its sections into three bands:
472
+
473
+ | Band | Contains | Why |
474
+ | ---- | -------- | --- |
475
+ | **Priority** | Secret Scanning, Dependabot: Security Alerts, CodeQL, Mutable Releases | Findings worth acting on today |
476
+ | **Middle** | Everything else | Neither urgent nor constant |
477
+ | **BAU** | OpenSSF Scorecard, GitHub Issues, Pull Requests, Assigned to Me | Carry data every run, so none of it is news |
478
+
479
+ The bands are not fixed slots. **A band member with nothing to report moves
480
+ into the middle**: a clean priority category is noise at the top of a page, and
481
+ a clean BAU category has stopped being background. Demoted priority categories
482
+ sit at the top of the middle band and demoted BAU ones at the bottom, so
483
+ whatever survives in each band still bounds the middle from its own side.
484
+
485
+ "Nothing to report" means no rows and no named repositories — no offenders, and
486
+ nobody nagged for having the tool disabled. The bare counts every category
487
+ prints regardless (`All Clean`, an unknown tally) do not hold a band position. A
488
+ section skipped by [feature gating](#organisation-feature-gating) renders a
489
+ notice rather than results, so it demotes too.
490
+
491
+ The order is resolved **once**, when the report is assembled, and every surface
492
+ draws it — the terminal, the Slack digest, the Markdown artifact and the Pages
493
+ HTML cannot disagree. `report.json` publishes the resolved sequence as
494
+ `section_order`, listing each nested posture table after the signal it renders
495
+ beneath, so a machine consumer can reproduce the same layout.
496
+
497
+ Four styles are available via `report.order.style`:
498
+
499
+ | Style | Reads | Behaviour |
500
+ | ----- | ----- | --------- |
501
+ | `auto` (default) | — | The bands above, with the built-in membership. `automatic` is accepted as the same thing |
502
+ | `dual` | `priority`, `bau` | The same algorithm over your own band lists. Omit either to keep the built-in one |
503
+ | `single` | `sequence` | A strict hierarchy, applied verbatim with **no** demotion. Categories the list omits keep their assembly order behind it |
504
+ | `fixed` | — | No reordering at all — the order this tool produced before `report.order` existed |
505
+
506
+ Two bands of your own:
507
+
508
+ ```json
509
+ {
510
+ "report": {
511
+ "order": {
512
+ "style": "dual",
513
+ "priority": ["secret_scanning", "codeql"],
514
+ "bau": ["github_issues", "pull_requests"]
515
+ }
516
+ },
517
+ "organizations": [{ "name": "lfreleng-actions" }]
518
+ }
519
+ ```
520
+
521
+ Or one strict hierarchy, honoured whatever the data says:
522
+
523
+ ```json
524
+ {
525
+ "report": {
526
+ "order": {
527
+ "style": "single",
528
+ "sequence": ["secret_scanning", "dependabot_alerts", "codeql"]
529
+ }
530
+ },
531
+ "organizations": [{ "name": "lfreleng-actions" }]
532
+ }
533
+ ```
534
+
535
+ A list key paired with a style that does not read it is an **error** rather than
536
+ a silent no-op: writing a `priority` band and leaving the style at `auto` asks
537
+ for a custom band and would otherwise quietly get the built-in one. Listing a
538
+ category twice, or in both bands, is rejected for the same reason — a category
539
+ holds exactly one position.
540
+
541
+ An organisation inherits the global `order` block and may override it. Setting
542
+ `style` back to `"auto"` restores the built-in bands rather than keeping the
543
+ ones a global `dual` block supplied, since the built-in bands are what `auto`
544
+ means.
545
+
546
+ The three Dependabot posture tables (alerts enabled, security updates enabled,
547
+ cooldown) are not independently placeable. They render beneath the Dependabot
548
+ Alerts signal on every surface and travel with it, since three near-identical
549
+ headings adrift in the report would not say which signal they qualified.
550
+ Naming one in an ordering list is rejected rather than accepted and ignored.
551
+
446
552
  ### GitHub Issues
447
553
 
448
554
  The `github_issues` category counts each repository's **open issues**, split by
@@ -898,7 +1004,8 @@ code-scanning alerts from the tool, analyses on a sample of repositories, or
898
1004
  **skipped** — not probed per repository, not classified — and its section
899
1005
  shows a single `⏩ Skipping feature: organisation support missing` line
900
1006
  linking the setup guide, on every output surface. Set `report.gating` to
901
- `false` (globally or per organisation) to always probe everything:
1007
+ `false` (globally or per organisation), or pass `--no-gating` for a single run,
1008
+ to always probe everything:
902
1009
 
903
1010
  ```json
904
1011
  {
@@ -907,6 +1014,10 @@ linking the setup guide, on every output surface. Set `report.gating` to
907
1014
  }
908
1015
  ```
909
1016
 
1017
+ Because a skipped section is the most likely prompt for "why is zizmor missing
1018
+ from my report?", `--no-gating` exists so that question can be answered without
1019
+ writing a configuration file to set one boolean.
1020
+
910
1021
  Gating decides **collection**; the per-category render toggles above decide
911
1022
  **presentation**. A skipped section still renders (as the one-line notice)
912
1023
  unless its category is also disabled.
@@ -977,7 +1088,7 @@ and the Slack **bot token** is consumed by the workflow, not the CLI.
977
1088
  | `org` | No | — | Single organisation (shorthand for org mode) |
978
1089
  | `repo` | No | detected | `owner/name` for repo mode |
979
1090
  | `token` | No | `${{ github.token }}` | PAT (org mode) or `GITHUB_TOKEN` (repo mode) |
980
- | `token_env` | No | `GITHUB_TOKEN` | Env var name the token is exported under. In org mode it **must match** the per-org `token_env` in your config (e.g. `LFRELENG_ACTIONS_REPORT_PAT`), otherwise the tool looks up an unset variable and reports no token. |
1091
+ | `token_env` | No | `GITHUB_TOKEN` | Env var name the token is exported under. When set it **overrides** the per-org `token_env` in your config, so the two no longer have to be kept in step; leave it unset to let each organisation use its own configured variable. |
981
1092
  | `output_dir` | No | — | Directory for Pages output (org mode) |
982
1093
  | `pages_url` | No | — | Published Pages URL (used in the Slack link) |
983
1094
  | `slack_channel` | No | — | Slack channel ID; overrides the config `slack.channel` (e.g. the `SLACK_CHANNEL_ID` variable) |
@@ -64,8 +64,8 @@ dev = [
64
64
  "pytest-cov==7.1.0",
65
65
  "respx==0.23.1",
66
66
  "syrupy==5.5.3",
67
- "mypy==2.3.0",
68
- "ruff==0.16.2",
67
+ "mypy==2.3.1",
68
+ "ruff==0.16.3",
69
69
  "types-jsonschema==4.26.0.20260518",
70
70
  "types-PyYAML==6.0.12.20250915",
71
71
  ]
@@ -96,8 +96,8 @@ dev = [
96
96
  "pytest-cov==7.1.0",
97
97
  "respx==0.23.1",
98
98
  "syrupy==5.5.3",
99
- "mypy==2.3.0",
100
- "ruff==0.16.2",
99
+ "mypy==2.3.1",
100
+ "ruff==0.16.3",
101
101
  "types-jsonschema==4.26.0.20260518",
102
102
  "types-PyYAML==6.0.12.20250915",
103
103
  ]
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '0.12.0'
22
- __version_tuple__ = version_tuple = (0, 12, 0)
21
+ __version__ = version = '0.13.0'
22
+ __version_tuple__ = version_tuple = (0, 13, 0)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -337,6 +337,33 @@ def category_meta(key: CategoryKey) -> CategoryMeta:
337
337
  return _CATEGORIES[key]
338
338
 
339
339
 
340
+ # Categories rendered as sub-tables beneath another category rather than as
341
+ # sections of their own. The three Dependabot posture tables qualify their
342
+ # parent signal -- "Alerts Enabled" means nothing adrift from "Dependabot:
343
+ # Security Alerts" -- so they travel with it and cannot be positioned
344
+ # independently. Named here rather than in the layout module so the config
345
+ # schema can refuse to accept one in an ordering list, which would otherwise be
346
+ # a setting that validates and then does nothing.
347
+ NESTED_CATEGORIES: frozenset[CategoryKey] = frozenset(
348
+ {
349
+ CategoryKey.DEPENDABOT_ALERTS_ENABLED,
350
+ CategoryKey.DEPENDABOT_UPDATES_ENABLED,
351
+ CategoryKey.DEPENDABOT_COOLDOWN,
352
+ }
353
+ )
354
+
355
+
356
+ def orderable_categories() -> tuple[CategoryMeta, ...]:
357
+ """Categories an ordering list may name, in registry order.
358
+
359
+ Every category except the nested ones, which have no position of their own
360
+ to configure.
361
+ """
362
+ return tuple(
363
+ meta for meta in _CATEGORIES.values() if meta.key not in NESTED_CATEGORIES
364
+ )
365
+
366
+
340
367
  def all_categories() -> tuple[CategoryMeta, ...]:
341
368
  """Every category's metadata, in registry (render) order."""
342
369
  return tuple(_CATEGORIES.values())
@@ -11,6 +11,8 @@ The command surface is split across:
11
11
 
12
12
  - :mod:`cli.app` -- the Typer application, options and validation
13
13
  - :mod:`cli.modes` -- the org, repo and remediate run modes
14
+ - :mod:`cli.options` -- what a run takes from the command line
15
+ - :mod:`cli.publish` -- the Pages files, Slack digest and job summary
14
16
  - :mod:`cli.outputs` -- offender limits, category visibility, file writing
15
17
  - :mod:`cli.serialise` -- the machine-readable ``report.json`` shape
16
18
  """
@@ -25,8 +27,6 @@ from github_security_report.cli.app import (
25
27
  report,
26
28
  )
27
29
  from github_security_report.cli.modes import (
28
- OrgRunOptions,
29
- ReleaseOverrides,
30
30
  _abort_auth,
31
31
  _abort_network,
32
32
  _load_config,
@@ -34,6 +34,7 @@ from github_security_report.cli.modes import (
34
34
  _run_remediate,
35
35
  _run_repo,
36
36
  )
37
+ from github_security_report.cli.options import OrgRunOptions, ReportOverrides
37
38
  from github_security_report.cli.outputs import (
38
39
  TopNLimits,
39
40
  _safe_component,
@@ -42,12 +43,18 @@ from github_security_report.cli.outputs import (
42
43
  )
43
44
  from github_security_report.cli.serialise import _org_to_dict, _table_to_dict
44
45
 
45
- # The package façade: every name previously importable from
46
- # ``github_security_report.cli`` still is, including the private helpers the
47
- # tests reach for, so splitting the module changed no caller's import path.
46
+ # The package façade: every name the module split moved is still importable
47
+ # from ``github_security_report.cli``, including the private helpers the tests
48
+ # reach for, so relocating a definition changes no caller's import path.
49
+ #
50
+ # ``ReleaseOverrides`` is the one deliberate exception. It gained the gating
51
+ # and inclusion overrides and became ``ReportOverrides``, so it is a rename
52
+ # rather than a move. No alias is kept: the old name now describes only a third
53
+ # of what the class carries, and a façade entry that points somewhere
54
+ # misleading is worse than an import error that says exactly what happened.
48
55
  __all__ = [
49
56
  "OrgRunOptions",
50
- "ReleaseOverrides",
57
+ "ReportOverrides",
51
58
  "TopNLimits",
52
59
  "_abort_auth",
53
60
  "_abort_network",