github-security-report 0.1.0__tar.gz → 0.2.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.2.0}/PKG-INFO +98 -26
- {github_security_report-0.1.0 → github_security_report-0.2.0}/README.md +95 -23
- {github_security_report-0.1.0 → github_security_report-0.2.0}/pyproject.toml +4 -4
- {github_security_report-0.1.0 → github_security_report-0.2.0}/scripts/README.md +52 -0
- {github_security_report-0.1.0 → github_security_report-0.2.0}/src/github_security_report/_version.py +2 -2
- {github_security_report-0.1.0 → github_security_report-0.2.0}/src/github_security_report/cli.py +38 -19
- {github_security_report-0.1.0 → github_security_report-0.2.0}/src/github_security_report/client.py +189 -68
- {github_security_report-0.1.0 → github_security_report-0.2.0}/src/github_security_report/collect.py +132 -60
- {github_security_report-0.1.0 → github_security_report-0.2.0}/src/github_security_report/config.py +46 -8
- {github_security_report-0.1.0 → github_security_report-0.2.0}/src/github_security_report/models.py +41 -0
- github_security_report-0.2.0/src/github_security_report/posture.py +446 -0
- {github_security_report-0.1.0 → github_security_report-0.2.0}/src/github_security_report/render/html.py +16 -0
- {github_security_report-0.1.0 → github_security_report-0.2.0}/src/github_security_report/render/markdown.py +37 -0
- {github_security_report-0.1.0 → github_security_report-0.2.0}/src/github_security_report/render/slack.py +49 -5
- {github_security_report-0.1.0 → github_security_report-0.2.0}/src/github_security_report/render/terminal.py +22 -19
- {github_security_report-0.1.0 → github_security_report-0.2.0}/src/github_security_report/report.py +46 -5
- {github_security_report-0.1.0 → github_security_report-0.2.0}/src/github_security_report/templates/report.html.j2 +35 -2
- github_security_report-0.1.0/src/github_security_report/posture.py +0 -264
- {github_security_report-0.1.0 → github_security_report-0.2.0}/.gitignore +0 -0
- {github_security_report-0.1.0 → github_security_report-0.2.0}/LICENSE +0 -0
- {github_security_report-0.1.0 → github_security_report-0.2.0}/LICENSES/Apache-2.0.txt +0 -0
- {github_security_report-0.1.0 → github_security_report-0.2.0}/src/github_security_report/__init__.py +0 -0
- {github_security_report-0.1.0 → github_security_report-0.2.0}/src/github_security_report/classify.py +0 -0
- {github_security_report-0.1.0 → github_security_report-0.2.0}/src/github_security_report/gitctx.py +0 -0
- {github_security_report-0.1.0 → github_security_report-0.2.0}/src/github_security_report/py.typed +0 -0
- {github_security_report-0.1.0 → github_security_report-0.2.0}/src/github_security_report/render/__init__.py +0 -0
- {github_security_report-0.1.0 → github_security_report-0.2.0}/src/github_security_report/rulesets.py +0 -0
- {github_security_report-0.1.0 → github_security_report-0.2.0}/src/github_security_report/runner.py +0 -0
- {github_security_report-0.1.0 → github_security_report-0.2.0}/src/github_security_report/scope.py +0 -0
- {github_security_report-0.1.0 → github_security_report-0.2.0}/src/github_security_report/severity.py +0 -0
- {github_security_report-0.1.0 → github_security_report-0.2.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.2.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.0
|
|
37
|
+
Requires-Dist: pytest==9.1.0; extra == 'dev'
|
|
38
38
|
Requires-Dist: respx==0.23.1; extra == 'dev'
|
|
39
|
-
Requires-Dist: ruff==0.15.
|
|
39
|
+
Requires-Dist: ruff==0.15.17; 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'
|
|
@@ -86,10 +86,13 @@ tables (org mode):
|
|
|
86
86
|
- **Dependabot** — three tables: repositories with vulnerability **alerts not
|
|
87
87
|
enabled**, repositories with **security updates not enabled**, and ecosystems
|
|
88
88
|
with no update `cooldown` configured (mandatory; any value passes).
|
|
89
|
-
- **Releases / Tagging** — repositories overdue a release or tag, ranked by
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
`
|
|
89
|
+
- **Releases / Tagging** — repositories overdue a release or tag, ranked by
|
|
90
|
+
release/tag staleness (repository age never affects ordering; a repository
|
|
91
|
+
with no release or tag ranks highest). Repositories younger than
|
|
92
|
+
`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).
|
|
93
96
|
|
|
94
97
|
## Operating modes
|
|
95
98
|
|
|
@@ -109,11 +112,20 @@ Repo mode needs nothing beyond the workflow's ephemeral `GITHUB_TOKEN`. Org mode
|
|
|
109
112
|
needs a Personal Access Token; choose **one** of the two options below depending
|
|
110
113
|
on how many organisations the report covers.
|
|
111
114
|
|
|
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
|
|
115
|
+
Almost all required access is **read-only**. The tool degrades any read it is
|
|
116
|
+
not permitted to make to an "unknown" status rather than reporting a repository
|
|
117
|
+
as clean, so an under-scoped token surfaces as unknowns in the report instead of
|
|
115
118
|
silently wrong results — start minimal and widen if you see unknowns.
|
|
116
119
|
|
|
120
|
+
The **one** exception is organisation-ruleset coverage. GitHub gates the
|
|
121
|
+
org-rulesets endpoint behind an org-admin permission (classic `admin:org` scope,
|
|
122
|
+
or fine-grained Administration **write**), even though the tool only reads it.
|
|
123
|
+
That coverage is **optional**: it detects tools enforced through an org ruleset
|
|
124
|
+
(for example a required-workflow or code-scanning ruleset). Without it that one
|
|
125
|
+
signal is skipped and every other part of the report is unaffected, so the
|
|
126
|
+
minimal tokens below omit it. Grant the org-admin permission only if you want
|
|
127
|
+
ruleset-based tool coverage.
|
|
128
|
+
|
|
117
129
|
### Single organisation — fine-grained PAT
|
|
118
130
|
|
|
119
131
|
A fine-grained PAT is bound to one resource owner, so it works for a report
|
|
@@ -131,14 +143,15 @@ organisation and **Repository access** set to *All repositories*, then grant:
|
|
|
131
143
|
| Secret scanning alerts | Open secret-scanning alerts |
|
|
132
144
|
| Administration | Dependabot enablement + security-updates status, and effective branch rules |
|
|
133
145
|
|
|
134
|
-
**Organization permissions
|
|
146
|
+
**Organization permissions:**
|
|
135
147
|
|
|
136
|
-
| Permission | Used for |
|
|
137
|
-
| ---------- | -------- |
|
|
138
|
-
| Administration |
|
|
148
|
+
| Permission | Access | Used for |
|
|
149
|
+
| ---------- | ------ | -------- |
|
|
150
|
+
| 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
151
|
|
|
140
|
-
>
|
|
141
|
-
>
|
|
152
|
+
> Read-only is enough for everything except the optional ruleset coverage
|
|
153
|
+
> above. A fine-grained token cannot span organisations. For a report covering
|
|
154
|
+
> more than one org, use a classic PAT (below).
|
|
142
155
|
|
|
143
156
|
### Multiple organisations — classic PAT
|
|
144
157
|
|
|
@@ -150,7 +163,8 @@ organisations. Grant these scopes:
|
|
|
150
163
|
| ----- | -------- |
|
|
151
164
|
| `repo` | Repository data, including private repositories |
|
|
152
165
|
| `security_events` | Code scanning, secret scanning, and Dependabot alerts (org-bulk and per-repo) |
|
|
153
|
-
| `read:org` | Listing organisation repositories
|
|
166
|
+
| `read:org` | Listing organisation repositories |
|
|
167
|
+
| `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
168
|
|
|
155
169
|
> For organisations that enforce SSO, the PAT must be **SSO-authorised** for
|
|
156
170
|
> each target organisation, or the org-level endpoints return `403` (reported as
|
|
@@ -207,7 +221,8 @@ environment-variable name, never embedded.
|
|
|
207
221
|
"top_n_slack": 10,
|
|
208
222
|
"include_archived": false,
|
|
209
223
|
"include_test": false,
|
|
210
|
-
"
|
|
224
|
+
"repo_min_age_days": 28,
|
|
225
|
+
"release_max_age_days": 0
|
|
211
226
|
},
|
|
212
227
|
"organizations": [
|
|
213
228
|
{
|
|
@@ -226,12 +241,29 @@ environment-variable name, never embedded.
|
|
|
226
241
|
`top_n` controls how many offenders are shown per signal. It is the shared
|
|
227
242
|
default for all three outputs; set any of `top_n_report` (GitHub Pages),
|
|
228
243
|
`top_n_cli` (terminal), or `top_n_slack` (Slack digest) to override an
|
|
229
|
-
individual output.
|
|
244
|
+
individual output. Set a value to `0` to remove the limit entirely and show
|
|
245
|
+
every offender. Each can also be set at the CLI with `--top-n`,
|
|
230
246
|
`--top-n-report`, `--top-n-cli`, and `--top-n-slack`.
|
|
231
247
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
248
|
+
The Releases / Tagging section has two independent freshness levers:
|
|
249
|
+
|
|
250
|
+
- `report.repo_min_age_days` (default `28`, `0` = include all) is a grace
|
|
251
|
+
period that omits **brand-new repositories** — those *created* within that
|
|
252
|
+
many days — before a release or tag is expected of them. CLI:
|
|
253
|
+
`--repo-min-age-days`.
|
|
254
|
+
- `report.release_max_age_days` (default `0` = flag everything) is the
|
|
255
|
+
release-staleness threshold: a repository is only flagged when its newest
|
|
256
|
+
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
|
|
258
|
+
repositories drop out of the table. CLI: `--release-max-age-days`.
|
|
259
|
+
|
|
260
|
+
The per-org `releases_exclude` (CLI `--releases-exclude`, repeatable) drops
|
|
261
|
+
named repositories from the section entirely.
|
|
262
|
+
|
|
263
|
+
> The former `release_min_age_days` key was a misleading name for
|
|
264
|
+
> `repo_min_age_days` (it gates *repository* age, not *release* age). It is
|
|
265
|
+
> still accepted as a deprecated alias and emits a warning; prefer
|
|
266
|
+
> `repo_min_age_days`.
|
|
235
267
|
|
|
236
268
|
The per-org `exclude` list removes repositories from analysis entirely; they are
|
|
237
269
|
reported as **excluded** (distinct from "not enabled"), so an intentional
|
|
@@ -276,10 +308,10 @@ and the Slack **bot token** is consumed by the workflow, not the CLI.
|
|
|
276
308
|
| `output_dir` | No | — | Directory for Pages output (org mode) |
|
|
277
309
|
| `pages_url` | No | — | Published Pages URL (used in the Slack link) |
|
|
278
310
|
| `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`) |
|
|
311
|
+
| `top_n` | No | `10` | Offenders per signal across all outputs (shared default; `0` = no limit) |
|
|
312
|
+
| `top_n_report` | No | — | Offenders per signal in the GitHub Pages output (`0` = no limit; overrides `top_n`) |
|
|
313
|
+
| `top_n_cli` | No | — | Offenders per signal in the terminal output (`0` = no limit; overrides `top_n`) |
|
|
314
|
+
| `top_n_slack` | No | — | Offenders per signal in the Slack digest (`0` = no limit; overrides `top_n`) |
|
|
283
315
|
| `fail_threshold` | No | `none` | `none`/`low`/`medium`/`high`/`critical`/`any` (repo mode) |
|
|
284
316
|
| `force_notify` | No | `false` | Post to Slack regardless of `report_day` |
|
|
285
317
|
| `tool_version` | No | `0.1.0` | Published PyPI version (ignored on pull requests) |
|
|
@@ -309,6 +341,46 @@ uvx github-security-report report
|
|
|
309
341
|
uvx github-security-report report --org lfreleng-actions
|
|
310
342
|
```
|
|
311
343
|
|
|
344
|
+
## Bulk Remediation Scripts
|
|
345
|
+
|
|
346
|
+
The report ends with **nag lists** — repositories where a supported feature is
|
|
347
|
+
switched off. Where GitHub exposes the relevant toggle through its REST API,
|
|
348
|
+
the [`scripts/`](scripts/) directory ships standalone helpers that clear a whole
|
|
349
|
+
nag list in one pass instead of clicking through each repository's settings.
|
|
350
|
+
They reuse the tool's own scoping rules
|
|
351
|
+
([`src/github_security_report/scope.py`](src/github_security_report/scope.py)),
|
|
352
|
+
so they act on exactly the repositories the report does. See
|
|
353
|
+
[`scripts/README.md`](scripts/README.md) for full details.
|
|
354
|
+
|
|
355
|
+
Each script is a self-contained [PEP 723](https://peps.python.org/pep-0723/)
|
|
356
|
+
program: `uv run` resolves its inline dependencies on the fly — no project
|
|
357
|
+
install required.
|
|
358
|
+
|
|
359
|
+
### `enable_dependabot_security_updates.py`
|
|
360
|
+
|
|
361
|
+
Enables **Dependabot security updates** (and the prerequisite alerts) across an
|
|
362
|
+
organisation, clearing the "Dependabot: Security Updates" nag list. It reads
|
|
363
|
+
the current state of each repository, enables the feature where it is off, and
|
|
364
|
+
verifies the result.
|
|
365
|
+
|
|
366
|
+
```bash
|
|
367
|
+
# An org-admin token is required (classic PAT with repo admin / admin:org).
|
|
368
|
+
source ~/.secrets.github.classic.god # exports $GITHUB_TOKEN
|
|
369
|
+
|
|
370
|
+
# Dry run (default): preview every change, touch nothing.
|
|
371
|
+
uv run scripts/enable_dependabot_security_updates.py \
|
|
372
|
+
--config ~/.config/github-security-report/config.json
|
|
373
|
+
|
|
374
|
+
# Apply: switch the feature on for every in-scope repository.
|
|
375
|
+
uv run scripts/enable_dependabot_security_updates.py \
|
|
376
|
+
--config ~/.config/github-security-report/config.json --apply
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
`--config` reads the organisation name and exclusions straight from the
|
|
380
|
+
reporting tool's JSON config, so the script and the report never drift. The
|
|
381
|
+
operation is **dry-run by default** (these are privileged writes) and reversible
|
|
382
|
+
via `DELETE /repos/{owner}/{repo}/automated-security-fixes`.
|
|
383
|
+
|
|
312
384
|
## Development
|
|
313
385
|
|
|
314
386
|
```bash
|
|
@@ -42,10 +42,13 @@ tables (org mode):
|
|
|
42
42
|
- **Dependabot** — three tables: repositories with vulnerability **alerts not
|
|
43
43
|
enabled**, repositories with **security updates not enabled**, and ecosystems
|
|
44
44
|
with no update `cooldown` configured (mandatory; any value passes).
|
|
45
|
-
- **Releases / Tagging** — repositories overdue a release or tag, ranked by
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
`
|
|
45
|
+
- **Releases / Tagging** — repositories overdue a release or tag, ranked by
|
|
46
|
+
release/tag staleness (repository age never affects ordering; a repository
|
|
47
|
+
with no release or tag ranks highest). Repositories younger than
|
|
48
|
+
`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).
|
|
49
52
|
|
|
50
53
|
## Operating modes
|
|
51
54
|
|
|
@@ -65,11 +68,20 @@ Repo mode needs nothing beyond the workflow's ephemeral `GITHUB_TOKEN`. Org mode
|
|
|
65
68
|
needs a Personal Access Token; choose **one** of the two options below depending
|
|
66
69
|
on how many organisations the report covers.
|
|
67
70
|
|
|
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
|
|
71
|
+
Almost all required access is **read-only**. The tool degrades any read it is
|
|
72
|
+
not permitted to make to an "unknown" status rather than reporting a repository
|
|
73
|
+
as clean, so an under-scoped token surfaces as unknowns in the report instead of
|
|
71
74
|
silently wrong results — start minimal and widen if you see unknowns.
|
|
72
75
|
|
|
76
|
+
The **one** exception is organisation-ruleset coverage. GitHub gates the
|
|
77
|
+
org-rulesets endpoint behind an org-admin permission (classic `admin:org` scope,
|
|
78
|
+
or fine-grained Administration **write**), even though the tool only reads it.
|
|
79
|
+
That coverage is **optional**: it detects tools enforced through an org ruleset
|
|
80
|
+
(for example a required-workflow or code-scanning ruleset). Without it that one
|
|
81
|
+
signal is skipped and every other part of the report is unaffected, so the
|
|
82
|
+
minimal tokens below omit it. Grant the org-admin permission only if you want
|
|
83
|
+
ruleset-based tool coverage.
|
|
84
|
+
|
|
73
85
|
### Single organisation — fine-grained PAT
|
|
74
86
|
|
|
75
87
|
A fine-grained PAT is bound to one resource owner, so it works for a report
|
|
@@ -87,14 +99,15 @@ organisation and **Repository access** set to *All repositories*, then grant:
|
|
|
87
99
|
| Secret scanning alerts | Open secret-scanning alerts |
|
|
88
100
|
| Administration | Dependabot enablement + security-updates status, and effective branch rules |
|
|
89
101
|
|
|
90
|
-
**Organization permissions
|
|
102
|
+
**Organization permissions:**
|
|
91
103
|
|
|
92
|
-
| Permission | Used for |
|
|
93
|
-
| ---------- | -------- |
|
|
94
|
-
| Administration |
|
|
104
|
+
| Permission | Access | Used for |
|
|
105
|
+
| ---------- | ------ | -------- |
|
|
106
|
+
| 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
107
|
|
|
96
|
-
>
|
|
97
|
-
>
|
|
108
|
+
> Read-only is enough for everything except the optional ruleset coverage
|
|
109
|
+
> above. A fine-grained token cannot span organisations. For a report covering
|
|
110
|
+
> more than one org, use a classic PAT (below).
|
|
98
111
|
|
|
99
112
|
### Multiple organisations — classic PAT
|
|
100
113
|
|
|
@@ -106,7 +119,8 @@ organisations. Grant these scopes:
|
|
|
106
119
|
| ----- | -------- |
|
|
107
120
|
| `repo` | Repository data, including private repositories |
|
|
108
121
|
| `security_events` | Code scanning, secret scanning, and Dependabot alerts (org-bulk and per-repo) |
|
|
109
|
-
| `read:org` | Listing organisation repositories
|
|
122
|
+
| `read:org` | Listing organisation repositories |
|
|
123
|
+
| `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
124
|
|
|
111
125
|
> For organisations that enforce SSO, the PAT must be **SSO-authorised** for
|
|
112
126
|
> each target organisation, or the org-level endpoints return `403` (reported as
|
|
@@ -163,7 +177,8 @@ environment-variable name, never embedded.
|
|
|
163
177
|
"top_n_slack": 10,
|
|
164
178
|
"include_archived": false,
|
|
165
179
|
"include_test": false,
|
|
166
|
-
"
|
|
180
|
+
"repo_min_age_days": 28,
|
|
181
|
+
"release_max_age_days": 0
|
|
167
182
|
},
|
|
168
183
|
"organizations": [
|
|
169
184
|
{
|
|
@@ -182,12 +197,29 @@ environment-variable name, never embedded.
|
|
|
182
197
|
`top_n` controls how many offenders are shown per signal. It is the shared
|
|
183
198
|
default for all three outputs; set any of `top_n_report` (GitHub Pages),
|
|
184
199
|
`top_n_cli` (terminal), or `top_n_slack` (Slack digest) to override an
|
|
185
|
-
individual output.
|
|
200
|
+
individual output. Set a value to `0` to remove the limit entirely and show
|
|
201
|
+
every offender. Each can also be set at the CLI with `--top-n`,
|
|
186
202
|
`--top-n-report`, `--top-n-cli`, and `--top-n-slack`.
|
|
187
203
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
204
|
+
The Releases / Tagging section has two independent freshness levers:
|
|
205
|
+
|
|
206
|
+
- `report.repo_min_age_days` (default `28`, `0` = include all) is a grace
|
|
207
|
+
period that omits **brand-new repositories** — those *created* within that
|
|
208
|
+
many days — before a release or tag is expected of them. CLI:
|
|
209
|
+
`--repo-min-age-days`.
|
|
210
|
+
- `report.release_max_age_days` (default `0` = flag everything) is the
|
|
211
|
+
release-staleness threshold: a repository is only flagged when its newest
|
|
212
|
+
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
|
|
214
|
+
repositories drop out of the table. CLI: `--release-max-age-days`.
|
|
215
|
+
|
|
216
|
+
The per-org `releases_exclude` (CLI `--releases-exclude`, repeatable) drops
|
|
217
|
+
named repositories from the section entirely.
|
|
218
|
+
|
|
219
|
+
> The former `release_min_age_days` key was a misleading name for
|
|
220
|
+
> `repo_min_age_days` (it gates *repository* age, not *release* age). It is
|
|
221
|
+
> still accepted as a deprecated alias and emits a warning; prefer
|
|
222
|
+
> `repo_min_age_days`.
|
|
191
223
|
|
|
192
224
|
The per-org `exclude` list removes repositories from analysis entirely; they are
|
|
193
225
|
reported as **excluded** (distinct from "not enabled"), so an intentional
|
|
@@ -232,10 +264,10 @@ and the Slack **bot token** is consumed by the workflow, not the CLI.
|
|
|
232
264
|
| `output_dir` | No | — | Directory for Pages output (org mode) |
|
|
233
265
|
| `pages_url` | No | — | Published Pages URL (used in the Slack link) |
|
|
234
266
|
| `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`) |
|
|
267
|
+
| `top_n` | No | `10` | Offenders per signal across all outputs (shared default; `0` = no limit) |
|
|
268
|
+
| `top_n_report` | No | — | Offenders per signal in the GitHub Pages output (`0` = no limit; overrides `top_n`) |
|
|
269
|
+
| `top_n_cli` | No | — | Offenders per signal in the terminal output (`0` = no limit; overrides `top_n`) |
|
|
270
|
+
| `top_n_slack` | No | — | Offenders per signal in the Slack digest (`0` = no limit; overrides `top_n`) |
|
|
239
271
|
| `fail_threshold` | No | `none` | `none`/`low`/`medium`/`high`/`critical`/`any` (repo mode) |
|
|
240
272
|
| `force_notify` | No | `false` | Post to Slack regardless of `report_day` |
|
|
241
273
|
| `tool_version` | No | `0.1.0` | Published PyPI version (ignored on pull requests) |
|
|
@@ -265,6 +297,46 @@ uvx github-security-report report
|
|
|
265
297
|
uvx github-security-report report --org lfreleng-actions
|
|
266
298
|
```
|
|
267
299
|
|
|
300
|
+
## Bulk Remediation Scripts
|
|
301
|
+
|
|
302
|
+
The report ends with **nag lists** — repositories where a supported feature is
|
|
303
|
+
switched off. Where GitHub exposes the relevant toggle through its REST API,
|
|
304
|
+
the [`scripts/`](scripts/) directory ships standalone helpers that clear a whole
|
|
305
|
+
nag list in one pass instead of clicking through each repository's settings.
|
|
306
|
+
They reuse the tool's own scoping rules
|
|
307
|
+
([`src/github_security_report/scope.py`](src/github_security_report/scope.py)),
|
|
308
|
+
so they act on exactly the repositories the report does. See
|
|
309
|
+
[`scripts/README.md`](scripts/README.md) for full details.
|
|
310
|
+
|
|
311
|
+
Each script is a self-contained [PEP 723](https://peps.python.org/pep-0723/)
|
|
312
|
+
program: `uv run` resolves its inline dependencies on the fly — no project
|
|
313
|
+
install required.
|
|
314
|
+
|
|
315
|
+
### `enable_dependabot_security_updates.py`
|
|
316
|
+
|
|
317
|
+
Enables **Dependabot security updates** (and the prerequisite alerts) across an
|
|
318
|
+
organisation, clearing the "Dependabot: Security Updates" nag list. It reads
|
|
319
|
+
the current state of each repository, enables the feature where it is off, and
|
|
320
|
+
verifies the result.
|
|
321
|
+
|
|
322
|
+
```bash
|
|
323
|
+
# An org-admin token is required (classic PAT with repo admin / admin:org).
|
|
324
|
+
source ~/.secrets.github.classic.god # exports $GITHUB_TOKEN
|
|
325
|
+
|
|
326
|
+
# Dry run (default): preview every change, touch nothing.
|
|
327
|
+
uv run scripts/enable_dependabot_security_updates.py \
|
|
328
|
+
--config ~/.config/github-security-report/config.json
|
|
329
|
+
|
|
330
|
+
# Apply: switch the feature on for every in-scope repository.
|
|
331
|
+
uv run scripts/enable_dependabot_security_updates.py \
|
|
332
|
+
--config ~/.config/github-security-report/config.json --apply
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
`--config` reads the organisation name and exclusions straight from the
|
|
336
|
+
reporting tool's JSON config, so the script and the report never drift. The
|
|
337
|
+
operation is **dry-run by default** (these are privileged writes) and reversible
|
|
338
|
+
via `DELETE /repos/{owner}/{repo}/automated-security-fixes`.
|
|
339
|
+
|
|
268
340
|
## Development
|
|
269
341
|
|
|
270
342
|
```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.0
|
|
62
|
+
"pytest==9.1.0",
|
|
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.17",
|
|
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.0
|
|
94
|
+
"pytest==9.1.0",
|
|
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.17",
|
|
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
|
{github_security_report-0.1.0 → github_security_report-0.2.0}/src/github_security_report/_version.py
RENAMED
|
@@ -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.
|
|
22
|
-
__version_tuple__ = version_tuple = (0,
|
|
21
|
+
__version__ = version = '0.2.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 2, 0)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
{github_security_report-0.1.0 → github_security_report-0.2.0}/src/github_security_report/cli.py
RENAMED
|
@@ -192,7 +192,8 @@ def _load_config(
|
|
|
192
192
|
async def _run_org(cfg: Config, *, console: Console, output_dir: Path | None,
|
|
193
193
|
pages_url: str | None, top_n: int | None, force_notify: bool,
|
|
194
194
|
slack_channel: str | None = None,
|
|
195
|
-
|
|
195
|
+
repo_min_age_days: int | None = None,
|
|
196
|
+
release_max_age_days: int | None = None,
|
|
196
197
|
releases_exclude: tuple[str, ...] | None = None,
|
|
197
198
|
top_n_report: int | None = None,
|
|
198
199
|
top_n_cli: int | None = None,
|
|
@@ -206,8 +207,12 @@ async def _run_org(cfg: Config, *, console: Console, output_dir: Path | None,
|
|
|
206
207
|
return 2
|
|
207
208
|
# CLI overrides win over config for the Releases/Tagging controls.
|
|
208
209
|
report_cfg = org_cfg.report
|
|
209
|
-
if
|
|
210
|
-
report_cfg = replace(report_cfg,
|
|
210
|
+
if repo_min_age_days is not None:
|
|
211
|
+
report_cfg = replace(report_cfg, repo_min_age_days=repo_min_age_days)
|
|
212
|
+
if release_max_age_days is not None:
|
|
213
|
+
report_cfg = replace(
|
|
214
|
+
report_cfg, release_max_age_days=release_max_age_days
|
|
215
|
+
)
|
|
211
216
|
effective_cfg = org_cfg
|
|
212
217
|
if releases_exclude is not None:
|
|
213
218
|
effective_cfg = replace(org_cfg, releases_exclude=releases_exclude)
|
|
@@ -226,6 +231,15 @@ async def _run_org(cfg: Config, *, console: Console, output_dir: Path | None,
|
|
|
226
231
|
return top_n
|
|
227
232
|
return int(getattr(org_cfg.report, attr))
|
|
228
233
|
|
|
234
|
+
def _most_generous(limits: list[int]) -> int:
|
|
235
|
+
# 0 means "no limit", so it is the most generous value of all; otherwise
|
|
236
|
+
# the largest positive cap wins. Without this, max() would treat 0 as
|
|
237
|
+
# the smallest limit and silently re-impose a cap on an org that asked
|
|
238
|
+
# for everything when it shares a channel with a capped org.
|
|
239
|
+
if any(limit <= 0 for limit in limits):
|
|
240
|
+
return 0
|
|
241
|
+
return max(limits)
|
|
242
|
+
|
|
229
243
|
for org_cfg, org_report in pairs:
|
|
230
244
|
term_render.render_org(
|
|
231
245
|
org_report, console, top_n=_limit(org_cfg, top_n_cli, "cli_top_n")
|
|
@@ -276,8 +290,8 @@ async def _run_org(cfg: Config, *, console: Console, output_dir: Path | None,
|
|
|
276
290
|
slack_render.render_payload(
|
|
277
291
|
[report for _, report in items],
|
|
278
292
|
channel=channel,
|
|
279
|
-
top_n=
|
|
280
|
-
_limit(oc, top_n_slack, "slack_top_n") for oc, _ in items
|
|
293
|
+
top_n=_most_generous(
|
|
294
|
+
[_limit(oc, top_n_slack, "slack_top_n") for oc, _ in items]
|
|
281
295
|
),
|
|
282
296
|
pages_url=pages_url,
|
|
283
297
|
)
|
|
@@ -358,13 +372,14 @@ def report(
|
|
|
358
372
|
output_dir: str | None = typer.Option(None, "--output-dir", "-o", help="Directory for Pages output (org mode)."),
|
|
359
373
|
pages_url: str | None = typer.Option(None, "--pages-url", help="GitHub Pages URL for the Slack link."),
|
|
360
374
|
slack_channel: str | None = typer.Option(None, "--slack-channel", help="Slack channel ID; overrides config slack.channel (e.g. SLACK_CHANNEL_ID)."),
|
|
361
|
-
top_n: int | None = typer.Option(None, "--top-n", help="Offenders shown per signal across all outputs (default: config, else 10). Overridden per output by the flags below."),
|
|
362
|
-
top_n_report: int | None = typer.Option(None, "--top-n-report", help="Offenders per signal in the GitHub Pages output (overrides --top-n)."),
|
|
363
|
-
top_n_cli: int | None = typer.Option(None, "--top-n-cli", help="Offenders per signal in the terminal output (overrides --top-n)."),
|
|
364
|
-
top_n_slack: int | None = typer.Option(None, "--top-n-slack", help="Offenders per signal in the Slack digest (overrides --top-n)."),
|
|
375
|
+
top_n: int | None = typer.Option(None, "--top-n", help="Offenders shown per signal across all outputs (0 = no limit; default: config, else 10). Overridden per output by the flags below."),
|
|
376
|
+
top_n_report: int | None = typer.Option(None, "--top-n-report", help="Offenders per signal in the GitHub Pages output (0 = no limit; overrides --top-n)."),
|
|
377
|
+
top_n_cli: int | None = typer.Option(None, "--top-n-cli", help="Offenders per signal in the terminal output (0 = no limit; overrides --top-n)."),
|
|
378
|
+
top_n_slack: int | None = typer.Option(None, "--top-n-slack", help="Offenders per signal in the Slack digest (0 = no limit; overrides --top-n)."),
|
|
365
379
|
fail_threshold: str = typer.Option("none", "--fail-threshold", help="none|low|medium|high|critical|any (repo mode)."),
|
|
366
380
|
force_notify: bool = typer.Option(False, "--force-notify", help="Post to Slack regardless of report_day."),
|
|
367
|
-
|
|
381
|
+
repo_min_age_days: int | None = typer.Option(None, "--repo-min-age-days", "--release-min-age-days", help="Exclude repos created within N days from Releases/Tagging (0 = include all; default: config, else 28). --release-min-age-days is a deprecated alias."),
|
|
382
|
+
release_max_age_days: int | None = typer.Option(None, "--release-max-age-days", help="Flag a repo in Releases/Tagging only when its newest release or tag is older than N days (0 = flag every eligible repo; default: config, else 0)."),
|
|
368
383
|
releases_exclude: list[str] | None = typer.Option(None, "--releases-exclude", help="Repository name to omit from the Releases/Tagging table (repeatable; overrides config)."),
|
|
369
384
|
no_color: bool = typer.Option(False, "--no-color", help="Disable coloured output."),
|
|
370
385
|
) -> None:
|
|
@@ -372,22 +387,25 @@ def report(
|
|
|
372
387
|
plain = no_color or bool(os.environ.get("CI")) or not sys.stdout.isatty()
|
|
373
388
|
console = Console(no_color=plain, highlight=False)
|
|
374
389
|
|
|
375
|
-
# Match the config schema (top_n minimum is
|
|
376
|
-
#
|
|
390
|
+
# Match the config schema (top_n minimum is 0): reject a negative override
|
|
391
|
+
# at the boundary. 0 is permitted and disables the limit (show everything).
|
|
377
392
|
for name, value in (
|
|
378
393
|
("--top-n", top_n),
|
|
379
394
|
("--top-n-report", top_n_report),
|
|
380
395
|
("--top-n-cli", top_n_cli),
|
|
381
396
|
("--top-n-slack", top_n_slack),
|
|
382
397
|
):
|
|
383
|
-
if value is not None and value <
|
|
384
|
-
console.print(f"[red]{name} must be
|
|
398
|
+
if value is not None and value < 0:
|
|
399
|
+
console.print(f"[red]{name} must be 0 or greater (0 = no limit)[/red]")
|
|
385
400
|
raise typer.Exit(2)
|
|
386
401
|
|
|
387
|
-
# Match the config schema (
|
|
388
|
-
#
|
|
389
|
-
if
|
|
390
|
-
console.print("[red]--
|
|
402
|
+
# Match the config schema (minimum is 0): reject negative overrides at the
|
|
403
|
+
# boundary.
|
|
404
|
+
if repo_min_age_days is not None and repo_min_age_days < 0:
|
|
405
|
+
console.print("[red]--repo-min-age-days must be 0 or greater[/red]")
|
|
406
|
+
raise typer.Exit(2)
|
|
407
|
+
if release_max_age_days is not None and release_max_age_days < 0:
|
|
408
|
+
console.print("[red]--release-max-age-days must be 0 or greater[/red]")
|
|
391
409
|
raise typer.Exit(2)
|
|
392
410
|
|
|
393
411
|
cfg = _load_config(config_file, config_data, org, token_env, console=console)
|
|
@@ -421,7 +439,8 @@ def report(
|
|
|
421
439
|
output_dir=Path(output_dir) if output_dir else None,
|
|
422
440
|
pages_url=pages_url, top_n=top_n, force_notify=force_notify,
|
|
423
441
|
slack_channel=slack_channel or None,
|
|
424
|
-
|
|
442
|
+
repo_min_age_days=repo_min_age_days,
|
|
443
|
+
release_max_age_days=release_max_age_days,
|
|
425
444
|
releases_exclude=tuple(releases_exclude) if releases_exclude else None,
|
|
426
445
|
top_n_report=top_n_report,
|
|
427
446
|
top_n_cli=top_n_cli,
|