github-security-report 0.2.0__tar.gz → 0.3.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 (34) hide show
  1. {github_security_report-0.2.0 → github_security_report-0.3.0}/PKG-INFO +88 -12
  2. {github_security_report-0.2.0 → github_security_report-0.3.0}/README.md +85 -9
  3. {github_security_report-0.2.0 → github_security_report-0.3.0}/pyproject.toml +4 -4
  4. {github_security_report-0.2.0 → github_security_report-0.3.0}/src/github_security_report/_version.py +2 -2
  5. github_security_report-0.3.0/src/github_security_report/categories.py +223 -0
  6. {github_security_report-0.2.0 → github_security_report-0.3.0}/src/github_security_report/classify.py +70 -16
  7. {github_security_report-0.2.0 → github_security_report-0.3.0}/src/github_security_report/cli.py +98 -30
  8. {github_security_report-0.2.0 → github_security_report-0.3.0}/src/github_security_report/client.py +196 -12
  9. {github_security_report-0.2.0 → github_security_report-0.3.0}/src/github_security_report/collect.py +13 -1
  10. {github_security_report-0.2.0 → github_security_report-0.3.0}/src/github_security_report/config.py +180 -2
  11. {github_security_report-0.2.0 → github_security_report-0.3.0}/src/github_security_report/models.py +62 -15
  12. {github_security_report-0.2.0 → github_security_report-0.3.0}/src/github_security_report/posture.py +35 -88
  13. github_security_report-0.3.0/src/github_security_report/render/html.py +254 -0
  14. {github_security_report-0.2.0 → github_security_report-0.3.0}/src/github_security_report/render/markdown.py +131 -62
  15. {github_security_report-0.2.0 → github_security_report-0.3.0}/src/github_security_report/render/slack.py +100 -69
  16. {github_security_report-0.2.0 → github_security_report-0.3.0}/src/github_security_report/render/terminal.py +95 -51
  17. github_security_report-0.3.0/src/github_security_report/report.py +343 -0
  18. {github_security_report-0.2.0 → github_security_report-0.3.0}/src/github_security_report/severity.py +25 -13
  19. github_security_report-0.3.0/src/github_security_report/templates/report.html.j2 +148 -0
  20. github_security_report-0.2.0/src/github_security_report/render/html.py +0 -158
  21. github_security_report-0.2.0/src/github_security_report/report.py +0 -214
  22. github_security_report-0.2.0/src/github_security_report/templates/report.html.j2 +0 -195
  23. {github_security_report-0.2.0 → github_security_report-0.3.0}/.gitignore +0 -0
  24. {github_security_report-0.2.0 → github_security_report-0.3.0}/LICENSE +0 -0
  25. {github_security_report-0.2.0 → github_security_report-0.3.0}/LICENSES/Apache-2.0.txt +0 -0
  26. {github_security_report-0.2.0 → github_security_report-0.3.0}/scripts/README.md +0 -0
  27. {github_security_report-0.2.0 → github_security_report-0.3.0}/src/github_security_report/__init__.py +0 -0
  28. {github_security_report-0.2.0 → github_security_report-0.3.0}/src/github_security_report/gitctx.py +0 -0
  29. {github_security_report-0.2.0 → github_security_report-0.3.0}/src/github_security_report/py.typed +0 -0
  30. {github_security_report-0.2.0 → github_security_report-0.3.0}/src/github_security_report/render/__init__.py +0 -0
  31. {github_security_report-0.2.0 → github_security_report-0.3.0}/src/github_security_report/rulesets.py +0 -0
  32. {github_security_report-0.2.0 → github_security_report-0.3.0}/src/github_security_report/runner.py +0 -0
  33. {github_security_report-0.2.0 → github_security_report-0.3.0}/src/github_security_report/scope.py +0 -0
  34. {github_security_report-0.2.0 → github_security_report-0.3.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.2.0
3
+ Version: 0.3.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
@@ -34,9 +34,9 @@ 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
- Requires-Dist: pytest==9.1.0; extra == 'dev'
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.17; extra == 'dev'
39
+ Requires-Dist: ruff==0.15.18; extra == 'dev'
40
40
  Requires-Dist: syrupy==5.3.2; 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'
@@ -70,9 +70,16 @@ states and rendered worst-first:
70
70
 
71
71
  - **Offenders** — enabled with open findings (a ranked table row).
72
72
  - **Clean** — enabled with zero findings (a count beneath the table).
73
- - **Not enabled** — supported but switched off (a nag list prompting you to
74
- enable it).
75
- - **Unknown** — indeterminate (insufficient permission), footnoted separately.
73
+ - **Not enabled** — supported but switched off (a counted "disabled" footer
74
+ line, with the affected repositories named).
75
+ - **Unknown** — indeterminate (insufficient permission), counted separately.
76
+
77
+ Every category renders the same **standardised summary footer** beneath its
78
+ table: remediation-first count lines (failures, disabled, unknown, then the
79
+ healthy pass line, then excluded). The pass line reads **"All <state>"** when
80
+ nothing needs attention, or **"N <state>"** otherwise. The terminal and Slack
81
+ stay brevity-first; the explanatory per-category description and documentation
82
+ link are shown only on the richer Markdown and HTML (GitHub Pages) outputs.
76
83
 
77
84
  The single GitHub code-scanning feed is partitioned by `tool.name` into CodeQL,
78
85
  Scorecard, and zizmor; Scorecard prefers the external aggregate score and falls
@@ -90,9 +97,10 @@ tables (org mode):
90
97
  release/tag staleness (repository age never affects ordering; a repository
91
98
  with no release or tag ranks highest). Repositories younger than
92
99
  `repo_min_age_days` (default 28; `0` includes all) and those in
93
- `releases_exclude` are omitted. Set `release_max_age_days` to only flag
94
- repositories whose newest release or tag is older than that many days
95
- (default `0` = flag every eligible repository).
100
+ `releases_exclude` are omitted. A repository is flagged only when its newest
101
+ release or tag is older than `release_max_age_days` (default 60; `0` flags
102
+ every eligible repository), so a repository released or tagged within that
103
+ window counts as recently maintained and drops out of the table.
96
104
 
97
105
  ## Operating modes
98
106
 
@@ -222,7 +230,7 @@ environment-variable name, never embedded.
222
230
  "include_archived": false,
223
231
  "include_test": false,
224
232
  "repo_min_age_days": 28,
225
- "release_max_age_days": 0
233
+ "release_max_age_days": 60
226
234
  },
227
235
  "organizations": [
228
236
  {
@@ -251,10 +259,10 @@ The Releases / Tagging section has two independent freshness levers:
251
259
  period that omits **brand-new repositories** — those *created* within that
252
260
  many days — before a release or tag is expected of them. CLI:
253
261
  `--repo-min-age-days`.
254
- - `report.release_max_age_days` (default `0` = flag everything) is the
262
+ - `report.release_max_age_days` (default `60`; `0` = flag everything) is the
255
263
  release-staleness threshold: a repository is only flagged when its newest
256
264
  release **or** tag is older than that many days (a repository with neither is
257
- always flagged). Raise it to match your release cadence so actively released
265
+ always flagged). Tune it to match your release cadence so actively released
258
266
  repositories drop out of the table. CLI: `--release-max-age-days`.
259
267
 
260
268
  The per-org `releases_exclude` (CLI `--releases-exclude`, repeatable) drops
@@ -269,6 +277,74 @@ The per-org `exclude` list removes repositories from analysis entirely; they are
269
277
  reported as **excluded** (distinct from "not enabled"), so an intentional
270
278
  exclusion is visible rather than silently dropped.
271
279
 
280
+ ### Per-category render toggles
281
+
282
+ Every reporting category can be switched on or off, globally and per output
283
+ surface, under `report.categories`. Data is **always** collected; these toggles
284
+ govern presentation only. Each category key takes an `enabled` switch (highest
285
+ precedence — `false` hides it everywhere) and a lower-precedence `outputs` map
286
+ for the four surfaces (`cli`, `slack`, `markdown`, `html`). Everything defaults
287
+ to `true`, so an omitted category or key stays fully enabled. A category is
288
+ rendered on a surface only when `enabled` **and** that surface's toggle are
289
+ both true.
290
+
291
+ ```json
292
+ {
293
+ "report": {
294
+ "categories": {
295
+ "zizmor": { "enabled": false },
296
+ "releases": { "outputs": { "cli": false, "slack": false } }
297
+ }
298
+ },
299
+ "organizations": [{ "name": "lfreleng-actions" }]
300
+ }
301
+ ```
302
+
303
+ The example above hides Zizmor on every surface, and keeps Releases / Tagging
304
+ out of the terminal and Slack while still publishing it to the Markdown and HTML
305
+ Pages output. The valid category keys are: `codeql`, `scorecard`, `zizmor`,
306
+ `dependabot_alerts`, `secret_scanning`, `dependabot_alerts_enabled`,
307
+ `dependabot_updates_enabled`, `dependabot_cooldown`, `releases`,
308
+ `mutable_releases`. Like the other `report` settings, `categories` can be set
309
+ globally and overridden per organisation (overrides merge key-by-key, so
310
+ flipping one output leaves the rest untouched). The machine-readable
311
+ `report.json` artifact always contains the complete dataset, regardless of these
312
+ toggles.
313
+
314
+ When several organisations share one Slack channel they render into a single
315
+ combined digest, so the per-org Slack toggles are unioned for that channel: a
316
+ category appears if **any** contributing org would show it on Slack. An org-level
317
+ Slack disable therefore does not suppress a category in a shared-channel digest
318
+ unless every org sharing that channel also disables it (this mirrors the
319
+ most-generous `top_n` rule applied to the same grouping). The terminal, Markdown
320
+ and HTML surfaces are per-org and are not affected by this union.
321
+
322
+ ### Pass/fail severity cutoff
323
+
324
+ The severity-ranked signals (CodeQL, Scorecard, Zizmor, Dependabot alerts) use a
325
+ `fail_severity` cutoff to decide when a repository counts as a failure. A
326
+ repository is flagged as an offender only when it carries a finding **at or
327
+ above** the cutoff; findings below it fold into the clean count. Severities run
328
+ (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.
331
+
332
+ The global default cutoff is `medium`, so `low` and `informational` findings
333
+ pass. Zizmor defaults to `low` (only `informational` passes). Override the
334
+ cutoff per category under `report.categories.<key>.fail_severity`:
335
+
336
+ ```json
337
+ {
338
+ "report": {
339
+ "categories": {
340
+ "codeql": { "fail_severity": "low" },
341
+ "zizmor": { "fail_severity": "informational" }
342
+ }
343
+ },
344
+ "organizations": [{ "name": "lfreleng-actions" }]
345
+ }
346
+ ```
347
+
272
348
  `slack.channel` is optional. The action's `slack_channel` input (wired to the
273
349
  `SLACK_CHANNEL_ID` variable in `reporting.yaml`) overrides it, so the channel
274
350
  can live as an org/repo variable rather than in the config JSON. It must be the
@@ -26,9 +26,16 @@ states and rendered worst-first:
26
26
 
27
27
  - **Offenders** — enabled with open findings (a ranked table row).
28
28
  - **Clean** — enabled with zero findings (a count beneath the table).
29
- - **Not enabled** — supported but switched off (a nag list prompting you to
30
- enable it).
31
- - **Unknown** — indeterminate (insufficient permission), footnoted separately.
29
+ - **Not enabled** — supported but switched off (a counted "disabled" footer
30
+ line, with the affected repositories named).
31
+ - **Unknown** — indeterminate (insufficient permission), counted separately.
32
+
33
+ Every category renders the same **standardised summary footer** beneath its
34
+ table: remediation-first count lines (failures, disabled, unknown, then the
35
+ healthy pass line, then excluded). The pass line reads **"All <state>"** when
36
+ nothing needs attention, or **"N <state>"** otherwise. The terminal and Slack
37
+ stay brevity-first; the explanatory per-category description and documentation
38
+ link are shown only on the richer Markdown and HTML (GitHub Pages) outputs.
32
39
 
33
40
  The single GitHub code-scanning feed is partitioned by `tool.name` into CodeQL,
34
41
  Scorecard, and zizmor; Scorecard prefers the external aggregate score and falls
@@ -46,9 +53,10 @@ tables (org mode):
46
53
  release/tag staleness (repository age never affects ordering; a repository
47
54
  with no release or tag ranks highest). Repositories younger than
48
55
  `repo_min_age_days` (default 28; `0` includes all) and those in
49
- `releases_exclude` are omitted. Set `release_max_age_days` to only flag
50
- repositories whose newest release or tag is older than that many days
51
- (default `0` = flag every eligible repository).
56
+ `releases_exclude` are omitted. A repository is flagged only when its newest
57
+ release or tag is older than `release_max_age_days` (default 60; `0` flags
58
+ every eligible repository), so a repository released or tagged within that
59
+ window counts as recently maintained and drops out of the table.
52
60
 
53
61
  ## Operating modes
54
62
 
@@ -178,7 +186,7 @@ environment-variable name, never embedded.
178
186
  "include_archived": false,
179
187
  "include_test": false,
180
188
  "repo_min_age_days": 28,
181
- "release_max_age_days": 0
189
+ "release_max_age_days": 60
182
190
  },
183
191
  "organizations": [
184
192
  {
@@ -207,10 +215,10 @@ The Releases / Tagging section has two independent freshness levers:
207
215
  period that omits **brand-new repositories** — those *created* within that
208
216
  many days — before a release or tag is expected of them. CLI:
209
217
  `--repo-min-age-days`.
210
- - `report.release_max_age_days` (default `0` = flag everything) is the
218
+ - `report.release_max_age_days` (default `60`; `0` = flag everything) is the
211
219
  release-staleness threshold: a repository is only flagged when its newest
212
220
  release **or** tag is older than that many days (a repository with neither is
213
- always flagged). Raise it to match your release cadence so actively released
221
+ always flagged). Tune it to match your release cadence so actively released
214
222
  repositories drop out of the table. CLI: `--release-max-age-days`.
215
223
 
216
224
  The per-org `releases_exclude` (CLI `--releases-exclude`, repeatable) drops
@@ -225,6 +233,74 @@ The per-org `exclude` list removes repositories from analysis entirely; they are
225
233
  reported as **excluded** (distinct from "not enabled"), so an intentional
226
234
  exclusion is visible rather than silently dropped.
227
235
 
236
+ ### Per-category render toggles
237
+
238
+ Every reporting category can be switched on or off, globally and per output
239
+ surface, under `report.categories`. Data is **always** collected; these toggles
240
+ govern presentation only. Each category key takes an `enabled` switch (highest
241
+ precedence — `false` hides it everywhere) and a lower-precedence `outputs` map
242
+ for the four surfaces (`cli`, `slack`, `markdown`, `html`). Everything defaults
243
+ to `true`, so an omitted category or key stays fully enabled. A category is
244
+ rendered on a surface only when `enabled` **and** that surface's toggle are
245
+ both true.
246
+
247
+ ```json
248
+ {
249
+ "report": {
250
+ "categories": {
251
+ "zizmor": { "enabled": false },
252
+ "releases": { "outputs": { "cli": false, "slack": false } }
253
+ }
254
+ },
255
+ "organizations": [{ "name": "lfreleng-actions" }]
256
+ }
257
+ ```
258
+
259
+ The example above hides Zizmor on every surface, and keeps Releases / Tagging
260
+ out of the terminal and Slack while still publishing it to the Markdown and HTML
261
+ Pages output. The valid category keys are: `codeql`, `scorecard`, `zizmor`,
262
+ `dependabot_alerts`, `secret_scanning`, `dependabot_alerts_enabled`,
263
+ `dependabot_updates_enabled`, `dependabot_cooldown`, `releases`,
264
+ `mutable_releases`. Like the other `report` settings, `categories` can be set
265
+ globally and overridden per organisation (overrides merge key-by-key, so
266
+ flipping one output leaves the rest untouched). The machine-readable
267
+ `report.json` artifact always contains the complete dataset, regardless of these
268
+ toggles.
269
+
270
+ When several organisations share one Slack channel they render into a single
271
+ combined digest, so the per-org Slack toggles are unioned for that channel: a
272
+ category appears if **any** contributing org would show it on Slack. An org-level
273
+ Slack disable therefore does not suppress a category in a shared-channel digest
274
+ unless every org sharing that channel also disables it (this mirrors the
275
+ most-generous `top_n` rule applied to the same grouping). The terminal, Markdown
276
+ and HTML surfaces are per-org and are not affected by this union.
277
+
278
+ ### Pass/fail severity cutoff
279
+
280
+ The severity-ranked signals (CodeQL, Scorecard, Zizmor, Dependabot alerts) use a
281
+ `fail_severity` cutoff to decide when a repository counts as a failure. A
282
+ repository is flagged as an offender only when it carries a finding **at or
283
+ above** the cutoff; findings below it fold into the clean count. Severities run
284
+ (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.
287
+
288
+ The global default cutoff is `medium`, so `low` and `informational` findings
289
+ pass. Zizmor defaults to `low` (only `informational` passes). Override the
290
+ cutoff per category under `report.categories.<key>.fail_severity`:
291
+
292
+ ```json
293
+ {
294
+ "report": {
295
+ "categories": {
296
+ "codeql": { "fail_severity": "low" },
297
+ "zizmor": { "fail_severity": "informational" }
298
+ }
299
+ },
300
+ "organizations": [{ "name": "lfreleng-actions" }]
301
+ }
302
+ ```
303
+
228
304
  `slack.channel` is optional. The action's `slack_channel` input (wired to the
229
305
  `SLACK_CHANNEL_ID` variable in `reporting.yaml`) overrides it, so the channel
230
306
  can live as an org/repo variable rather than in the config JSON. It must be the
@@ -59,13 +59,13 @@ Issues = "https://github.com/lfreleng-actions/github-security-report-action/issu
59
59
 
60
60
  [project.optional-dependencies]
61
61
  dev = [
62
- "pytest==9.1.0",
62
+ "pytest==9.1.1",
63
63
  "pytest-asyncio==1.4.0",
64
64
  "pytest-cov==7.1.0",
65
65
  "respx==0.23.1",
66
66
  "syrupy==5.3.2",
67
67
  "mypy==2.1.0",
68
- "ruff==0.15.17",
68
+ "ruff==0.15.18",
69
69
  "types-jsonschema==4.26.0.20260518",
70
70
  "types-PyYAML==6.0.12.20250915",
71
71
  ]
@@ -91,13 +91,13 @@ include = ["src", "README.md", "LICENSE*", "pyproject.toml"]
91
91
 
92
92
  [dependency-groups]
93
93
  dev = [
94
- "pytest==9.1.0",
94
+ "pytest==9.1.1",
95
95
  "pytest-asyncio==1.4.0",
96
96
  "pytest-cov==7.1.0",
97
97
  "respx==0.23.1",
98
98
  "syrupy==5.3.2",
99
99
  "mypy==2.1.0",
100
- "ruff==0.15.17",
100
+ "ruff==0.15.18",
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.2.0'
22
- __version_tuple__ = version_tuple = (0, 2, 0)
21
+ __version__ = version = '0.3.0'
22
+ __version_tuple__ = version_tuple = (0, 3, 0)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -0,0 +1,223 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ # SPDX-FileCopyrightText: 2026 The Linux Foundation
3
+ """Report category metadata.
4
+
5
+ A single, render-surface-agnostic registry describing every reporting category
6
+ the tool produces. Each category carries its display title, the pass/fail
7
+ vocabulary used in the standardised summary footer, a documentation URL, and a
8
+ default human description. Renderers read this registry instead of hard-coding
9
+ per-category headings, labels and explanatory text, so a wording change here
10
+ flows to the terminal, Slack, Markdown and HTML surfaces at once.
11
+
12
+ The registry deliberately holds no behaviour and imports nothing from the rest
13
+ of the package except the leaf ``severity`` module (which itself imports nothing
14
+ from the package), so both the domain models and the renderers can depend on it
15
+ without a cycle. ``key`` values are the stable identifiers used by the
16
+ per-category configuration toggles, so treat them as part of the config
17
+ contract: rename with care.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ from dataclasses import dataclass
23
+ from enum import Enum
24
+
25
+ from github_security_report.severity import Severity
26
+
27
+
28
+ class CategoryKey(str, Enum):
29
+ """Stable identifier for one reporting category (also the config key)."""
30
+
31
+ CODEQL = "codeql"
32
+ SCORECARD = "scorecard"
33
+ ZIZMOR = "zizmor"
34
+ DEPENDABOT_ALERTS = "dependabot_alerts"
35
+ SECRET_SCANNING = "secret_scanning"
36
+ DEPENDABOT_ALERTS_ENABLED = "dependabot_alerts_enabled"
37
+ DEPENDABOT_UPDATES_ENABLED = "dependabot_updates_enabled"
38
+ DEPENDABOT_COOLDOWN = "dependabot_cooldown"
39
+ RELEASES = "releases"
40
+ MUTABLE_RELEASES = "mutable_releases"
41
+
42
+
43
+ @dataclass(frozen=True)
44
+ class CategoryMeta:
45
+ """Display and documentation metadata for one reporting category.
46
+
47
+ ``pass_label`` names the healthy state (e.g. ``"Clean"``, ``"Immutable"``)
48
+ and is what the summary footer reports as ``All <pass_label>`` when nothing
49
+ needs attention. ``fail_label`` names the actionable state for categories
50
+ with a binary pass/fail axis (enablement, cooldown, mutability, release
51
+ freshness); it is ``None`` for the severity-ranked signals, whose offenders
52
+ are enumerated in the table itself rather than as a single failure count.
53
+ ``description`` is the default explanatory text shown beneath the table on
54
+ the Markdown and HTML surfaces; a builder may override it at runtime when
55
+ the wording depends on configuration (e.g. the release-age thresholds).
56
+ """
57
+
58
+ key: CategoryKey
59
+ title: str
60
+ pass_label: str
61
+ fail_label: str | None
62
+ url: str
63
+ description: str = ""
64
+ # The lowest finding severity that counts as a failure for this category.
65
+ # A repository fails (appears as an offender) only when it carries a finding
66
+ # at or above this rung; findings below it fold into the clean count. The
67
+ # global default is MEDIUM, so Low and Informational findings pass; a
68
+ # category may lower it (Zizmor uses LOW, so only Informational passes).
69
+ # Meaningful only for the severity-ranked signals; binary categories ignore
70
+ # it. Overridable per category via the JSON config.
71
+ fail_severity: Severity = Severity.MEDIUM
72
+
73
+
74
+ _CATEGORIES: dict[CategoryKey, CategoryMeta] = {
75
+ CategoryKey.CODEQL: CategoryMeta(
76
+ key=CategoryKey.CODEQL,
77
+ title="CodeQL",
78
+ pass_label="Clean",
79
+ fail_label=None,
80
+ url="https://codeql.github.com/",
81
+ description=(
82
+ "CodeQL code-scanning findings, ranked worst-first by severity. "
83
+ "Each row shows a repository's open-alert counts."
84
+ ),
85
+ ),
86
+ CategoryKey.SCORECARD: CategoryMeta(
87
+ key=CategoryKey.SCORECARD,
88
+ title="OpenSSF Scorecard",
89
+ pass_label="Clean",
90
+ fail_label=None,
91
+ url="https://github.com/ossf/scorecard",
92
+ description=(
93
+ "OpenSSF Scorecard supply-chain health scores (a lower score is "
94
+ "weaker), ranked weakest-first."
95
+ ),
96
+ ),
97
+ CategoryKey.ZIZMOR: CategoryMeta(
98
+ key=CategoryKey.ZIZMOR,
99
+ title="Zizmor Static Analysis",
100
+ pass_label="Clean",
101
+ fail_label=None,
102
+ url="https://github.com/zizmorcore/zizmor",
103
+ description=(
104
+ "Zizmor static analysis of GitHub Actions workflows, ranked "
105
+ "worst-first by severity."
106
+ ),
107
+ # Zizmor's advisory "note" findings normalise to informational; only
108
+ # those pass. A low-or-higher zizmor finding is treated as a failure.
109
+ fail_severity=Severity.LOW,
110
+ ),
111
+ CategoryKey.DEPENDABOT_ALERTS: CategoryMeta(
112
+ key=CategoryKey.DEPENDABOT_ALERTS,
113
+ title="Dependabot: Security Alerts",
114
+ pass_label="Clean",
115
+ fail_label=None,
116
+ url=(
117
+ "https://docs.github.com/en/code-security/dependabot/"
118
+ "dependabot-alerts/about-dependabot-alerts"
119
+ ),
120
+ description=(
121
+ "Open Dependabot alerts for vulnerable dependencies, counted by "
122
+ "severity per repository."
123
+ ),
124
+ ),
125
+ CategoryKey.SECRET_SCANNING: CategoryMeta(
126
+ key=CategoryKey.SECRET_SCANNING,
127
+ title="Secret scanning",
128
+ pass_label="Clean",
129
+ fail_label=None,
130
+ url=(
131
+ "https://docs.github.com/en/code-security/secret-scanning/"
132
+ "about-secret-scanning"
133
+ ),
134
+ description=(
135
+ "Open secret-scanning alerts. Each row shows a repository's count "
136
+ "of detected, unresolved secrets."
137
+ ),
138
+ ),
139
+ CategoryKey.DEPENDABOT_ALERTS_ENABLED: CategoryMeta(
140
+ key=CategoryKey.DEPENDABOT_ALERTS_ENABLED,
141
+ title="Dependabot: Alerts Enabled",
142
+ pass_label="Enabled",
143
+ fail_label="Not enabled",
144
+ url=(
145
+ "https://docs.github.com/en/code-security/dependabot/"
146
+ "dependabot-alerts/configuring-dependabot-alerts"
147
+ ),
148
+ description=(
149
+ "Repositories with Dependabot security alerts disabled. Enable "
150
+ "them so vulnerable dependencies surface as alerts."
151
+ ),
152
+ ),
153
+ CategoryKey.DEPENDABOT_UPDATES_ENABLED: CategoryMeta(
154
+ key=CategoryKey.DEPENDABOT_UPDATES_ENABLED,
155
+ title="Dependabot: Security Updates",
156
+ pass_label="Enabled",
157
+ fail_label="Not enabled",
158
+ url=(
159
+ "https://docs.github.com/en/code-security/concepts/"
160
+ "supply-chain-security/dependabot-security-updates"
161
+ ),
162
+ description=(
163
+ "Repositories with Dependabot security updates disabled. Enable "
164
+ "them so fixes for vulnerable dependencies arrive as pull requests "
165
+ "automatically."
166
+ ),
167
+ ),
168
+ CategoryKey.DEPENDABOT_COOLDOWN: CategoryMeta(
169
+ key=CategoryKey.DEPENDABOT_COOLDOWN,
170
+ title="Dependabot: Cooldown Settings",
171
+ pass_label="Enabled",
172
+ fail_label="Without cooldown",
173
+ url=(
174
+ "https://docs.github.com/en/code-security/reference/"
175
+ "supply-chain-security/dependabot-options-reference#cooldown-"
176
+ ),
177
+ description=(
178
+ "Repositories whose Dependabot configuration omits an update "
179
+ "cooldown. A cooldown is mandatory; any cooldown value passes. "
180
+ "Repositories with no Dependabot configuration do not appear here."
181
+ ),
182
+ ),
183
+ CategoryKey.RELEASES: CategoryMeta(
184
+ key=CategoryKey.RELEASES,
185
+ title="Releases / Tagging",
186
+ pass_label="Current",
187
+ fail_label="Overdue",
188
+ url=(
189
+ "https://docs.github.com/en/repositories/"
190
+ "releasing-projects-on-github/about-releases"
191
+ ),
192
+ description=(
193
+ "Repositories ranked by combined release and tag staleness "
194
+ "(oldest first). A repository with neither a release nor a tag "
195
+ "ranks highest."
196
+ ),
197
+ ),
198
+ CategoryKey.MUTABLE_RELEASES: CategoryMeta(
199
+ key=CategoryKey.MUTABLE_RELEASES,
200
+ title="Mutable Releases",
201
+ pass_label="Immutable",
202
+ fail_label="Mutable",
203
+ url=(
204
+ "https://docs.github.com/en/code-security/concepts/"
205
+ "supply-chain-security/immutable-releases"
206
+ ),
207
+ description=(
208
+ "Repositories whose latest or last-published release is mutable. "
209
+ "Republish them as immutable releases so a published artifact "
210
+ "cannot change after the fact."
211
+ ),
212
+ ),
213
+ }
214
+
215
+
216
+ def category_meta(key: CategoryKey) -> CategoryMeta:
217
+ """The :class:`CategoryMeta` for ``key`` (registry lookup)."""
218
+ return _CATEGORIES[key]
219
+
220
+
221
+ def all_categories() -> tuple[CategoryMeta, ...]:
222
+ """Every category's metadata, in registry (render) order."""
223
+ return tuple(_CATEGORIES.values())