github-security-report 0.1.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.
- {github_security_report-0.1.0 → github_security_report-0.3.0}/PKG-INFO +177 -29
- {github_security_report-0.1.0 → github_security_report-0.3.0}/README.md +174 -26
- {github_security_report-0.1.0 → github_security_report-0.3.0}/pyproject.toml +4 -4
- {github_security_report-0.1.0 → github_security_report-0.3.0}/scripts/README.md +52 -0
- {github_security_report-0.1.0 → github_security_report-0.3.0}/src/github_security_report/_version.py +2 -2
- github_security_report-0.3.0/src/github_security_report/categories.py +223 -0
- {github_security_report-0.1.0 → github_security_report-0.3.0}/src/github_security_report/classify.py +70 -16
- {github_security_report-0.1.0 → github_security_report-0.3.0}/src/github_security_report/cli.py +133 -46
- github_security_report-0.3.0/src/github_security_report/client.py +798 -0
- {github_security_report-0.1.0 → github_security_report-0.3.0}/src/github_security_report/collect.py +145 -61
- {github_security_report-0.1.0 → github_security_report-0.3.0}/src/github_security_report/config.py +224 -8
- {github_security_report-0.1.0 → github_security_report-0.3.0}/src/github_security_report/models.py +103 -15
- github_security_report-0.3.0/src/github_security_report/posture.py +393 -0
- github_security_report-0.3.0/src/github_security_report/render/html.py +254 -0
- github_security_report-0.3.0/src/github_security_report/render/markdown.py +278 -0
- {github_security_report-0.1.0 → github_security_report-0.3.0}/src/github_security_report/render/slack.py +132 -57
- github_security_report-0.3.0/src/github_security_report/render/terminal.py +210 -0
- github_security_report-0.3.0/src/github_security_report/report.py +343 -0
- {github_security_report-0.1.0 → github_security_report-0.3.0}/src/github_security_report/severity.py +25 -13
- {github_security_report-0.1.0 → github_security_report-0.3.0}/src/github_security_report/templates/report.html.j2 +57 -71
- github_security_report-0.1.0/src/github_security_report/client.py +0 -493
- github_security_report-0.1.0/src/github_security_report/posture.py +0 -264
- github_security_report-0.1.0/src/github_security_report/render/html.py +0 -142
- github_security_report-0.1.0/src/github_security_report/render/markdown.py +0 -172
- github_security_report-0.1.0/src/github_security_report/render/terminal.py +0 -163
- github_security_report-0.1.0/src/github_security_report/report.py +0 -173
- {github_security_report-0.1.0 → github_security_report-0.3.0}/.gitignore +0 -0
- {github_security_report-0.1.0 → github_security_report-0.3.0}/LICENSE +0 -0
- {github_security_report-0.1.0 → github_security_report-0.3.0}/LICENSES/Apache-2.0.txt +0 -0
- {github_security_report-0.1.0 → github_security_report-0.3.0}/src/github_security_report/__init__.py +0 -0
- {github_security_report-0.1.0 → github_security_report-0.3.0}/src/github_security_report/gitctx.py +0 -0
- {github_security_report-0.1.0 → github_security_report-0.3.0}/src/github_security_report/py.typed +0 -0
- {github_security_report-0.1.0 → github_security_report-0.3.0}/src/github_security_report/render/__init__.py +0 -0
- {github_security_report-0.1.0 → github_security_report-0.3.0}/src/github_security_report/rulesets.py +0 -0
- {github_security_report-0.1.0 → github_security_report-0.3.0}/src/github_security_report/runner.py +0 -0
- {github_security_report-0.1.0 → github_security_report-0.3.0}/src/github_security_report/scope.py +0 -0
- {github_security_report-0.1.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.
|
|
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.
|
|
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.
|
|
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
|
|
74
|
-
|
|
75
|
-
- **Unknown** — indeterminate (insufficient permission),
|
|
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
|
|
@@ -86,10 +93,14 @@ tables (org mode):
|
|
|
86
93
|
- **Dependabot** — three tables: repositories with vulnerability **alerts not
|
|
87
94
|
enabled**, repositories with **security updates not enabled**, and ecosystems
|
|
88
95
|
with no update `cooldown` configured (mandatory; any value passes).
|
|
89
|
-
- **Releases / Tagging** — repositories overdue a release or tag, ranked by
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
`
|
|
96
|
+
- **Releases / Tagging** — repositories overdue a release or tag, ranked by
|
|
97
|
+
release/tag staleness (repository age never affects ordering; a repository
|
|
98
|
+
with no release or tag ranks highest). Repositories younger than
|
|
99
|
+
`repo_min_age_days` (default 28; `0` includes all) and those in
|
|
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.
|
|
93
104
|
|
|
94
105
|
## Operating modes
|
|
95
106
|
|
|
@@ -109,11 +120,20 @@ Repo mode needs nothing beyond the workflow's ephemeral `GITHUB_TOKEN`. Org mode
|
|
|
109
120
|
needs a Personal Access Token; choose **one** of the two options below depending
|
|
110
121
|
on how many organisations the report covers.
|
|
111
122
|
|
|
112
|
-
|
|
113
|
-
permitted to make to an "unknown" status rather than reporting a repository
|
|
114
|
-
clean, so an under-scoped token surfaces as unknowns in the report instead of
|
|
123
|
+
Almost all required access is **read-only**. The tool degrades any read it is
|
|
124
|
+
not permitted to make to an "unknown" status rather than reporting a repository
|
|
125
|
+
as clean, so an under-scoped token surfaces as unknowns in the report instead of
|
|
115
126
|
silently wrong results — start minimal and widen if you see unknowns.
|
|
116
127
|
|
|
128
|
+
The **one** exception is organisation-ruleset coverage. GitHub gates the
|
|
129
|
+
org-rulesets endpoint behind an org-admin permission (classic `admin:org` scope,
|
|
130
|
+
or fine-grained Administration **write**), even though the tool only reads it.
|
|
131
|
+
That coverage is **optional**: it detects tools enforced through an org ruleset
|
|
132
|
+
(for example a required-workflow or code-scanning ruleset). Without it that one
|
|
133
|
+
signal is skipped and every other part of the report is unaffected, so the
|
|
134
|
+
minimal tokens below omit it. Grant the org-admin permission only if you want
|
|
135
|
+
ruleset-based tool coverage.
|
|
136
|
+
|
|
117
137
|
### Single organisation — fine-grained PAT
|
|
118
138
|
|
|
119
139
|
A fine-grained PAT is bound to one resource owner, so it works for a report
|
|
@@ -131,14 +151,15 @@ organisation and **Repository access** set to *All repositories*, then grant:
|
|
|
131
151
|
| Secret scanning alerts | Open secret-scanning alerts |
|
|
132
152
|
| Administration | Dependabot enablement + security-updates status, and effective branch rules |
|
|
133
153
|
|
|
134
|
-
**Organization permissions
|
|
154
|
+
**Organization permissions:**
|
|
135
155
|
|
|
136
|
-
| Permission | Used for |
|
|
137
|
-
| ---------- | -------- |
|
|
138
|
-
| Administration |
|
|
156
|
+
| Permission | Access | Used for |
|
|
157
|
+
| ---------- | ------ | -------- |
|
|
158
|
+
| Administration | Read and write | *Optional* — organisation rulesets (detect tools enforced through an org ruleset). GitHub gates this endpoint behind Administration **write**; omit it to keep the token read-only and skip ruleset-based tool coverage. |
|
|
139
159
|
|
|
140
|
-
>
|
|
141
|
-
>
|
|
160
|
+
> Read-only is enough for everything except the optional ruleset coverage
|
|
161
|
+
> above. A fine-grained token cannot span organisations. For a report covering
|
|
162
|
+
> more than one org, use a classic PAT (below).
|
|
142
163
|
|
|
143
164
|
### Multiple organisations — classic PAT
|
|
144
165
|
|
|
@@ -150,7 +171,8 @@ organisations. Grant these scopes:
|
|
|
150
171
|
| ----- | -------- |
|
|
151
172
|
| `repo` | Repository data, including private repositories |
|
|
152
173
|
| `security_events` | Code scanning, secret scanning, and Dependabot alerts (org-bulk and per-repo) |
|
|
153
|
-
| `read:org` | Listing organisation repositories
|
|
174
|
+
| `read:org` | Listing organisation repositories |
|
|
175
|
+
| `admin:org` | *Optional* — reading organisation rulesets for ruleset-based tool coverage. GitHub gates `GET /orgs/{org}/rulesets` behind the full `admin:org` scope; `read:org` and `write:org` return 404. Omit it to skip that one signal; everything else is unaffected. |
|
|
154
176
|
|
|
155
177
|
> For organisations that enforce SSO, the PAT must be **SSO-authorised** for
|
|
156
178
|
> each target organisation, or the org-level endpoints return `403` (reported as
|
|
@@ -207,7 +229,8 @@ environment-variable name, never embedded.
|
|
|
207
229
|
"top_n_slack": 10,
|
|
208
230
|
"include_archived": false,
|
|
209
231
|
"include_test": false,
|
|
210
|
-
"
|
|
232
|
+
"repo_min_age_days": 28,
|
|
233
|
+
"release_max_age_days": 60
|
|
211
234
|
},
|
|
212
235
|
"organizations": [
|
|
213
236
|
{
|
|
@@ -226,17 +249,102 @@ environment-variable name, never embedded.
|
|
|
226
249
|
`top_n` controls how many offenders are shown per signal. It is the shared
|
|
227
250
|
default for all three outputs; set any of `top_n_report` (GitHub Pages),
|
|
228
251
|
`top_n_cli` (terminal), or `top_n_slack` (Slack digest) to override an
|
|
229
|
-
individual output.
|
|
252
|
+
individual output. Set a value to `0` to remove the limit entirely and show
|
|
253
|
+
every offender. Each can also be set at the CLI with `--top-n`,
|
|
230
254
|
`--top-n-report`, `--top-n-cli`, and `--top-n-slack`.
|
|
231
255
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
256
|
+
The Releases / Tagging section has two independent freshness levers:
|
|
257
|
+
|
|
258
|
+
- `report.repo_min_age_days` (default `28`, `0` = include all) is a grace
|
|
259
|
+
period that omits **brand-new repositories** — those *created* within that
|
|
260
|
+
many days — before a release or tag is expected of them. CLI:
|
|
261
|
+
`--repo-min-age-days`.
|
|
262
|
+
- `report.release_max_age_days` (default `60`; `0` = flag everything) is the
|
|
263
|
+
release-staleness threshold: a repository is only flagged when its newest
|
|
264
|
+
release **or** tag is older than that many days (a repository with neither is
|
|
265
|
+
always flagged). Tune it to match your release cadence so actively released
|
|
266
|
+
repositories drop out of the table. CLI: `--release-max-age-days`.
|
|
267
|
+
|
|
268
|
+
The per-org `releases_exclude` (CLI `--releases-exclude`, repeatable) drops
|
|
269
|
+
named repositories from the section entirely.
|
|
270
|
+
|
|
271
|
+
> The former `release_min_age_days` key was a misleading name for
|
|
272
|
+
> `repo_min_age_days` (it gates *repository* age, not *release* age). It is
|
|
273
|
+
> still accepted as a deprecated alias and emits a warning; prefer
|
|
274
|
+
> `repo_min_age_days`.
|
|
235
275
|
|
|
236
276
|
The per-org `exclude` list removes repositories from analysis entirely; they are
|
|
237
277
|
reported as **excluded** (distinct from "not enabled"), so an intentional
|
|
238
278
|
exclusion is visible rather than silently dropped.
|
|
239
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
|
+
|
|
240
348
|
`slack.channel` is optional. The action's `slack_channel` input (wired to the
|
|
241
349
|
`SLACK_CHANNEL_ID` variable in `reporting.yaml`) overrides it, so the channel
|
|
242
350
|
can live as an org/repo variable rather than in the config JSON. It must be the
|
|
@@ -276,10 +384,10 @@ and the Slack **bot token** is consumed by the workflow, not the CLI.
|
|
|
276
384
|
| `output_dir` | No | — | Directory for Pages output (org mode) |
|
|
277
385
|
| `pages_url` | No | — | Published Pages URL (used in the Slack link) |
|
|
278
386
|
| `slack_channel` | No | — | Slack channel ID; overrides the config `slack.channel` (e.g. the `SLACK_CHANNEL_ID` variable) |
|
|
279
|
-
| `top_n` | No | `10` | Offenders per signal across all outputs (shared default) |
|
|
280
|
-
| `top_n_report` | No | — | Offenders per signal in the GitHub Pages output (overrides `top_n`) |
|
|
281
|
-
| `top_n_cli` | No | — | Offenders per signal in the terminal output (overrides `top_n`) |
|
|
282
|
-
| `top_n_slack` | No | — | Offenders per signal in the Slack digest (overrides `top_n`) |
|
|
387
|
+
| `top_n` | No | `10` | Offenders per signal across all outputs (shared default; `0` = no limit) |
|
|
388
|
+
| `top_n_report` | No | — | Offenders per signal in the GitHub Pages output (`0` = no limit; overrides `top_n`) |
|
|
389
|
+
| `top_n_cli` | No | — | Offenders per signal in the terminal output (`0` = no limit; overrides `top_n`) |
|
|
390
|
+
| `top_n_slack` | No | — | Offenders per signal in the Slack digest (`0` = no limit; overrides `top_n`) |
|
|
283
391
|
| `fail_threshold` | No | `none` | `none`/`low`/`medium`/`high`/`critical`/`any` (repo mode) |
|
|
284
392
|
| `force_notify` | No | `false` | Post to Slack regardless of `report_day` |
|
|
285
393
|
| `tool_version` | No | `0.1.0` | Published PyPI version (ignored on pull requests) |
|
|
@@ -309,6 +417,46 @@ uvx github-security-report report
|
|
|
309
417
|
uvx github-security-report report --org lfreleng-actions
|
|
310
418
|
```
|
|
311
419
|
|
|
420
|
+
## Bulk Remediation Scripts
|
|
421
|
+
|
|
422
|
+
The report ends with **nag lists** — repositories where a supported feature is
|
|
423
|
+
switched off. Where GitHub exposes the relevant toggle through its REST API,
|
|
424
|
+
the [`scripts/`](scripts/) directory ships standalone helpers that clear a whole
|
|
425
|
+
nag list in one pass instead of clicking through each repository's settings.
|
|
426
|
+
They reuse the tool's own scoping rules
|
|
427
|
+
([`src/github_security_report/scope.py`](src/github_security_report/scope.py)),
|
|
428
|
+
so they act on exactly the repositories the report does. See
|
|
429
|
+
[`scripts/README.md`](scripts/README.md) for full details.
|
|
430
|
+
|
|
431
|
+
Each script is a self-contained [PEP 723](https://peps.python.org/pep-0723/)
|
|
432
|
+
program: `uv run` resolves its inline dependencies on the fly — no project
|
|
433
|
+
install required.
|
|
434
|
+
|
|
435
|
+
### `enable_dependabot_security_updates.py`
|
|
436
|
+
|
|
437
|
+
Enables **Dependabot security updates** (and the prerequisite alerts) across an
|
|
438
|
+
organisation, clearing the "Dependabot: Security Updates" nag list. It reads
|
|
439
|
+
the current state of each repository, enables the feature where it is off, and
|
|
440
|
+
verifies the result.
|
|
441
|
+
|
|
442
|
+
```bash
|
|
443
|
+
# An org-admin token is required (classic PAT with repo admin / admin:org).
|
|
444
|
+
source ~/.secrets.github.classic.god # exports $GITHUB_TOKEN
|
|
445
|
+
|
|
446
|
+
# Dry run (default): preview every change, touch nothing.
|
|
447
|
+
uv run scripts/enable_dependabot_security_updates.py \
|
|
448
|
+
--config ~/.config/github-security-report/config.json
|
|
449
|
+
|
|
450
|
+
# Apply: switch the feature on for every in-scope repository.
|
|
451
|
+
uv run scripts/enable_dependabot_security_updates.py \
|
|
452
|
+
--config ~/.config/github-security-report/config.json --apply
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
`--config` reads the organisation name and exclusions straight from the
|
|
456
|
+
reporting tool's JSON config, so the script and the report never drift. The
|
|
457
|
+
operation is **dry-run by default** (these are privileged writes) and reversible
|
|
458
|
+
via `DELETE /repos/{owner}/{repo}/automated-security-fixes`.
|
|
459
|
+
|
|
312
460
|
## Development
|
|
313
461
|
|
|
314
462
|
```bash
|
|
@@ -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
|
|
30
|
-
|
|
31
|
-
- **Unknown** — indeterminate (insufficient permission),
|
|
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
|
|
@@ -42,10 +49,14 @@ tables (org mode):
|
|
|
42
49
|
- **Dependabot** — three tables: repositories with vulnerability **alerts not
|
|
43
50
|
enabled**, repositories with **security updates not enabled**, and ecosystems
|
|
44
51
|
with no update `cooldown` configured (mandatory; any value passes).
|
|
45
|
-
- **Releases / Tagging** — repositories overdue a release or tag, ranked by
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
`
|
|
52
|
+
- **Releases / Tagging** — repositories overdue a release or tag, ranked by
|
|
53
|
+
release/tag staleness (repository age never affects ordering; a repository
|
|
54
|
+
with no release or tag ranks highest). Repositories younger than
|
|
55
|
+
`repo_min_age_days` (default 28; `0` includes all) and those in
|
|
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.
|
|
49
60
|
|
|
50
61
|
## Operating modes
|
|
51
62
|
|
|
@@ -65,11 +76,20 @@ Repo mode needs nothing beyond the workflow's ephemeral `GITHUB_TOKEN`. Org mode
|
|
|
65
76
|
needs a Personal Access Token; choose **one** of the two options below depending
|
|
66
77
|
on how many organisations the report covers.
|
|
67
78
|
|
|
68
|
-
|
|
69
|
-
permitted to make to an "unknown" status rather than reporting a repository
|
|
70
|
-
clean, so an under-scoped token surfaces as unknowns in the report instead of
|
|
79
|
+
Almost all required access is **read-only**. The tool degrades any read it is
|
|
80
|
+
not permitted to make to an "unknown" status rather than reporting a repository
|
|
81
|
+
as clean, so an under-scoped token surfaces as unknowns in the report instead of
|
|
71
82
|
silently wrong results — start minimal and widen if you see unknowns.
|
|
72
83
|
|
|
84
|
+
The **one** exception is organisation-ruleset coverage. GitHub gates the
|
|
85
|
+
org-rulesets endpoint behind an org-admin permission (classic `admin:org` scope,
|
|
86
|
+
or fine-grained Administration **write**), even though the tool only reads it.
|
|
87
|
+
That coverage is **optional**: it detects tools enforced through an org ruleset
|
|
88
|
+
(for example a required-workflow or code-scanning ruleset). Without it that one
|
|
89
|
+
signal is skipped and every other part of the report is unaffected, so the
|
|
90
|
+
minimal tokens below omit it. Grant the org-admin permission only if you want
|
|
91
|
+
ruleset-based tool coverage.
|
|
92
|
+
|
|
73
93
|
### Single organisation — fine-grained PAT
|
|
74
94
|
|
|
75
95
|
A fine-grained PAT is bound to one resource owner, so it works for a report
|
|
@@ -87,14 +107,15 @@ organisation and **Repository access** set to *All repositories*, then grant:
|
|
|
87
107
|
| Secret scanning alerts | Open secret-scanning alerts |
|
|
88
108
|
| Administration | Dependabot enablement + security-updates status, and effective branch rules |
|
|
89
109
|
|
|
90
|
-
**Organization permissions
|
|
110
|
+
**Organization permissions:**
|
|
91
111
|
|
|
92
|
-
| Permission | Used for |
|
|
93
|
-
| ---------- | -------- |
|
|
94
|
-
| Administration |
|
|
112
|
+
| Permission | Access | Used for |
|
|
113
|
+
| ---------- | ------ | -------- |
|
|
114
|
+
| Administration | Read and write | *Optional* — organisation rulesets (detect tools enforced through an org ruleset). GitHub gates this endpoint behind Administration **write**; omit it to keep the token read-only and skip ruleset-based tool coverage. |
|
|
95
115
|
|
|
96
|
-
>
|
|
97
|
-
>
|
|
116
|
+
> Read-only is enough for everything except the optional ruleset coverage
|
|
117
|
+
> above. A fine-grained token cannot span organisations. For a report covering
|
|
118
|
+
> more than one org, use a classic PAT (below).
|
|
98
119
|
|
|
99
120
|
### Multiple organisations — classic PAT
|
|
100
121
|
|
|
@@ -106,7 +127,8 @@ organisations. Grant these scopes:
|
|
|
106
127
|
| ----- | -------- |
|
|
107
128
|
| `repo` | Repository data, including private repositories |
|
|
108
129
|
| `security_events` | Code scanning, secret scanning, and Dependabot alerts (org-bulk and per-repo) |
|
|
109
|
-
| `read:org` | Listing organisation repositories
|
|
130
|
+
| `read:org` | Listing organisation repositories |
|
|
131
|
+
| `admin:org` | *Optional* — reading organisation rulesets for ruleset-based tool coverage. GitHub gates `GET /orgs/{org}/rulesets` behind the full `admin:org` scope; `read:org` and `write:org` return 404. Omit it to skip that one signal; everything else is unaffected. |
|
|
110
132
|
|
|
111
133
|
> For organisations that enforce SSO, the PAT must be **SSO-authorised** for
|
|
112
134
|
> each target organisation, or the org-level endpoints return `403` (reported as
|
|
@@ -163,7 +185,8 @@ environment-variable name, never embedded.
|
|
|
163
185
|
"top_n_slack": 10,
|
|
164
186
|
"include_archived": false,
|
|
165
187
|
"include_test": false,
|
|
166
|
-
"
|
|
188
|
+
"repo_min_age_days": 28,
|
|
189
|
+
"release_max_age_days": 60
|
|
167
190
|
},
|
|
168
191
|
"organizations": [
|
|
169
192
|
{
|
|
@@ -182,17 +205,102 @@ environment-variable name, never embedded.
|
|
|
182
205
|
`top_n` controls how many offenders are shown per signal. It is the shared
|
|
183
206
|
default for all three outputs; set any of `top_n_report` (GitHub Pages),
|
|
184
207
|
`top_n_cli` (terminal), or `top_n_slack` (Slack digest) to override an
|
|
185
|
-
individual output.
|
|
208
|
+
individual output. Set a value to `0` to remove the limit entirely and show
|
|
209
|
+
every offender. Each can also be set at the CLI with `--top-n`,
|
|
186
210
|
`--top-n-report`, `--top-n-cli`, and `--top-n-slack`.
|
|
187
211
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
212
|
+
The Releases / Tagging section has two independent freshness levers:
|
|
213
|
+
|
|
214
|
+
- `report.repo_min_age_days` (default `28`, `0` = include all) is a grace
|
|
215
|
+
period that omits **brand-new repositories** — those *created* within that
|
|
216
|
+
many days — before a release or tag is expected of them. CLI:
|
|
217
|
+
`--repo-min-age-days`.
|
|
218
|
+
- `report.release_max_age_days` (default `60`; `0` = flag everything) is the
|
|
219
|
+
release-staleness threshold: a repository is only flagged when its newest
|
|
220
|
+
release **or** tag is older than that many days (a repository with neither is
|
|
221
|
+
always flagged). Tune it to match your release cadence so actively released
|
|
222
|
+
repositories drop out of the table. CLI: `--release-max-age-days`.
|
|
223
|
+
|
|
224
|
+
The per-org `releases_exclude` (CLI `--releases-exclude`, repeatable) drops
|
|
225
|
+
named repositories from the section entirely.
|
|
226
|
+
|
|
227
|
+
> The former `release_min_age_days` key was a misleading name for
|
|
228
|
+
> `repo_min_age_days` (it gates *repository* age, not *release* age). It is
|
|
229
|
+
> still accepted as a deprecated alias and emits a warning; prefer
|
|
230
|
+
> `repo_min_age_days`.
|
|
191
231
|
|
|
192
232
|
The per-org `exclude` list removes repositories from analysis entirely; they are
|
|
193
233
|
reported as **excluded** (distinct from "not enabled"), so an intentional
|
|
194
234
|
exclusion is visible rather than silently dropped.
|
|
195
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
|
+
|
|
196
304
|
`slack.channel` is optional. The action's `slack_channel` input (wired to the
|
|
197
305
|
`SLACK_CHANNEL_ID` variable in `reporting.yaml`) overrides it, so the channel
|
|
198
306
|
can live as an org/repo variable rather than in the config JSON. It must be the
|
|
@@ -232,10 +340,10 @@ and the Slack **bot token** is consumed by the workflow, not the CLI.
|
|
|
232
340
|
| `output_dir` | No | — | Directory for Pages output (org mode) |
|
|
233
341
|
| `pages_url` | No | — | Published Pages URL (used in the Slack link) |
|
|
234
342
|
| `slack_channel` | No | — | Slack channel ID; overrides the config `slack.channel` (e.g. the `SLACK_CHANNEL_ID` variable) |
|
|
235
|
-
| `top_n` | No | `10` | Offenders per signal across all outputs (shared default) |
|
|
236
|
-
| `top_n_report` | No | — | Offenders per signal in the GitHub Pages output (overrides `top_n`) |
|
|
237
|
-
| `top_n_cli` | No | — | Offenders per signal in the terminal output (overrides `top_n`) |
|
|
238
|
-
| `top_n_slack` | No | — | Offenders per signal in the Slack digest (overrides `top_n`) |
|
|
343
|
+
| `top_n` | No | `10` | Offenders per signal across all outputs (shared default; `0` = no limit) |
|
|
344
|
+
| `top_n_report` | No | — | Offenders per signal in the GitHub Pages output (`0` = no limit; overrides `top_n`) |
|
|
345
|
+
| `top_n_cli` | No | — | Offenders per signal in the terminal output (`0` = no limit; overrides `top_n`) |
|
|
346
|
+
| `top_n_slack` | No | — | Offenders per signal in the Slack digest (`0` = no limit; overrides `top_n`) |
|
|
239
347
|
| `fail_threshold` | No | `none` | `none`/`low`/`medium`/`high`/`critical`/`any` (repo mode) |
|
|
240
348
|
| `force_notify` | No | `false` | Post to Slack regardless of `report_day` |
|
|
241
349
|
| `tool_version` | No | `0.1.0` | Published PyPI version (ignored on pull requests) |
|
|
@@ -265,6 +373,46 @@ uvx github-security-report report
|
|
|
265
373
|
uvx github-security-report report --org lfreleng-actions
|
|
266
374
|
```
|
|
267
375
|
|
|
376
|
+
## Bulk Remediation Scripts
|
|
377
|
+
|
|
378
|
+
The report ends with **nag lists** — repositories where a supported feature is
|
|
379
|
+
switched off. Where GitHub exposes the relevant toggle through its REST API,
|
|
380
|
+
the [`scripts/`](scripts/) directory ships standalone helpers that clear a whole
|
|
381
|
+
nag list in one pass instead of clicking through each repository's settings.
|
|
382
|
+
They reuse the tool's own scoping rules
|
|
383
|
+
([`src/github_security_report/scope.py`](src/github_security_report/scope.py)),
|
|
384
|
+
so they act on exactly the repositories the report does. See
|
|
385
|
+
[`scripts/README.md`](scripts/README.md) for full details.
|
|
386
|
+
|
|
387
|
+
Each script is a self-contained [PEP 723](https://peps.python.org/pep-0723/)
|
|
388
|
+
program: `uv run` resolves its inline dependencies on the fly — no project
|
|
389
|
+
install required.
|
|
390
|
+
|
|
391
|
+
### `enable_dependabot_security_updates.py`
|
|
392
|
+
|
|
393
|
+
Enables **Dependabot security updates** (and the prerequisite alerts) across an
|
|
394
|
+
organisation, clearing the "Dependabot: Security Updates" nag list. It reads
|
|
395
|
+
the current state of each repository, enables the feature where it is off, and
|
|
396
|
+
verifies the result.
|
|
397
|
+
|
|
398
|
+
```bash
|
|
399
|
+
# An org-admin token is required (classic PAT with repo admin / admin:org).
|
|
400
|
+
source ~/.secrets.github.classic.god # exports $GITHUB_TOKEN
|
|
401
|
+
|
|
402
|
+
# Dry run (default): preview every change, touch nothing.
|
|
403
|
+
uv run scripts/enable_dependabot_security_updates.py \
|
|
404
|
+
--config ~/.config/github-security-report/config.json
|
|
405
|
+
|
|
406
|
+
# Apply: switch the feature on for every in-scope repository.
|
|
407
|
+
uv run scripts/enable_dependabot_security_updates.py \
|
|
408
|
+
--config ~/.config/github-security-report/config.json --apply
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
`--config` reads the organisation name and exclusions straight from the
|
|
412
|
+
reporting tool's JSON config, so the script and the report never drift. The
|
|
413
|
+
operation is **dry-run by default** (these are privileged writes) and reversible
|
|
414
|
+
via `DELETE /repos/{owner}/{repo}/automated-security-fixes`.
|
|
415
|
+
|
|
268
416
|
## Development
|
|
269
417
|
|
|
270
418
|
```bash
|
|
@@ -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.
|
|
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.
|
|
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.
|
|
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.
|
|
100
|
+
"ruff==0.15.18",
|
|
101
101
|
"types-jsonschema==4.26.0.20260518",
|
|
102
102
|
"types-PyYAML==6.0.12.20250915",
|
|
103
103
|
]
|
|
@@ -5,6 +5,58 @@
|
|
|
5
5
|
|
|
6
6
|
# Scripts
|
|
7
7
|
|
|
8
|
+
## `enable_dependabot_security_updates.py`
|
|
9
|
+
|
|
10
|
+
Bulk-enables **Dependabot security updates** across an organisation, clearing
|
|
11
|
+
the "Dependabot: Security Updates" nag list the reporting tool produces. It
|
|
12
|
+
drives three REST endpoints per repository:
|
|
13
|
+
|
|
14
|
+
- `PUT /repos/{owner}/{repo}/vulnerability-alerts` — Dependabot *alerts* (the
|
|
15
|
+
prerequisite; idempotent).
|
|
16
|
+
- `PUT /repos/{owner}/{repo}/automated-security-fixes` — Dependabot *security
|
|
17
|
+
updates*.
|
|
18
|
+
- `GET /repos/{owner}/{repo}/automated-security-fixes` — current state, read
|
|
19
|
+
before and after each change.
|
|
20
|
+
|
|
21
|
+
By default the scope matches the reporting tool (it reuses the same fork /
|
|
22
|
+
template / archived / test-name / explicit-exclude rules from
|
|
23
|
+
[`../src/github_security_report/scope.py`](../src/github_security_report/scope.py)),
|
|
24
|
+
and likewise skips empty repositories, which the reporting tool already drops
|
|
25
|
+
at the listing stage. The `--include-empty` flag deliberately widens that scope
|
|
26
|
+
to act on empty repositories the report never includes. Pass `--config` to read
|
|
27
|
+
the org name and exclusions straight from the tool's JSON config so the two
|
|
28
|
+
never drift.
|
|
29
|
+
|
|
30
|
+
It is a self-contained [PEP 723](https://peps.python.org/pep-0723/) script; `uv`
|
|
31
|
+
resolves its inline dependencies (`httpx`, `rich`) on the fly.
|
|
32
|
+
|
|
33
|
+
### Run
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
# An org-admin token is required (e.g. a classic PAT with repo admin /
|
|
37
|
+
# admin:org). The god token export publishes it as $GITHUB_TOKEN:
|
|
38
|
+
source ~/.secrets.github.classic.god
|
|
39
|
+
|
|
40
|
+
# Dry run (default): previews exactly what would change, touches nothing.
|
|
41
|
+
uv run scripts/enable_dependabot_security_updates.py \
|
|
42
|
+
--config ~/.config/github-security-report/config.json
|
|
43
|
+
|
|
44
|
+
# Apply: switch the feature on for every in-scope repository.
|
|
45
|
+
uv run scripts/enable_dependabot_security_updates.py \
|
|
46
|
+
--config ~/.config/github-security-report/config.json --apply
|
|
47
|
+
|
|
48
|
+
# Or drive it from flags, with a different token variable:
|
|
49
|
+
uv run scripts/enable_dependabot_security_updates.py \
|
|
50
|
+
--org lfreleng-actions --token-env SECURITY_REPORT_PAT \
|
|
51
|
+
--exclude project-reporting-artifacts --apply
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
**Dry-run by default.** These are privileged writes, so the script previews
|
|
55
|
+
unless you pass `--apply`. Useful extra flags: `--repo` (operate on named repos
|
|
56
|
+
only, skipping scope), `--limit N`, and `--include-archived` /
|
|
57
|
+
`--include-test` / `--include-empty`. The operation is reversible via
|
|
58
|
+
`DELETE /repos/{owner}/{repo}/automated-security-fixes`.
|
|
59
|
+
|
|
8
60
|
## `phase0_capability_spike.py` (throwaway)
|
|
9
61
|
|
|
10
62
|
A self-contained [PEP 723](https://peps.python.org/pep-0723/) spike that
|