github-security-report 0.4.1__tar.gz → 0.6.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 (32) hide show
  1. {github_security_report-0.4.1 → github_security_report-0.6.0}/PKG-INFO +76 -14
  2. {github_security_report-0.4.1 → github_security_report-0.6.0}/README.md +72 -10
  3. {github_security_report-0.4.1 → github_security_report-0.6.0}/pyproject.toml +5 -5
  4. {github_security_report-0.4.1 → github_security_report-0.6.0}/src/github_security_report/_version.py +2 -2
  5. {github_security_report-0.4.1 → github_security_report-0.6.0}/src/github_security_report/categories.py +20 -2
  6. {github_security_report-0.4.1 → github_security_report-0.6.0}/src/github_security_report/cli.py +118 -2
  7. {github_security_report-0.4.1 → github_security_report-0.6.0}/src/github_security_report/client.py +149 -0
  8. {github_security_report-0.4.1 → github_security_report-0.6.0}/src/github_security_report/collect.py +21 -5
  9. {github_security_report-0.4.1 → github_security_report-0.6.0}/src/github_security_report/posture.py +22 -0
  10. github_security_report-0.6.0/src/github_security_report/remediate.py +248 -0
  11. {github_security_report-0.4.1 → github_security_report-0.6.0}/src/github_security_report/render/html.py +10 -0
  12. {github_security_report-0.4.1 → github_security_report-0.6.0}/src/github_security_report/render/markdown.py +11 -0
  13. {github_security_report-0.4.1 → github_security_report-0.6.0}/src/github_security_report/render/slack.py +8 -0
  14. {github_security_report-0.4.1 → github_security_report-0.6.0}/src/github_security_report/render/terminal.py +125 -11
  15. {github_security_report-0.4.1 → github_security_report-0.6.0}/src/github_security_report/report.py +3 -0
  16. {github_security_report-0.4.1 → github_security_report-0.6.0}/src/github_security_report/severity.py +21 -12
  17. {github_security_report-0.4.1 → github_security_report-0.6.0}/src/github_security_report/templates/report.html.j2 +7 -0
  18. {github_security_report-0.4.1 → github_security_report-0.6.0}/.gitignore +0 -0
  19. {github_security_report-0.4.1 → github_security_report-0.6.0}/LICENSE +0 -0
  20. {github_security_report-0.4.1 → github_security_report-0.6.0}/LICENSES/Apache-2.0.txt +0 -0
  21. {github_security_report-0.4.1 → github_security_report-0.6.0}/scripts/README.md +0 -0
  22. {github_security_report-0.4.1 → github_security_report-0.6.0}/src/github_security_report/__init__.py +0 -0
  23. {github_security_report-0.4.1 → github_security_report-0.6.0}/src/github_security_report/classify.py +0 -0
  24. {github_security_report-0.4.1 → github_security_report-0.6.0}/src/github_security_report/config.py +0 -0
  25. {github_security_report-0.4.1 → github_security_report-0.6.0}/src/github_security_report/gitctx.py +0 -0
  26. {github_security_report-0.4.1 → github_security_report-0.6.0}/src/github_security_report/models.py +0 -0
  27. {github_security_report-0.4.1 → github_security_report-0.6.0}/src/github_security_report/py.typed +0 -0
  28. {github_security_report-0.4.1 → github_security_report-0.6.0}/src/github_security_report/render/__init__.py +0 -0
  29. {github_security_report-0.4.1 → github_security_report-0.6.0}/src/github_security_report/rulesets.py +0 -0
  30. {github_security_report-0.4.1 → github_security_report-0.6.0}/src/github_security_report/runner.py +0 -0
  31. {github_security_report-0.4.1 → github_security_report-0.6.0}/src/github_security_report/scope.py +0 -0
  32. {github_security_report-0.4.1 → github_security_report-0.6.0}/src/github_security_report/templates/index.html.j2 +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: github-security-report
3
- Version: 0.4.1
3
+ Version: 0.6.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,15 +29,15 @@ 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.26.7
32
+ Requires-Dist: typer==0.26.8
33
33
  Provides-Extra: dev
34
34
  Requires-Dist: mypy==2.1.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.15.19; extra == 'dev'
40
- Requires-Dist: syrupy==5.3.3; extra == 'dev'
39
+ Requires-Dist: ruff==0.15.20; extra == 'dev'
40
+ Requires-Dist: syrupy==5.3.4; extra == 'dev'
41
41
  Requires-Dist: types-jsonschema==4.26.0.20260518; extra == 'dev'
42
42
  Requires-Dist: types-pyyaml==6.0.12.20250915; extra == 'dev'
43
43
  Description-Content-Type: text/markdown
@@ -87,7 +87,7 @@ back to code-scanning findings. See [`docs/BRIEF.md`](docs/BRIEF.md) and
87
87
  [`docs/phase0-findings.md`](docs/phase0-findings.md) for the full design and the
88
88
  API research it is built on.
89
89
 
90
- Two further sections report **configuration posture** and **freshness** as plain
90
+ Further sections report **configuration posture** and **freshness** as plain
91
91
  tables (org mode):
92
92
 
93
93
  - **Dependabot** — three tables: repositories with vulnerability **alerts not
@@ -101,6 +101,12 @@ tables (org mode):
101
101
  release or tag is older than `release_max_age_days` (default 60; `0` flags
102
102
  every eligible repository), so a repository released or tagged within that
103
103
  window counts as recently maintained and drops out of the table.
104
+ - **Private Vulnerability Reporting** — repositories where GitHub's private
105
+ vulnerability reporting feature is **not enabled**, so security researchers
106
+ cannot privately disclose vulnerabilities. Probed per repository (GitHub
107
+ exposes no org-wide or GraphQL equivalent) and, like every other category,
108
+ always collected; hide it with the `private_vulnerability_reporting` render
109
+ toggle.
104
110
 
105
111
  ## Operating modes
106
112
 
@@ -176,7 +182,7 @@ organisations. Grant these scopes:
176
182
 
177
183
  > For organisations that enforce SSO, the PAT must be **SSO-authorised** for
178
184
  > each target organisation, or the org-level endpoints return `403` (reported as
179
- > unknown). Store the token as a secret (e.g. `SECURITY_REPORT_PAT`) and
185
+ > unknown). Store the token as a secret (e.g. `LFRELENG_ACTIONS_REPORT_PAT`) and
180
186
  > reference it by env-var name via `token_env`; never embed it in the config.
181
187
 
182
188
  ## Usage
@@ -190,7 +196,9 @@ organisations. Grant these scopes:
190
196
  with:
191
197
  scope: "org"
192
198
  config: "${{ secrets.GSR_CONFIG || vars.GSR_CONFIG }}"
193
- token: "${{ secrets.SECURITY_REPORT_PAT }}"
199
+ token: "${{ secrets.LFRELENG_ACTIONS_REPORT_PAT }}"
200
+ # Must match the per-org "token_env" in your config (below).
201
+ token_env: "LFRELENG_ACTIONS_REPORT_PAT"
194
202
  output_dir: "site"
195
203
  pages_url: "https://lfreleng-actions.github.io/github-security-report-action/"
196
204
  ```
@@ -235,7 +243,7 @@ environment-variable name, never embedded.
235
243
  "organizations": [
236
244
  {
237
245
  "name": "lfreleng-actions",
238
- "token_env": "GITHUB_TOKEN",
246
+ "token_env": "LFRELENG_ACTIONS_REPORT_PAT",
239
247
  "exclude": ["actions-template"],
240
248
  "releases_exclude": ["internal-only-repo"]
241
249
  }
@@ -305,7 +313,8 @@ out of the terminal and Slack while still publishing it to the Markdown and HTML
305
313
  Pages output. The valid category keys are: `codeql`, `scorecard`, `zizmor`,
306
314
  `dependabot_alerts`, `secret_scanning`, `dependabot_alerts_enabled`,
307
315
  `dependabot_updates_enabled`, `dependabot_cooldown`, `releases`,
308
- `mutable_releases`. Like the other `report` settings, `categories` can be set
316
+ `mutable_releases`, `private_vulnerability_reporting`. Like the other `report`
317
+ settings, `categories` can be set
309
318
  globally and overridden per organisation (overrides merge key-by-key, so
310
319
  flipping one output leaves the rest untouched). The machine-readable
311
320
  `report.json` artifact always contains the complete dataset, regardless of these
@@ -326,8 +335,11 @@ The severity-ranked signals (CodeQL, Scorecard, Zizmor, Dependabot alerts) use a
326
335
  repository is flagged as an offender only when it carries a finding **at or
327
336
  above** the cutoff; findings below it fold into the clean count. Severities run
328
337
  (lowest to highest) `informational`, `low`, `medium`, `high`, `critical` —
329
- `informational` being the new sub-low rung that SARIF `note`/`none` findings
330
- (the bulk of a tool like Zizmor) normalise to.
338
+ `informational` being the sub-low rung for SARIF `none` findings and
339
+ unclassifiable alerts. Zizmor's SARIF `note` findings normalise to `low`
340
+ (zizmor emits its Low findings at `note`, and the organisation scan pipeline's
341
+ `--min-severity low` floor keeps informational findings out of the uploaded
342
+ SARIF), matching the ruleset-enforced PR gate that blocks on note-and-above.
331
343
 
332
344
  The global default cutoff is `medium`, so `low` and `informational` findings
333
345
  pass. Zizmor defaults to `low` (only `informational` passes). Override the
@@ -365,7 +377,7 @@ $XDG_CONFIG_HOME/github-security-report/config.json
365
377
  An explicit `--config`, `--config-data`, or `--org` always takes precedence, and
366
378
  the action itself never reads this path (it is supplied configuration directly).
367
379
  Secrets stay out of the file: reference the token by environment-variable name
368
- via `token_env` (e.g. `SECURITY_REPORT_PAT`, exported in your shell or sourced
380
+ via `token_env` (e.g. `LFRELENG_ACTIONS_REPORT_PAT`, exported in your shell or sourced
369
381
  from a secrets file) — the channel ID is the only Slack value the file holds,
370
382
  and the Slack **bot token** is consumed by the workflow, not the CLI.
371
383
 
@@ -380,7 +392,7 @@ and the Slack **bot token** is consumed by the workflow, not the CLI.
380
392
  | `org` | No | — | Single organisation (shorthand for org mode) |
381
393
  | `repo` | No | detected | `owner/name` for repo mode |
382
394
  | `token` | No | `${{ github.token }}` | PAT (org mode) or `GITHUB_TOKEN` (repo mode) |
383
- | `token_env` | No | `GITHUB_TOKEN` | Env var name the tool reads the token from |
395
+ | `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. |
384
396
  | `output_dir` | No | — | Directory for Pages output (org mode) |
385
397
  | `pages_url` | No | — | Published Pages URL (used in the Slack link) |
386
398
  | `slack_channel` | No | — | Slack channel ID; overrides the config `slack.channel` (e.g. the `SLACK_CHANNEL_ID` variable) |
@@ -417,8 +429,57 @@ uvx github-security-report report
417
429
  uvx github-security-report report --org lfreleng-actions
418
430
  ```
419
431
 
432
+ ## Remediation
433
+
434
+ The `remediate` subcommand is the in-tool counterpart to the report: it runs the
435
+ same collection, then switches on each selected security feature wherever a
436
+ repository has it **confirmed off**. Only the offenders the report already
437
+ surfaces are acted on — repositories whose state could not be read are counted
438
+ as *unknown* and are never written to, so remediation never blind-writes.
439
+
440
+ It is **dry run by default** (these are privileged writes); pass `--apply` to
441
+ make changes. A single **write-capable** org-admin token (from `--token-env`,
442
+ default `GITHUB_TOKEN`) drives both the read and the writes across every
443
+ configured organisation, so it bypasses the per-org read-only `token_env` in the
444
+ config.
445
+
446
+ ```bash
447
+ # An org-admin token is required: a classic PAT with the `repo` scope
448
+ # (administers repository security settings) plus `read:org` to enumerate repos.
449
+ source ~/.secrets.github.classic.god # exports $GITHUB_TOKEN
450
+
451
+ # Dry run (default): preview every change, touch nothing.
452
+ uvx github-security-report remediate --org lfreleng-actions
453
+
454
+ # Apply: enable every remediable feature that is off, across all configured orgs.
455
+ uvx github-security-report remediate \
456
+ --config ~/.config/github-security-report/config.json --apply
457
+
458
+ # Limit to specific categories (repeatable).
459
+ uvx github-security-report remediate --org lfreleng-actions \
460
+ --category codeql --category private_vulnerability_reporting --apply
461
+ ```
462
+
463
+ The remediable categories are the simple on/off features with a documented
464
+ enablement endpoint:
465
+
466
+ | `--category` | Enables |
467
+ |---|---|
468
+ | `codeql` | CodeQL default setup (provisioned asynchronously) |
469
+ | `secret_scanning` | Secret scanning |
470
+ | `dependabot_alerts_enabled` | Dependabot vulnerability alerts |
471
+ | `dependabot_updates_enabled` | Dependabot security updates (plus alerts) |
472
+ | `private_vulnerability_reporting` | Private vulnerability reporting |
473
+
474
+ Qualitative findings (Scorecard, zizmor, open Dependabot alerts, cooldown,
475
+ release freshness/mutability) are reported but not auto-remediated. Remediation
476
+ is organisation-scoped (`--scope org`, the default and only supported scope).
477
+
420
478
  ## Bulk Remediation Scripts
421
479
 
480
+ The standalone scripts below predate the `remediate` subcommand and remain for
481
+ ad-hoc, single-feature runs. For most workflows, prefer `remediate` above.
482
+
422
483
  The report ends with **nag lists** — repositories where a supported feature is
423
484
  switched off. Where GitHub exposes the relevant toggle through its REST API,
424
485
  the [`scripts/`](scripts/) directory ships standalone helpers that clear a whole
@@ -440,7 +501,8 @@ the current state of each repository, enables the feature where it is off, and
440
501
  verifies the result.
441
502
 
442
503
  ```bash
443
- # An org-admin token is required (classic PAT with repo admin / admin:org).
504
+ # An org-admin token is required: a classic PAT with the `repo` scope
505
+ # (administers repository security settings) plus `read:org` to enumerate repos.
444
506
  source ~/.secrets.github.classic.god # exports $GITHUB_TOKEN
445
507
 
446
508
  # Dry run (default): preview every change, touch nothing.
@@ -43,7 +43,7 @@ back to code-scanning findings. See [`docs/BRIEF.md`](docs/BRIEF.md) and
43
43
  [`docs/phase0-findings.md`](docs/phase0-findings.md) for the full design and the
44
44
  API research it is built on.
45
45
 
46
- Two further sections report **configuration posture** and **freshness** as plain
46
+ Further sections report **configuration posture** and **freshness** as plain
47
47
  tables (org mode):
48
48
 
49
49
  - **Dependabot** — three tables: repositories with vulnerability **alerts not
@@ -57,6 +57,12 @@ tables (org mode):
57
57
  release or tag is older than `release_max_age_days` (default 60; `0` flags
58
58
  every eligible repository), so a repository released or tagged within that
59
59
  window counts as recently maintained and drops out of the table.
60
+ - **Private Vulnerability Reporting** — repositories where GitHub's private
61
+ vulnerability reporting feature is **not enabled**, so security researchers
62
+ cannot privately disclose vulnerabilities. Probed per repository (GitHub
63
+ exposes no org-wide or GraphQL equivalent) and, like every other category,
64
+ always collected; hide it with the `private_vulnerability_reporting` render
65
+ toggle.
60
66
 
61
67
  ## Operating modes
62
68
 
@@ -132,7 +138,7 @@ organisations. Grant these scopes:
132
138
 
133
139
  > For organisations that enforce SSO, the PAT must be **SSO-authorised** for
134
140
  > each target organisation, or the org-level endpoints return `403` (reported as
135
- > unknown). Store the token as a secret (e.g. `SECURITY_REPORT_PAT`) and
141
+ > unknown). Store the token as a secret (e.g. `LFRELENG_ACTIONS_REPORT_PAT`) and
136
142
  > reference it by env-var name via `token_env`; never embed it in the config.
137
143
 
138
144
  ## Usage
@@ -146,7 +152,9 @@ organisations. Grant these scopes:
146
152
  with:
147
153
  scope: "org"
148
154
  config: "${{ secrets.GSR_CONFIG || vars.GSR_CONFIG }}"
149
- token: "${{ secrets.SECURITY_REPORT_PAT }}"
155
+ token: "${{ secrets.LFRELENG_ACTIONS_REPORT_PAT }}"
156
+ # Must match the per-org "token_env" in your config (below).
157
+ token_env: "LFRELENG_ACTIONS_REPORT_PAT"
150
158
  output_dir: "site"
151
159
  pages_url: "https://lfreleng-actions.github.io/github-security-report-action/"
152
160
  ```
@@ -191,7 +199,7 @@ environment-variable name, never embedded.
191
199
  "organizations": [
192
200
  {
193
201
  "name": "lfreleng-actions",
194
- "token_env": "GITHUB_TOKEN",
202
+ "token_env": "LFRELENG_ACTIONS_REPORT_PAT",
195
203
  "exclude": ["actions-template"],
196
204
  "releases_exclude": ["internal-only-repo"]
197
205
  }
@@ -261,7 +269,8 @@ out of the terminal and Slack while still publishing it to the Markdown and HTML
261
269
  Pages output. The valid category keys are: `codeql`, `scorecard`, `zizmor`,
262
270
  `dependabot_alerts`, `secret_scanning`, `dependabot_alerts_enabled`,
263
271
  `dependabot_updates_enabled`, `dependabot_cooldown`, `releases`,
264
- `mutable_releases`. Like the other `report` settings, `categories` can be set
272
+ `mutable_releases`, `private_vulnerability_reporting`. Like the other `report`
273
+ settings, `categories` can be set
265
274
  globally and overridden per organisation (overrides merge key-by-key, so
266
275
  flipping one output leaves the rest untouched). The machine-readable
267
276
  `report.json` artifact always contains the complete dataset, regardless of these
@@ -282,8 +291,11 @@ The severity-ranked signals (CodeQL, Scorecard, Zizmor, Dependabot alerts) use a
282
291
  repository is flagged as an offender only when it carries a finding **at or
283
292
  above** the cutoff; findings below it fold into the clean count. Severities run
284
293
  (lowest to highest) `informational`, `low`, `medium`, `high`, `critical` —
285
- `informational` being the new sub-low rung that SARIF `note`/`none` findings
286
- (the bulk of a tool like Zizmor) normalise to.
294
+ `informational` being the sub-low rung for SARIF `none` findings and
295
+ unclassifiable alerts. Zizmor's SARIF `note` findings normalise to `low`
296
+ (zizmor emits its Low findings at `note`, and the organisation scan pipeline's
297
+ `--min-severity low` floor keeps informational findings out of the uploaded
298
+ SARIF), matching the ruleset-enforced PR gate that blocks on note-and-above.
287
299
 
288
300
  The global default cutoff is `medium`, so `low` and `informational` findings
289
301
  pass. Zizmor defaults to `low` (only `informational` passes). Override the
@@ -321,7 +333,7 @@ $XDG_CONFIG_HOME/github-security-report/config.json
321
333
  An explicit `--config`, `--config-data`, or `--org` always takes precedence, and
322
334
  the action itself never reads this path (it is supplied configuration directly).
323
335
  Secrets stay out of the file: reference the token by environment-variable name
324
- via `token_env` (e.g. `SECURITY_REPORT_PAT`, exported in your shell or sourced
336
+ via `token_env` (e.g. `LFRELENG_ACTIONS_REPORT_PAT`, exported in your shell or sourced
325
337
  from a secrets file) — the channel ID is the only Slack value the file holds,
326
338
  and the Slack **bot token** is consumed by the workflow, not the CLI.
327
339
 
@@ -336,7 +348,7 @@ and the Slack **bot token** is consumed by the workflow, not the CLI.
336
348
  | `org` | No | — | Single organisation (shorthand for org mode) |
337
349
  | `repo` | No | detected | `owner/name` for repo mode |
338
350
  | `token` | No | `${{ github.token }}` | PAT (org mode) or `GITHUB_TOKEN` (repo mode) |
339
- | `token_env` | No | `GITHUB_TOKEN` | Env var name the tool reads the token from |
351
+ | `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. |
340
352
  | `output_dir` | No | — | Directory for Pages output (org mode) |
341
353
  | `pages_url` | No | — | Published Pages URL (used in the Slack link) |
342
354
  | `slack_channel` | No | — | Slack channel ID; overrides the config `slack.channel` (e.g. the `SLACK_CHANNEL_ID` variable) |
@@ -373,8 +385,57 @@ uvx github-security-report report
373
385
  uvx github-security-report report --org lfreleng-actions
374
386
  ```
375
387
 
388
+ ## Remediation
389
+
390
+ The `remediate` subcommand is the in-tool counterpart to the report: it runs the
391
+ same collection, then switches on each selected security feature wherever a
392
+ repository has it **confirmed off**. Only the offenders the report already
393
+ surfaces are acted on — repositories whose state could not be read are counted
394
+ as *unknown* and are never written to, so remediation never blind-writes.
395
+
396
+ It is **dry run by default** (these are privileged writes); pass `--apply` to
397
+ make changes. A single **write-capable** org-admin token (from `--token-env`,
398
+ default `GITHUB_TOKEN`) drives both the read and the writes across every
399
+ configured organisation, so it bypasses the per-org read-only `token_env` in the
400
+ config.
401
+
402
+ ```bash
403
+ # An org-admin token is required: a classic PAT with the `repo` scope
404
+ # (administers repository security settings) plus `read:org` to enumerate repos.
405
+ source ~/.secrets.github.classic.god # exports $GITHUB_TOKEN
406
+
407
+ # Dry run (default): preview every change, touch nothing.
408
+ uvx github-security-report remediate --org lfreleng-actions
409
+
410
+ # Apply: enable every remediable feature that is off, across all configured orgs.
411
+ uvx github-security-report remediate \
412
+ --config ~/.config/github-security-report/config.json --apply
413
+
414
+ # Limit to specific categories (repeatable).
415
+ uvx github-security-report remediate --org lfreleng-actions \
416
+ --category codeql --category private_vulnerability_reporting --apply
417
+ ```
418
+
419
+ The remediable categories are the simple on/off features with a documented
420
+ enablement endpoint:
421
+
422
+ | `--category` | Enables |
423
+ |---|---|
424
+ | `codeql` | CodeQL default setup (provisioned asynchronously) |
425
+ | `secret_scanning` | Secret scanning |
426
+ | `dependabot_alerts_enabled` | Dependabot vulnerability alerts |
427
+ | `dependabot_updates_enabled` | Dependabot security updates (plus alerts) |
428
+ | `private_vulnerability_reporting` | Private vulnerability reporting |
429
+
430
+ Qualitative findings (Scorecard, zizmor, open Dependabot alerts, cooldown,
431
+ release freshness/mutability) are reported but not auto-remediated. Remediation
432
+ is organisation-scoped (`--scope org`, the default and only supported scope).
433
+
376
434
  ## Bulk Remediation Scripts
377
435
 
436
+ The standalone scripts below predate the `remediate` subcommand and remain for
437
+ ad-hoc, single-feature runs. For most workflows, prefer `remediate` above.
438
+
378
439
  The report ends with **nag lists** — repositories where a supported feature is
379
440
  switched off. Where GitHub exposes the relevant toggle through its REST API,
380
441
  the [`scripts/`](scripts/) directory ships standalone helpers that clear a whole
@@ -396,7 +457,8 @@ the current state of each repository, enables the feature where it is off, and
396
457
  verifies the result.
397
458
 
398
459
  ```bash
399
- # An org-admin token is required (classic PAT with repo admin / admin:org).
460
+ # An org-admin token is required: a classic PAT with the `repo` scope
461
+ # (administers repository security settings) plus `read:org` to enumerate repos.
400
462
  source ~/.secrets.github.classic.god # exports $GITHUB_TOKEN
401
463
 
402
464
  # Dry run (default): preview every change, touch nothing.
@@ -45,7 +45,7 @@ keywords = [
45
45
  ]
46
46
  dependencies = [
47
47
  "httpx[http2]==0.28.1",
48
- "typer==0.26.7",
48
+ "typer==0.26.8",
49
49
  "rich==15.0.0",
50
50
  "jinja2==3.1.6",
51
51
  "jsonschema==4.26.0",
@@ -63,9 +63,9 @@ dev = [
63
63
  "pytest-asyncio==1.4.0",
64
64
  "pytest-cov==7.1.0",
65
65
  "respx==0.23.1",
66
- "syrupy==5.3.3",
66
+ "syrupy==5.3.4",
67
67
  "mypy==2.1.0",
68
- "ruff==0.15.19",
68
+ "ruff==0.15.20",
69
69
  "types-jsonschema==4.26.0.20260518",
70
70
  "types-PyYAML==6.0.12.20250915",
71
71
  ]
@@ -95,9 +95,9 @@ dev = [
95
95
  "pytest-asyncio==1.4.0",
96
96
  "pytest-cov==7.1.0",
97
97
  "respx==0.23.1",
98
- "syrupy==5.3.3",
98
+ "syrupy==5.3.4",
99
99
  "mypy==2.1.0",
100
- "ruff==0.15.19",
100
+ "ruff==0.15.20",
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.4.1'
22
- __version_tuple__ = version_tuple = (0, 4, 1)
21
+ __version__ = version = '0.6.0'
22
+ __version_tuple__ = version_tuple = (0, 6, 0)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -38,6 +38,7 @@ class CategoryKey(str, Enum):
38
38
  DEPENDABOT_COOLDOWN = "dependabot_cooldown"
39
39
  RELEASES = "releases"
40
40
  MUTABLE_RELEASES = "mutable_releases"
41
+ PRIVATE_VULNERABILITY_REPORTING = "private_vulnerability_reporting"
41
42
 
42
43
 
43
44
  @dataclass(frozen=True)
@@ -104,8 +105,9 @@ _CATEGORIES: dict[CategoryKey, CategoryMeta] = {
104
105
  "Zizmor static analysis of GitHub Actions workflows, ranked "
105
106
  "worst-first by severity."
106
107
  ),
107
- # Zizmor's advisory "note" findings normalise to informational; only
108
- # those pass. A low-or-higher zizmor finding is treated as a failure.
108
+ # zizmor emits its Low findings at SARIF level "note", which
109
+ # normalises to LOW (see severity.py), so any zizmor finding fails --
110
+ # matching the ruleset-enforced PR gate that blocks on note-and-above.
109
111
  fail_severity=Severity.LOW,
110
112
  ),
111
113
  CategoryKey.DEPENDABOT_ALERTS: CategoryMeta(
@@ -210,6 +212,22 @@ _CATEGORIES: dict[CategoryKey, CategoryMeta] = {
210
212
  "cannot change after the fact."
211
213
  ),
212
214
  ),
215
+ CategoryKey.PRIVATE_VULNERABILITY_REPORTING: CategoryMeta(
216
+ key=CategoryKey.PRIVATE_VULNERABILITY_REPORTING,
217
+ title="Private Vulnerability Reporting",
218
+ pass_label="Enabled",
219
+ fail_label="Not enabled",
220
+ url=(
221
+ "https://docs.github.com/en/code-security/security-advisories/"
222
+ "working-with-repository-security-advisories/"
223
+ "configuring-private-vulnerability-reporting-for-a-repository"
224
+ ),
225
+ description=(
226
+ "Repositories with private vulnerability reporting disabled. Enable "
227
+ "it so security researchers can privately report vulnerabilities "
228
+ "instead of disclosing them publicly."
229
+ ),
230
+ ),
213
231
  }
214
232
 
215
233
 
@@ -17,7 +17,7 @@ import logging
17
17
  import os
18
18
  import re
19
19
  import sys
20
- from collections.abc import Callable, Mapping
20
+ from collections.abc import Callable, Mapping, Sequence
21
21
  from dataclasses import replace
22
22
  from pathlib import Path
23
23
  from typing import NoReturn
@@ -26,6 +26,7 @@ import typer
26
26
  from rich.console import Console
27
27
 
28
28
  from github_security_report import __version__, collect, config, gitctx, runner
29
+ from github_security_report import remediate as remediate_mod
29
30
  from github_security_report.categories import CategoryKey
30
31
  from github_security_report.client import GitHubClient, NetworkError
31
32
  from github_security_report.config import Config, OrgConfig, ReportConfig
@@ -48,7 +49,10 @@ def _version_callback(value: bool) -> None:
48
49
  if value:
49
50
  # Match the dependamerge style: a label emoji plus a Rich-highlighted
50
51
  # version number (Rich colourises the numeric version automatically).
51
- Console().print(f"🏷️ github-security-report version {__version__}")
52
+ # A single space follows the emoji: terminals that honour the VS16
53
+ # emoji-presentation width (e.g. Ghostty) render it two cells wide, so
54
+ # the extra pad the old double space added now reads as a gap.
55
+ Console().print(f"🏷️ github-security-report version {__version__}")
52
56
  raise typer.Exit()
53
57
 
54
58
 
@@ -133,6 +137,11 @@ def _org_to_dict(org: OrgReport) -> dict:
133
137
  "mutable_releases": (
134
138
  _table_to_dict(org.mutable_releases) if org.mutable_releases else None
135
139
  ),
140
+ "private_vulnerability_reporting": (
141
+ _table_to_dict(org.private_vulnerability_reporting)
142
+ if org.private_vulnerability_reporting
143
+ else None
144
+ ),
136
145
  }
137
146
 
138
147
 
@@ -415,6 +424,43 @@ async def _run_repo(owner: str, repo_name: str, *, token_env: str, console: Cons
415
424
  return 0
416
425
 
417
426
 
427
+ async def _run_remediate(
428
+ cfg: Config,
429
+ *,
430
+ console: Console,
431
+ token: str,
432
+ categories: Sequence[CategoryKey],
433
+ apply: bool,
434
+ ) -> int:
435
+ """Collect each org's posture and enable (or preview enabling) features.
436
+
437
+ A single write-capable token drives both the read (collection) and the
438
+ writes for every configured org, so the per-org read ``token_env`` in the
439
+ config is intentionally bypassed. Returns 1 when any enable failed, else 0.
440
+ """
441
+ now = dt.datetime.now(dt.timezone.utc)
442
+ failures = 0
443
+ async with GitHubClient(token) as client:
444
+ for org_cfg in cfg.organizations:
445
+ report = await collect.collect_org(
446
+ client, org_cfg, org_cfg.report, generated_at=now
447
+ )
448
+ results = await remediate_mod.remediate_org(
449
+ client, report, categories=categories, apply=apply
450
+ )
451
+ # Honour the org's configured terminal offender limit, the same
452
+ # cap the report's CLI output uses, so large orgs stay readable.
453
+ term_render.render_remediation(
454
+ report.org,
455
+ results,
456
+ console,
457
+ apply=apply,
458
+ top_n=org_cfg.report.cli_top_n,
459
+ )
460
+ failures += sum(result.failures for result in results)
461
+ return 1 if failures else 0
462
+
463
+
418
464
  def _repo_outputs(signals: list[RepoSignal], fail_threshold: str) -> dict[str, str]:
419
465
  outputs = {s.signal.value + "_open": str(s.counts.total) for s in signals}
420
466
  outputs["failed"] = "true" if runner.should_fail(signals, fail_threshold) else "false"
@@ -532,5 +578,75 @@ def report(
532
578
  raise typer.Exit(code)
533
579
 
534
580
 
581
+ @app.command()
582
+ def remediate(
583
+ config_file: str | None = typer.Option(None, "--config", "-c", help="Path to a JSON config file."),
584
+ config_data: str | None = typer.Option(None, "--config-data", help="Raw or base64 JSON config (vars/secrets)."),
585
+ org: str | None = typer.Option(None, "--org", help="Single organisation (shorthand for org mode)."),
586
+ scope: str = typer.Option("org", "--scope", help="Only 'org' is supported; remediation is organisation-scoped."),
587
+ category: list[str] | None = typer.Option(None, "--category", help="Remediable category to act on (repeatable; default: all). One of: codeql, secret_scanning, dependabot_alerts_enabled, dependabot_updates_enabled, private_vulnerability_reporting."),
588
+ token_env: str = typer.Option("GITHUB_TOKEN", "--token-env", help="Env var holding a WRITE-capable org-admin PAT. Used for both reading posture and enabling features across every configured org."),
589
+ apply: bool = typer.Option(False, "--apply", help="Perform the writes. Without this flag remediate only previews (dry run)."),
590
+ no_color: bool = typer.Option(False, "--no-color", help="Disable coloured output."),
591
+ ) -> None:
592
+ """Enable security features on repositories that lack them.
593
+
594
+ Runs the same collection the report uses, then switches on each selected
595
+ remediable feature wherever a repository has it confirmed off. Dry run by
596
+ default: pass --apply to make changes. Requires a write-capable token
597
+ (org admin), distinct from the read-only reporting PAT.
598
+ """
599
+ plain = no_color or bool(os.environ.get("CI")) or not sys.stdout.isatty()
600
+ console = Console(no_color=plain, highlight=False)
601
+
602
+ if scope != "org":
603
+ console.print("[red]remediate supports only --scope org[/red]")
604
+ raise typer.Exit(2)
605
+
606
+ keys, unknown = remediate_mod.parse_categories(category or [])
607
+ if unknown:
608
+ valid = ", ".join(key.value for key in remediate_mod.REMEDIABLE)
609
+ # markup=False: the user-supplied --category values are printed
610
+ # literally, so bracketed input cannot be interpreted as Rich markup.
611
+ console.print(
612
+ f"Unknown --category: {', '.join(unknown)}. Valid values: {valid}",
613
+ style="red",
614
+ markup=False,
615
+ )
616
+ raise typer.Exit(2)
617
+ categories = keys or list(remediate_mod.REMEDIABLE)
618
+
619
+ cfg = _load_config(config_file, config_data, org, token_env, console=console)
620
+ if cfg is None:
621
+ console.print(
622
+ "[red]No configuration: provide --config, --config-data or --org.[/red]"
623
+ )
624
+ raise typer.Exit(2)
625
+
626
+ token = os.environ.get(token_env, "").strip()
627
+ if not token:
628
+ # markup=False guards the user-supplied --token-env value.
629
+ console.print(
630
+ f"No token in ${token_env} (a write-capable org-admin PAT is required).",
631
+ style="red",
632
+ markup=False,
633
+ )
634
+ raise typer.Exit(2)
635
+
636
+ try:
637
+ code = asyncio.run(
638
+ _run_remediate(
639
+ cfg,
640
+ console=console,
641
+ token=token,
642
+ categories=categories,
643
+ apply=apply,
644
+ )
645
+ )
646
+ except NetworkError as exc:
647
+ _abort_network(console, exc)
648
+ raise typer.Exit(code)
649
+
650
+
535
651
  if __name__ == "__main__": # pragma: no cover
536
652
  app()