github-security-report 0.5.0__tar.gz → 0.7.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 (33) hide show
  1. {github_security_report-0.5.0 → github_security_report-0.7.0}/PKG-INFO +112 -21
  2. {github_security_report-0.5.0 → github_security_report-0.7.0}/README.md +109 -18
  3. {github_security_report-0.5.0 → github_security_report-0.7.0}/pyproject.toml +3 -3
  4. {github_security_report-0.5.0 → github_security_report-0.7.0}/src/github_security_report/_version.py +2 -2
  5. {github_security_report-0.5.0 → github_security_report-0.7.0}/src/github_security_report/categories.py +19 -2
  6. {github_security_report-0.5.0 → github_security_report-0.7.0}/src/github_security_report/classify.py +56 -19
  7. {github_security_report-0.5.0 → github_security_report-0.7.0}/src/github_security_report/cli.py +112 -1
  8. {github_security_report-0.5.0 → github_security_report-0.7.0}/src/github_security_report/client.py +187 -30
  9. {github_security_report-0.5.0 → github_security_report-0.7.0}/src/github_security_report/collect.py +63 -8
  10. {github_security_report-0.5.0 → github_security_report-0.7.0}/src/github_security_report/config.py +16 -1
  11. github_security_report-0.7.0/src/github_security_report/gating.py +189 -0
  12. {github_security_report-0.5.0 → github_security_report-0.7.0}/src/github_security_report/models.py +18 -2
  13. github_security_report-0.7.0/src/github_security_report/remediate.py +248 -0
  14. {github_security_report-0.5.0 → github_security_report-0.7.0}/src/github_security_report/render/html.py +20 -1
  15. {github_security_report-0.5.0 → github_security_report-0.7.0}/src/github_security_report/render/markdown.py +11 -0
  16. {github_security_report-0.5.0 → github_security_report-0.7.0}/src/github_security_report/render/slack.py +13 -0
  17. {github_security_report-0.5.0 → github_security_report-0.7.0}/src/github_security_report/render/terminal.py +87 -0
  18. {github_security_report-0.5.0 → github_security_report-0.7.0}/src/github_security_report/report.py +26 -3
  19. {github_security_report-0.5.0 → github_security_report-0.7.0}/src/github_security_report/rulesets.py +15 -0
  20. {github_security_report-0.5.0 → github_security_report-0.7.0}/src/github_security_report/severity.py +22 -13
  21. {github_security_report-0.5.0 → github_security_report-0.7.0}/src/github_security_report/templates/report.html.j2 +4 -0
  22. {github_security_report-0.5.0 → github_security_report-0.7.0}/.gitignore +0 -0
  23. {github_security_report-0.5.0 → github_security_report-0.7.0}/LICENSE +0 -0
  24. {github_security_report-0.5.0 → github_security_report-0.7.0}/LICENSES/Apache-2.0.txt +0 -0
  25. {github_security_report-0.5.0 → github_security_report-0.7.0}/scripts/README.md +0 -0
  26. {github_security_report-0.5.0 → github_security_report-0.7.0}/src/github_security_report/__init__.py +0 -0
  27. {github_security_report-0.5.0 → github_security_report-0.7.0}/src/github_security_report/gitctx.py +0 -0
  28. {github_security_report-0.5.0 → github_security_report-0.7.0}/src/github_security_report/posture.py +0 -0
  29. {github_security_report-0.5.0 → github_security_report-0.7.0}/src/github_security_report/py.typed +0 -0
  30. {github_security_report-0.5.0 → github_security_report-0.7.0}/src/github_security_report/render/__init__.py +0 -0
  31. {github_security_report-0.5.0 → github_security_report-0.7.0}/src/github_security_report/runner.py +0 -0
  32. {github_security_report-0.5.0 → github_security_report-0.7.0}/src/github_security_report/scope.py +0 -0
  33. {github_security_report-0.5.0 → github_security_report-0.7.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.5.0
3
+ Version: 0.7.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,7 +29,7 @@ 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'
@@ -37,7 +37,7 @@ 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
39
  Requires-Dist: ruff==0.15.20; extra == 'dev'
40
- Requires-Dist: syrupy==5.3.3; 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
@@ -56,8 +56,8 @@ Description-Content-Type: text/markdown
56
56
 
57
57
  > Security and quality **reporting** (not scanning) across GitHub
58
58
  > organisations. Aggregates existing signals — CodeQL, OpenSSF Scorecard,
59
- > zizmor, Dependabot, and secret scanning — and ranks the worst offenders so
60
- > remediation effort goes where it is needed.
59
+ > zizmor, aislop (AI slop), Dependabot, and secret scanning — and ranks the
60
+ > worst offenders so remediation effort goes where it is needed.
61
61
 
62
62
  ## 🗒️ Published reports
63
63
 
@@ -82,10 +82,21 @@ stay brevity-first; the explanatory per-category description and documentation
82
82
  link are shown only on the richer Markdown and HTML (GitHub Pages) outputs.
83
83
 
84
84
  The single GitHub code-scanning feed is partitioned by `tool.name` into CodeQL,
85
- Scorecard, and zizmor; Scorecard prefers the external aggregate score and falls
86
- back to code-scanning findings. See [`docs/BRIEF.md`](docs/BRIEF.md) and
87
- [`docs/phase0-findings.md`](docs/phase0-findings.md) for the full design and the
88
- API research it is built on.
85
+ Scorecard, zizmor, and aislop; Scorecard prefers the external aggregate score
86
+ and falls back to code-scanning findings. See [`docs/BRIEF.md`](docs/BRIEF.md)
87
+ and [`docs/phase0-findings.md`](docs/phase0-findings.md) for the full design and
88
+ the API research it is built on.
89
+
90
+ The workflow-driven signals (OpenSSF Scorecard, zizmor, aislop) only produce
91
+ data when an organisation has deployed supporting workflows. The tool checks
92
+ for that support cheaply before collecting (**feature gating**): an
93
+ organisation with no evidence of a tool — no ruleset requiring its workflow,
94
+ no alerts, no analyses on a sample of repositories — gets a single
95
+ `⏩ Skipping feature: organisation support missing` line for that section
96
+ instead of a nag list. See the
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.
89
100
 
90
101
  Further sections report **configuration posture** and **freshness** as plain
91
102
  tables (org mode):
@@ -153,7 +164,7 @@ organisation and **Repository access** set to *All repositories*, then grant:
153
164
  | Metadata | Mandatory baseline; listing organisation repositories |
154
165
  | Contents | `.github/dependabot.yml`, latest release, and tag dates |
155
166
  | Dependabot alerts | Open Dependabot vulnerability alerts |
156
- | Code scanning alerts | CodeQL / Scorecard / zizmor findings |
167
+ | Code scanning alerts | CodeQL / Scorecard / zizmor / aislop findings |
157
168
  | Secret scanning alerts | Open secret-scanning alerts |
158
169
  | Administration | Dependabot enablement + security-updates status, and effective branch rules |
159
170
 
@@ -311,7 +322,7 @@ both true.
311
322
  The example above hides Zizmor on every surface, and keeps Releases / Tagging
312
323
  out of the terminal and Slack while still publishing it to the Markdown and HTML
313
324
  Pages output. The valid category keys are: `codeql`, `scorecard`, `zizmor`,
314
- `dependabot_alerts`, `secret_scanning`, `dependabot_alerts_enabled`,
325
+ `aislop`, `dependabot_alerts`, `secret_scanning`, `dependabot_alerts_enabled`,
315
326
  `dependabot_updates_enabled`, `dependabot_cooldown`, `releases`,
316
327
  `mutable_releases`, `private_vulnerability_reporting`. Like the other `report`
317
328
  settings, `categories` can be set
@@ -328,19 +339,49 @@ unless every org sharing that channel also disables it (this mirrors the
328
339
  most-generous `top_n` rule applied to the same grouping). The terminal, Markdown
329
340
  and HTML surfaces are per-org and are not affected by this union.
330
341
 
342
+ ### Organisation feature gating
343
+
344
+ The workflow-driven signals (OpenSSF Scorecard, zizmor, aislop) need
345
+ organisation-deployed workflows before they produce any data (see the
346
+ [organisation scan setup guide](docs/org-scan-setup.md)). By default the tool
347
+ runs a cheap support check per organisation before collecting each of them:
348
+ evidence is an org ruleset requiring the tool's workflow, existing
349
+ code-scanning alerts from the tool, analyses on a sample of repositories, or
350
+ (for Scorecard) an external scorecard.dev score. A signal with no evidence is
351
+ **skipped** — not probed per repository, not classified — and its section
352
+ shows a single `⏩ Skipping feature: organisation support missing` line
353
+ linking the setup guide, on every output surface. Set `report.gating` to
354
+ `false` (globally or per organisation) to always probe everything:
355
+
356
+ ```json
357
+ {
358
+ "report": { "gating": false },
359
+ "organizations": [{ "name": "lfreleng-actions" }]
360
+ }
361
+ ```
362
+
363
+ Gating decides **collection**; the per-category render toggles above decide
364
+ **presentation**. A skipped section still renders (as the one-line notice)
365
+ unless its category is also disabled.
366
+
331
367
  ### Pass/fail severity cutoff
332
368
 
333
- The severity-ranked signals (CodeQL, Scorecard, Zizmor, Dependabot alerts) use a
334
- `fail_severity` cutoff to decide when a repository counts as a failure. A
335
- repository is flagged as an offender only when it carries a finding **at or
336
- above** the cutoff; findings below it fold into the clean count. Severities run
337
- (lowest to highest) `informational`, `low`, `medium`, `high`, `critical` —
338
- `informational` being the new sub-low rung that SARIF `note`/`none` findings
339
- (the bulk of a tool like Zizmor) normalise to.
369
+ The severity-ranked signals (CodeQL, Scorecard, Zizmor, aislop, Dependabot
370
+ alerts) use a `fail_severity` cutoff to decide when a repository counts as a
371
+ failure. A repository is flagged as an offender only when it carries a finding
372
+ **at or above** the cutoff; findings below it fold into the clean count.
373
+ Severities run (lowest to highest) `informational`, `low`, `medium`, `high`,
374
+ `critical` — `informational` being the sub-low rung for SARIF `none` findings
375
+ and unclassifiable alerts. Zizmor's SARIF `note` findings normalise to `low`
376
+ (zizmor emits its Low findings at `note`, and the organisation scan pipeline's
377
+ `--min-severity low` floor keeps informational findings out of the uploaded
378
+ SARIF), matching the ruleset-enforced PR gate that blocks on note-and-above.
379
+ aislop populates the same SARIF level axis and normalises identically.
340
380
 
341
381
  The global default cutoff is `medium`, so `low` and `informational` findings
342
- pass. Zizmor defaults to `low` (only `informational` passes). Override the
343
- cutoff per category under `report.categories.<key>.fail_severity`:
382
+ pass. Zizmor and aislop default to `low` (only `informational` passes).
383
+ Override the cutoff per category under
384
+ `report.categories.<key>.fail_severity`:
344
385
 
345
386
  ```json
346
387
  {
@@ -426,8 +467,57 @@ uvx github-security-report report
426
467
  uvx github-security-report report --org lfreleng-actions
427
468
  ```
428
469
 
470
+ ## Remediation
471
+
472
+ The `remediate` subcommand is the in-tool counterpart to the report: it runs the
473
+ same collection, then switches on each selected security feature wherever a
474
+ repository has it **confirmed off**. Only the offenders the report already
475
+ surfaces are acted on — repositories whose state could not be read are counted
476
+ as *unknown* and are never written to, so remediation never blind-writes.
477
+
478
+ It is **dry run by default** (these are privileged writes); pass `--apply` to
479
+ make changes. A single **write-capable** org-admin token (from `--token-env`,
480
+ default `GITHUB_TOKEN`) drives both the read and the writes across every
481
+ configured organisation, so it bypasses the per-org read-only `token_env` in the
482
+ config.
483
+
484
+ ```bash
485
+ # An org-admin token is required: a classic PAT with the `repo` scope
486
+ # (administers repository security settings) plus `read:org` to enumerate repos.
487
+ source ~/.secrets.github.classic.god # exports $GITHUB_TOKEN
488
+
489
+ # Dry run (default): preview every change, touch nothing.
490
+ uvx github-security-report remediate --org lfreleng-actions
491
+
492
+ # Apply: enable every remediable feature that is off, across all configured orgs.
493
+ uvx github-security-report remediate \
494
+ --config ~/.config/github-security-report/config.json --apply
495
+
496
+ # Limit to specific categories (repeatable).
497
+ uvx github-security-report remediate --org lfreleng-actions \
498
+ --category codeql --category private_vulnerability_reporting --apply
499
+ ```
500
+
501
+ The remediable categories are the simple on/off features with a documented
502
+ enablement endpoint:
503
+
504
+ | `--category` | Enables |
505
+ |---|---|
506
+ | `codeql` | CodeQL default setup (provisioned asynchronously) |
507
+ | `secret_scanning` | Secret scanning |
508
+ | `dependabot_alerts_enabled` | Dependabot vulnerability alerts |
509
+ | `dependabot_updates_enabled` | Dependabot security updates (plus alerts) |
510
+ | `private_vulnerability_reporting` | Private vulnerability reporting |
511
+
512
+ Qualitative findings (Scorecard, zizmor, open Dependabot alerts, cooldown,
513
+ release freshness/mutability) are reported but not auto-remediated. Remediation
514
+ is organisation-scoped (`--scope org`, the default and only supported scope).
515
+
429
516
  ## Bulk Remediation Scripts
430
517
 
518
+ The standalone scripts below predate the `remediate` subcommand and remain for
519
+ ad-hoc, single-feature runs. For most workflows, prefer `remediate` above.
520
+
431
521
  The report ends with **nag lists** — repositories where a supported feature is
432
522
  switched off. Where GitHub exposes the relevant toggle through its REST API,
433
523
  the [`scripts/`](scripts/) directory ships standalone helpers that clear a whole
@@ -449,7 +539,8 @@ the current state of each repository, enables the feature where it is off, and
449
539
  verifies the result.
450
540
 
451
541
  ```bash
452
- # An org-admin token is required (classic PAT with repo admin / admin:org).
542
+ # An org-admin token is required: a classic PAT with the `repo` scope
543
+ # (administers repository security settings) plus `read:org` to enumerate repos.
453
544
  source ~/.secrets.github.classic.god # exports $GITHUB_TOKEN
454
545
 
455
546
  # Dry run (default): preview every change, touch nothing.
@@ -12,8 +12,8 @@
12
12
 
13
13
  > Security and quality **reporting** (not scanning) across GitHub
14
14
  > organisations. Aggregates existing signals — CodeQL, OpenSSF Scorecard,
15
- > zizmor, Dependabot, and secret scanning — and ranks the worst offenders so
16
- > remediation effort goes where it is needed.
15
+ > zizmor, aislop (AI slop), Dependabot, and secret scanning — and ranks the
16
+ > worst offenders so remediation effort goes where it is needed.
17
17
 
18
18
  ## 🗒️ Published reports
19
19
 
@@ -38,10 +38,21 @@ stay brevity-first; the explanatory per-category description and documentation
38
38
  link are shown only on the richer Markdown and HTML (GitHub Pages) outputs.
39
39
 
40
40
  The single GitHub code-scanning feed is partitioned by `tool.name` into CodeQL,
41
- Scorecard, and zizmor; Scorecard prefers the external aggregate score and falls
42
- back to code-scanning findings. See [`docs/BRIEF.md`](docs/BRIEF.md) and
43
- [`docs/phase0-findings.md`](docs/phase0-findings.md) for the full design and the
44
- API research it is built on.
41
+ Scorecard, zizmor, and aislop; Scorecard prefers the external aggregate score
42
+ and falls back to code-scanning findings. See [`docs/BRIEF.md`](docs/BRIEF.md)
43
+ and [`docs/phase0-findings.md`](docs/phase0-findings.md) for the full design and
44
+ the API research it is built on.
45
+
46
+ The workflow-driven signals (OpenSSF Scorecard, zizmor, aislop) only produce
47
+ data when an organisation has deployed supporting workflows. The tool checks
48
+ for that support cheaply before collecting (**feature gating**): an
49
+ organisation with no evidence of a tool — no ruleset requiring its workflow,
50
+ no alerts, no analyses on a sample of repositories — gets a single
51
+ `⏩ Skipping feature: organisation support missing` line for that section
52
+ instead of a nag list. See the
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.
45
56
 
46
57
  Further sections report **configuration posture** and **freshness** as plain
47
58
  tables (org mode):
@@ -109,7 +120,7 @@ organisation and **Repository access** set to *All repositories*, then grant:
109
120
  | Metadata | Mandatory baseline; listing organisation repositories |
110
121
  | Contents | `.github/dependabot.yml`, latest release, and tag dates |
111
122
  | Dependabot alerts | Open Dependabot vulnerability alerts |
112
- | Code scanning alerts | CodeQL / Scorecard / zizmor findings |
123
+ | Code scanning alerts | CodeQL / Scorecard / zizmor / aislop findings |
113
124
  | Secret scanning alerts | Open secret-scanning alerts |
114
125
  | Administration | Dependabot enablement + security-updates status, and effective branch rules |
115
126
 
@@ -267,7 +278,7 @@ both true.
267
278
  The example above hides Zizmor on every surface, and keeps Releases / Tagging
268
279
  out of the terminal and Slack while still publishing it to the Markdown and HTML
269
280
  Pages output. The valid category keys are: `codeql`, `scorecard`, `zizmor`,
270
- `dependabot_alerts`, `secret_scanning`, `dependabot_alerts_enabled`,
281
+ `aislop`, `dependabot_alerts`, `secret_scanning`, `dependabot_alerts_enabled`,
271
282
  `dependabot_updates_enabled`, `dependabot_cooldown`, `releases`,
272
283
  `mutable_releases`, `private_vulnerability_reporting`. Like the other `report`
273
284
  settings, `categories` can be set
@@ -284,19 +295,49 @@ unless every org sharing that channel also disables it (this mirrors the
284
295
  most-generous `top_n` rule applied to the same grouping). The terminal, Markdown
285
296
  and HTML surfaces are per-org and are not affected by this union.
286
297
 
298
+ ### Organisation feature gating
299
+
300
+ The workflow-driven signals (OpenSSF Scorecard, zizmor, aislop) need
301
+ organisation-deployed workflows before they produce any data (see the
302
+ [organisation scan setup guide](docs/org-scan-setup.md)). By default the tool
303
+ runs a cheap support check per organisation before collecting each of them:
304
+ evidence is an org ruleset requiring the tool's workflow, existing
305
+ code-scanning alerts from the tool, analyses on a sample of repositories, or
306
+ (for Scorecard) an external scorecard.dev score. A signal with no evidence is
307
+ **skipped** — not probed per repository, not classified — and its section
308
+ shows a single `⏩ Skipping feature: organisation support missing` line
309
+ linking the setup guide, on every output surface. Set `report.gating` to
310
+ `false` (globally or per organisation) to always probe everything:
311
+
312
+ ```json
313
+ {
314
+ "report": { "gating": false },
315
+ "organizations": [{ "name": "lfreleng-actions" }]
316
+ }
317
+ ```
318
+
319
+ Gating decides **collection**; the per-category render toggles above decide
320
+ **presentation**. A skipped section still renders (as the one-line notice)
321
+ unless its category is also disabled.
322
+
287
323
  ### Pass/fail severity cutoff
288
324
 
289
- The severity-ranked signals (CodeQL, Scorecard, Zizmor, Dependabot alerts) use a
290
- `fail_severity` cutoff to decide when a repository counts as a failure. A
291
- repository is flagged as an offender only when it carries a finding **at or
292
- above** the cutoff; findings below it fold into the clean count. Severities run
293
- (lowest to highest) `informational`, `low`, `medium`, `high`, `critical` —
294
- `informational` being the new sub-low rung that SARIF `note`/`none` findings
295
- (the bulk of a tool like Zizmor) normalise to.
325
+ The severity-ranked signals (CodeQL, Scorecard, Zizmor, aislop, Dependabot
326
+ alerts) use a `fail_severity` cutoff to decide when a repository counts as a
327
+ failure. A repository is flagged as an offender only when it carries a finding
328
+ **at or above** the cutoff; findings below it fold into the clean count.
329
+ Severities run (lowest to highest) `informational`, `low`, `medium`, `high`,
330
+ `critical` — `informational` being the sub-low rung for SARIF `none` findings
331
+ and unclassifiable alerts. Zizmor's SARIF `note` findings normalise to `low`
332
+ (zizmor emits its Low findings at `note`, and the organisation scan pipeline's
333
+ `--min-severity low` floor keeps informational findings out of the uploaded
334
+ SARIF), matching the ruleset-enforced PR gate that blocks on note-and-above.
335
+ aislop populates the same SARIF level axis and normalises identically.
296
336
 
297
337
  The global default cutoff is `medium`, so `low` and `informational` findings
298
- pass. Zizmor defaults to `low` (only `informational` passes). Override the
299
- cutoff per category under `report.categories.<key>.fail_severity`:
338
+ pass. Zizmor and aislop default to `low` (only `informational` passes).
339
+ Override the cutoff per category under
340
+ `report.categories.<key>.fail_severity`:
300
341
 
301
342
  ```json
302
343
  {
@@ -382,8 +423,57 @@ uvx github-security-report report
382
423
  uvx github-security-report report --org lfreleng-actions
383
424
  ```
384
425
 
426
+ ## Remediation
427
+
428
+ The `remediate` subcommand is the in-tool counterpart to the report: it runs the
429
+ same collection, then switches on each selected security feature wherever a
430
+ repository has it **confirmed off**. Only the offenders the report already
431
+ surfaces are acted on — repositories whose state could not be read are counted
432
+ as *unknown* and are never written to, so remediation never blind-writes.
433
+
434
+ It is **dry run by default** (these are privileged writes); pass `--apply` to
435
+ make changes. A single **write-capable** org-admin token (from `--token-env`,
436
+ default `GITHUB_TOKEN`) drives both the read and the writes across every
437
+ configured organisation, so it bypasses the per-org read-only `token_env` in the
438
+ config.
439
+
440
+ ```bash
441
+ # An org-admin token is required: a classic PAT with the `repo` scope
442
+ # (administers repository security settings) plus `read:org` to enumerate repos.
443
+ source ~/.secrets.github.classic.god # exports $GITHUB_TOKEN
444
+
445
+ # Dry run (default): preview every change, touch nothing.
446
+ uvx github-security-report remediate --org lfreleng-actions
447
+
448
+ # Apply: enable every remediable feature that is off, across all configured orgs.
449
+ uvx github-security-report remediate \
450
+ --config ~/.config/github-security-report/config.json --apply
451
+
452
+ # Limit to specific categories (repeatable).
453
+ uvx github-security-report remediate --org lfreleng-actions \
454
+ --category codeql --category private_vulnerability_reporting --apply
455
+ ```
456
+
457
+ The remediable categories are the simple on/off features with a documented
458
+ enablement endpoint:
459
+
460
+ | `--category` | Enables |
461
+ |---|---|
462
+ | `codeql` | CodeQL default setup (provisioned asynchronously) |
463
+ | `secret_scanning` | Secret scanning |
464
+ | `dependabot_alerts_enabled` | Dependabot vulnerability alerts |
465
+ | `dependabot_updates_enabled` | Dependabot security updates (plus alerts) |
466
+ | `private_vulnerability_reporting` | Private vulnerability reporting |
467
+
468
+ Qualitative findings (Scorecard, zizmor, open Dependabot alerts, cooldown,
469
+ release freshness/mutability) are reported but not auto-remediated. Remediation
470
+ is organisation-scoped (`--scope org`, the default and only supported scope).
471
+
385
472
  ## Bulk Remediation Scripts
386
473
 
474
+ The standalone scripts below predate the `remediate` subcommand and remain for
475
+ ad-hoc, single-feature runs. For most workflows, prefer `remediate` above.
476
+
387
477
  The report ends with **nag lists** — repositories where a supported feature is
388
478
  switched off. Where GitHub exposes the relevant toggle through its REST API,
389
479
  the [`scripts/`](scripts/) directory ships standalone helpers that clear a whole
@@ -405,7 +495,8 @@ the current state of each repository, enables the feature where it is off, and
405
495
  verifies the result.
406
496
 
407
497
  ```bash
408
- # An org-admin token is required (classic PAT with repo admin / admin:org).
498
+ # An org-admin token is required: a classic PAT with the `repo` scope
499
+ # (administers repository security settings) plus `read:org` to enumerate repos.
409
500
  source ~/.secrets.github.classic.god # exports $GITHUB_TOKEN
410
501
 
411
502
  # 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,7 +63,7 @@ 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
68
  "ruff==0.15.20",
69
69
  "types-jsonschema==4.26.0.20260518",
@@ -95,7 +95,7 @@ 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
100
  "ruff==0.15.20",
101
101
  "types-jsonschema==4.26.0.20260518",
@@ -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.5.0'
22
- __version_tuple__ = version_tuple = (0, 5, 0)
21
+ __version__ = version = '0.7.0'
22
+ __version_tuple__ = version_tuple = (0, 7, 0)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -31,6 +31,7 @@ class CategoryKey(str, Enum):
31
31
  CODEQL = "codeql"
32
32
  SCORECARD = "scorecard"
33
33
  ZIZMOR = "zizmor"
34
+ AISLOP = "aislop"
34
35
  DEPENDABOT_ALERTS = "dependabot_alerts"
35
36
  SECRET_SCANNING = "secret_scanning"
36
37
  DEPENDABOT_ALERTS_ENABLED = "dependabot_alerts_enabled"
@@ -105,8 +106,24 @@ _CATEGORIES: dict[CategoryKey, CategoryMeta] = {
105
106
  "Zizmor static analysis of GitHub Actions workflows, ranked "
106
107
  "worst-first by severity."
107
108
  ),
108
- # Zizmor's advisory "note" findings normalise to informational; only
109
- # those pass. A low-or-higher zizmor finding is treated as a failure.
109
+ # zizmor emits its Low findings at SARIF level "note", which
110
+ # normalises to LOW (see severity.py), so any zizmor finding fails --
111
+ # matching the ruleset-enforced PR gate that blocks on note-and-above.
112
+ fail_severity=Severity.LOW,
113
+ ),
114
+ CategoryKey.AISLOP: CategoryMeta(
115
+ key=CategoryKey.AISLOP,
116
+ title="AI Slop Analysis",
117
+ pass_label="Clean",
118
+ fail_label=None,
119
+ url="https://github.com/scanaislop/aislop",
120
+ description=(
121
+ "aislop AI-slop / code-quality findings, ranked worst-first by "
122
+ "severity."
123
+ ),
124
+ # aislop, like zizmor, populates only the SARIF level axis
125
+ # (error/warning/note); "note" normalises to LOW (see severity.py), so
126
+ # any aislop finding fails -- matching the ruleset-enforced PR gate.
110
127
  fail_severity=Severity.LOW,
111
128
  ),
112
129
  CategoryKey.DEPENDABOT_ALERTS: CategoryMeta(
@@ -6,8 +6,8 @@ Pure, transport-free logic encoding every Phase 0 finding
6
6
  (``docs/phase0-findings.md``):
7
7
 
8
8
  - the single code-scanning feed is partitioned by ``tool.name`` into CodeQL,
9
- Scorecard and zizmor -- counts are filtered per tool;
10
- - CodeQL/Scorecard/zizmor enablement is the presence of that tool in
9
+ Scorecard, zizmor and aislop -- counts are filtered per tool;
10
+ - CodeQL/Scorecard/zizmor/aislop enablement is the presence of that tool in
11
11
  ``code-scanning/analyses`` (not ``default-setup``); a 404 on code scanning
12
12
  means it is disabled entirely;
13
13
  - secret scanning 404 = disabled, 200 [] = enabled-clean;
@@ -18,11 +18,12 @@ Pure, transport-free logic encoding every Phase 0 finding
18
18
 
19
19
  from __future__ import annotations
20
20
 
21
- from collections.abc import Mapping
21
+ from collections.abc import Callable, Mapping, Set
22
22
  from dataclasses import dataclass, field
23
23
 
24
24
  from github_security_report import severity
25
25
  from github_security_report.models import (
26
+ CODE_SCANNING_TOOLS,
26
27
  Repo,
27
28
  RepoSignal,
28
29
  RepoState,
@@ -50,7 +51,7 @@ class RepoFacts:
50
51
  """Raw per-repository facts gathered by the client, pre-classification."""
51
52
 
52
53
  repo: Repo
53
- # Code scanning (covers CodeQL, Scorecard, zizmor).
54
+ # Code scanning (covers CodeQL, Scorecard, zizmor, aislop).
54
55
  code_scanning_status: int = 200 # 200 ok, 404 disabled, 403 forbidden
55
56
  code_scanning_tools: set[str] = field(default_factory=set) # analyses tool names
56
57
  code_scanning_alerts: list[dict] = field(default_factory=list) # all tools
@@ -149,7 +150,10 @@ def classify_codeql(
149
150
  facts: RepoFacts, fail_severities: FailSeverities | None = None
150
151
  ) -> RepoSignal:
151
152
  return _code_scanning_tool_signal(
152
- facts, SignalType.CODEQL, "CodeQL", fail_severities
153
+ facts,
154
+ SignalType.CODEQL,
155
+ CODE_SCANNING_TOOLS[SignalType.CODEQL],
156
+ fail_severities,
153
157
  )
154
158
 
155
159
 
@@ -157,7 +161,21 @@ def classify_zizmor(
157
161
  facts: RepoFacts, fail_severities: FailSeverities | None = None
158
162
  ) -> RepoSignal:
159
163
  return _code_scanning_tool_signal(
160
- facts, SignalType.ZIZMOR, "zizmor", fail_severities
164
+ facts,
165
+ SignalType.ZIZMOR,
166
+ CODE_SCANNING_TOOLS[SignalType.ZIZMOR],
167
+ fail_severities,
168
+ )
169
+
170
+
171
+ def classify_aislop(
172
+ facts: RepoFacts, fail_severities: FailSeverities | None = None
173
+ ) -> RepoSignal:
174
+ return _code_scanning_tool_signal(
175
+ facts,
176
+ SignalType.AISLOP,
177
+ CODE_SCANNING_TOOLS[SignalType.AISLOP],
178
+ fail_severities,
161
179
  )
162
180
 
163
181
 
@@ -166,8 +184,13 @@ def classify_scorecard(
166
184
  ) -> RepoSignal:
167
185
  """Scorecard: prefer the external aggregate score, else code-scanning findings."""
168
186
  repo = facts.repo
169
- counts = count_code_scanning(facts.code_scanning_alerts, "Scorecard")
170
- has_cs = "Scorecard" in facts.code_scanning_tools and facts.code_scanning_status == 200
187
+ counts = count_code_scanning(
188
+ facts.code_scanning_alerts, CODE_SCANNING_TOOLS[SignalType.SCORECARD]
189
+ )
190
+ has_cs = (
191
+ CODE_SCANNING_TOOLS[SignalType.SCORECARD] in facts.code_scanning_tools
192
+ and facts.code_scanning_status == 200
193
+ )
171
194
  cutoff = _cutoff(SignalType.SCORECARD, fail_severities)
172
195
 
173
196
  if facts.scorecard_status == 200 and facts.scorecard_score is not None:
@@ -241,22 +264,36 @@ def classify_dependabot(
241
264
  return RepoSignal(repo, SignalType.DEPENDABOT, state, counts=counts)
242
265
 
243
266
 
244
- _CLASSIFIERS = (
245
- classify_codeql,
246
- classify_scorecard,
247
- classify_zizmor,
248
- classify_dependabot,
249
- classify_secret_scanning,
250
- )
267
+ # One classifier per signal, keyed so orchestration can skip gated-out signals.
268
+ _Classifier = Callable[["RepoFacts", "FailSeverities | None"], RepoSignal]
269
+
270
+ _CLASSIFIERS: dict[SignalType, _Classifier] = {
271
+ SignalType.CODEQL: classify_codeql,
272
+ SignalType.SCORECARD: classify_scorecard,
273
+ SignalType.ZIZMOR: classify_zizmor,
274
+ SignalType.AISLOP: classify_aislop,
275
+ SignalType.DEPENDABOT: classify_dependabot,
276
+ SignalType.SECRET_SCANNING: classify_secret_scanning,
277
+ }
251
278
 
252
279
 
253
280
  def classify_repo(
254
- facts: RepoFacts, fail_severities: FailSeverities | None = None
281
+ facts: RepoFacts,
282
+ fail_severities: FailSeverities | None = None,
283
+ *,
284
+ skip: Set[SignalType] = frozenset(),
255
285
  ) -> list[RepoSignal]:
256
- """Classify a repository across all five signals.
286
+ """Classify a repository across all signals (minus any in ``skip``).
257
287
 
258
288
  ``fail_severities`` optionally overrides the per-signal fail-severity cutoff
259
289
  (otherwise each signal uses its category default); it governs which findings
260
- are severe enough to mark a repository as an offender.
290
+ are severe enough to mark a repository as an offender. ``skip`` names
291
+ signals to leave unclassified entirely -- used when organisation feature
292
+ gating (:mod:`gating`) found no support for a workflow-driven tool, so the
293
+ repository is neither nagged nor counted for that signal.
261
294
  """
262
- return [classifier(facts, fail_severities) for classifier in _CLASSIFIERS]
295
+ return [
296
+ classifier(facts, fail_severities)
297
+ for signal, classifier in _CLASSIFIERS.items()
298
+ if signal not in skip
299
+ ]