github-security-report 0.7.0__tar.gz → 0.7.1__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.7.0 → github_security_report-0.7.1}/PKG-INFO +12 -3
- {github_security_report-0.7.0 → github_security_report-0.7.1}/README.md +10 -1
- {github_security_report-0.7.0 → github_security_report-0.7.1}/pyproject.toml +2 -2
- {github_security_report-0.7.0 → github_security_report-0.7.1}/src/github_security_report/_version.py +2 -2
- {github_security_report-0.7.0 → github_security_report-0.7.1}/src/github_security_report/categories.py +1 -2
- {github_security_report-0.7.0 → github_security_report-0.7.1}/src/github_security_report/classify.py +65 -19
- {github_security_report-0.7.0 → github_security_report-0.7.1}/src/github_security_report/cli.py +164 -59
- {github_security_report-0.7.0 → github_security_report-0.7.1}/src/github_security_report/client.py +82 -50
- {github_security_report-0.7.0 → github_security_report-0.7.1}/src/github_security_report/collect.py +34 -29
- {github_security_report-0.7.0 → github_security_report-0.7.1}/src/github_security_report/config.py +9 -14
- {github_security_report-0.7.0 → github_security_report-0.7.1}/src/github_security_report/gating.py +26 -7
- {github_security_report-0.7.0 → github_security_report-0.7.1}/src/github_security_report/models.py +2 -6
- {github_security_report-0.7.0 → github_security_report-0.7.1}/src/github_security_report/posture.py +1 -4
- {github_security_report-0.7.0 → github_security_report-0.7.1}/src/github_security_report/remediate.py +3 -9
- {github_security_report-0.7.0 → github_security_report-0.7.1}/src/github_security_report/render/html.py +8 -7
- {github_security_report-0.7.0 → github_security_report-0.7.1}/src/github_security_report/render/markdown.py +22 -13
- {github_security_report-0.7.0 → github_security_report-0.7.1}/src/github_security_report/render/slack.py +21 -11
- {github_security_report-0.7.0 → github_security_report-0.7.1}/src/github_security_report/render/terminal.py +17 -17
- {github_security_report-0.7.0 → github_security_report-0.7.1}/src/github_security_report/report.py +2 -7
- {github_security_report-0.7.0 → github_security_report-0.7.1}/src/github_security_report/rulesets.py +2 -5
- {github_security_report-0.7.0 → github_security_report-0.7.1}/.gitignore +0 -0
- {github_security_report-0.7.0 → github_security_report-0.7.1}/LICENSE +0 -0
- {github_security_report-0.7.0 → github_security_report-0.7.1}/LICENSES/Apache-2.0.txt +0 -0
- {github_security_report-0.7.0 → github_security_report-0.7.1}/scripts/README.md +0 -0
- {github_security_report-0.7.0 → github_security_report-0.7.1}/src/github_security_report/__init__.py +0 -0
- {github_security_report-0.7.0 → github_security_report-0.7.1}/src/github_security_report/gitctx.py +0 -0
- {github_security_report-0.7.0 → github_security_report-0.7.1}/src/github_security_report/py.typed +0 -0
- {github_security_report-0.7.0 → github_security_report-0.7.1}/src/github_security_report/render/__init__.py +0 -0
- {github_security_report-0.7.0 → github_security_report-0.7.1}/src/github_security_report/runner.py +0 -0
- {github_security_report-0.7.0 → github_security_report-0.7.1}/src/github_security_report/scope.py +0 -0
- {github_security_report-0.7.0 → github_security_report-0.7.1}/src/github_security_report/severity.py +0 -0
- {github_security_report-0.7.0 → github_security_report-0.7.1}/src/github_security_report/templates/index.html.j2 +0 -0
- {github_security_report-0.7.0 → github_security_report-0.7.1}/src/github_security_report/templates/report.html.j2 +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: github-security-report
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.1
|
|
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
|
|
@@ -37,7 +37,7 @@ Requires-Dist: pytest-cov==7.1.0; extra == 'dev'
|
|
|
37
37
|
Requires-Dist: pytest==9.1.1; extra == 'dev'
|
|
38
38
|
Requires-Dist: respx==0.23.1; extra == 'dev'
|
|
39
39
|
Requires-Dist: ruff==0.15.20; extra == 'dev'
|
|
40
|
-
Requires-Dist: syrupy==5.
|
|
40
|
+
Requires-Dist: syrupy==5.5.1; extra == 'dev'
|
|
41
41
|
Requires-Dist: types-jsonschema==4.26.0.20260518; extra == 'dev'
|
|
42
42
|
Requires-Dist: types-pyyaml==6.0.12.20250915; extra == 'dev'
|
|
43
43
|
Description-Content-Type: text/markdown
|
|
@@ -460,13 +460,22 @@ The tool is published to PyPI and runs with `uvx`. Inside a Git checkout with a
|
|
|
460
460
|
`upstream` remote, then `origin`) and prints a Rich table report:
|
|
461
461
|
|
|
462
462
|
```bash
|
|
463
|
-
export GITHUB_TOKEN=
|
|
463
|
+
export GITHUB_TOKEN="your-token"
|
|
464
464
|
uvx github-security-report report
|
|
465
465
|
|
|
466
466
|
# Or org mode locally with a PAT:
|
|
467
467
|
uvx github-security-report report --org lfreleng-actions
|
|
468
468
|
```
|
|
469
469
|
|
|
470
|
+
### GitHub Enterprise Server
|
|
471
|
+
|
|
472
|
+
The API endpoints honour the standard environment variables that GitHub
|
|
473
|
+
Actions exports, so the tool works against GitHub Enterprise Server
|
|
474
|
+
without code changes: set `GITHUB_API_URL` and `GITHUB_GRAPHQL_URL` to
|
|
475
|
+
your enterprise endpoints (Actions sets these automatically on GHES
|
|
476
|
+
runners). `SCORECARD_API_URL` overrides the external OpenSSF Scorecard
|
|
477
|
+
API in the same way.
|
|
478
|
+
|
|
470
479
|
## Remediation
|
|
471
480
|
|
|
472
481
|
The `remediate` subcommand is the in-tool counterpart to the report: it runs the
|
|
@@ -416,13 +416,22 @@ The tool is published to PyPI and runs with `uvx`. Inside a Git checkout with a
|
|
|
416
416
|
`upstream` remote, then `origin`) and prints a Rich table report:
|
|
417
417
|
|
|
418
418
|
```bash
|
|
419
|
-
export GITHUB_TOKEN=
|
|
419
|
+
export GITHUB_TOKEN="your-token"
|
|
420
420
|
uvx github-security-report report
|
|
421
421
|
|
|
422
422
|
# Or org mode locally with a PAT:
|
|
423
423
|
uvx github-security-report report --org lfreleng-actions
|
|
424
424
|
```
|
|
425
425
|
|
|
426
|
+
### GitHub Enterprise Server
|
|
427
|
+
|
|
428
|
+
The API endpoints honour the standard environment variables that GitHub
|
|
429
|
+
Actions exports, so the tool works against GitHub Enterprise Server
|
|
430
|
+
without code changes: set `GITHUB_API_URL` and `GITHUB_GRAPHQL_URL` to
|
|
431
|
+
your enterprise endpoints (Actions sets these automatically on GHES
|
|
432
|
+
runners). `SCORECARD_API_URL` overrides the external OpenSSF Scorecard
|
|
433
|
+
API in the same way.
|
|
434
|
+
|
|
426
435
|
## Remediation
|
|
427
436
|
|
|
428
437
|
The `remediate` subcommand is the in-tool counterpart to the report: it runs the
|
|
@@ -63,7 +63,7 @@ dev = [
|
|
|
63
63
|
"pytest-asyncio==1.4.0",
|
|
64
64
|
"pytest-cov==7.1.0",
|
|
65
65
|
"respx==0.23.1",
|
|
66
|
-
"syrupy==5.
|
|
66
|
+
"syrupy==5.5.1",
|
|
67
67
|
"mypy==2.1.0",
|
|
68
68
|
"ruff==0.15.20",
|
|
69
69
|
"types-jsonschema==4.26.0.20260518",
|
|
@@ -95,7 +95,7 @@ dev = [
|
|
|
95
95
|
"pytest-asyncio==1.4.0",
|
|
96
96
|
"pytest-cov==7.1.0",
|
|
97
97
|
"respx==0.23.1",
|
|
98
|
-
"syrupy==5.
|
|
98
|
+
"syrupy==5.5.1",
|
|
99
99
|
"mypy==2.1.0",
|
|
100
100
|
"ruff==0.15.20",
|
|
101
101
|
"types-jsonschema==4.26.0.20260518",
|
{github_security_report-0.7.0 → github_security_report-0.7.1}/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.7.
|
|
22
|
-
__version_tuple__ = version_tuple = (0, 7,
|
|
21
|
+
__version__ = version = '0.7.1'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 7, 1)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -118,8 +118,7 @@ _CATEGORIES: dict[CategoryKey, CategoryMeta] = {
|
|
|
118
118
|
fail_label=None,
|
|
119
119
|
url="https://github.com/scanaislop/aislop",
|
|
120
120
|
description=(
|
|
121
|
-
"aislop AI-slop / code-quality findings, ranked worst-first by "
|
|
122
|
-
"severity."
|
|
121
|
+
"aislop AI-slop / code-quality findings, ranked worst-first by severity."
|
|
123
122
|
),
|
|
124
123
|
# aislop, like zizmor, populates only the SARIF level axis
|
|
125
124
|
# (error/warning/note); "note" normalises to LOW (see severity.py), so
|
{github_security_report-0.7.0 → github_security_report-0.7.1}/src/github_security_report/classify.py
RENAMED
|
@@ -37,9 +37,7 @@ from github_security_report.severity import Severity
|
|
|
37
37
|
FailSeverities = Mapping[SignalType, Severity]
|
|
38
38
|
|
|
39
39
|
|
|
40
|
-
def _cutoff(
|
|
41
|
-
signal: SignalType, fail_severities: FailSeverities | None
|
|
42
|
-
) -> Severity:
|
|
40
|
+
def _cutoff(signal: SignalType, fail_severities: FailSeverities | None) -> Severity:
|
|
43
41
|
"""The fail-severity cutoff for ``signal`` (config override or default)."""
|
|
44
42
|
if fail_severities is not None and signal in fail_severities:
|
|
45
43
|
return fail_severities[signal]
|
|
@@ -131,13 +129,19 @@ def _code_scanning_tool_signal(
|
|
|
131
129
|
facts.code_scanning_status == 200 and tool_name in facts.code_scanning_tools
|
|
132
130
|
)
|
|
133
131
|
if not enabled:
|
|
134
|
-
detail =
|
|
132
|
+
detail = (
|
|
133
|
+
"code scanning disabled"
|
|
134
|
+
if facts.code_scanning_status == 404
|
|
135
|
+
else f"{tool_name} not enabled"
|
|
136
|
+
)
|
|
135
137
|
return RepoSignal(repo, signal, RepoState.NAG, detail=detail)
|
|
136
138
|
counts = count_code_scanning(facts.code_scanning_alerts, tool_name)
|
|
137
139
|
# Enabled with no findings is only "clean" when the alert read succeeded;
|
|
138
140
|
# an unreadable sweep (e.g. org-bulk 403/5xx) must not masquerade as clean.
|
|
139
141
|
if counts.total == 0 and facts.code_scanning_alerts_status != 200:
|
|
140
|
-
return RepoSignal(
|
|
142
|
+
return RepoSignal(
|
|
143
|
+
repo, signal, RepoState.UNKNOWN, detail="alert data unavailable"
|
|
144
|
+
)
|
|
141
145
|
# Only findings at or above the category's fail-severity cutoff make the
|
|
142
146
|
# repo an offender; sub-threshold findings (e.g. informational) fold into
|
|
143
147
|
# the clean count.
|
|
@@ -201,21 +205,34 @@ def classify_scorecard(
|
|
|
201
205
|
# the code-scanning cutoff logic used elsewhere in this file.
|
|
202
206
|
clean = facts.scorecard_score >= 10.0 and not counts.at_or_above(cutoff)
|
|
203
207
|
state = RepoState.CLEAN if clean else RepoState.OFFENDER
|
|
204
|
-
return RepoSignal(
|
|
208
|
+
return RepoSignal(
|
|
209
|
+
repo,
|
|
210
|
+
SignalType.SCORECARD,
|
|
211
|
+
state,
|
|
212
|
+
counts=counts,
|
|
213
|
+
score=facts.scorecard_score,
|
|
214
|
+
)
|
|
205
215
|
if has_cs:
|
|
206
216
|
if counts.total == 0 and facts.code_scanning_alerts_status != 200:
|
|
207
|
-
return RepoSignal(
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
217
|
+
return RepoSignal(
|
|
218
|
+
repo,
|
|
219
|
+
SignalType.SCORECARD,
|
|
220
|
+
RepoState.UNKNOWN,
|
|
221
|
+
detail="alert data unavailable",
|
|
222
|
+
)
|
|
223
|
+
state = RepoState.OFFENDER if counts.at_or_above(cutoff) else RepoState.CLEAN
|
|
211
224
|
return RepoSignal(repo, SignalType.SCORECARD, state, counts=counts)
|
|
212
225
|
# An indeterminate external request (a 403 forbidden/blocked, a 5xx
|
|
213
226
|
# including the synthetic 503 a transport failure produces, or a forbidden
|
|
214
227
|
# code-scanning probe) is unknown, not a definitive nag. Only a clean 404
|
|
215
228
|
# with no code-scanning Scorecard data means "no results".
|
|
216
229
|
if facts.code_scanning_status == 403 or facts.scorecard_status not in (200, 404):
|
|
217
|
-
return RepoSignal(
|
|
218
|
-
|
|
230
|
+
return RepoSignal(
|
|
231
|
+
repo, SignalType.SCORECARD, RepoState.UNKNOWN, detail="indeterminate"
|
|
232
|
+
)
|
|
233
|
+
return RepoSignal(
|
|
234
|
+
repo, SignalType.SCORECARD, RepoState.NAG, detail="no Scorecard results"
|
|
235
|
+
)
|
|
219
236
|
|
|
220
237
|
|
|
221
238
|
def classify_secret_scanning(
|
|
@@ -226,9 +243,19 @@ def classify_secret_scanning(
|
|
|
226
243
|
# other classifiers (they are dispatched positionally) and is unused here.
|
|
227
244
|
repo = facts.repo
|
|
228
245
|
if facts.secret_scanning_status == 403:
|
|
229
|
-
return RepoSignal(
|
|
246
|
+
return RepoSignal(
|
|
247
|
+
repo,
|
|
248
|
+
SignalType.SECRET_SCANNING,
|
|
249
|
+
RepoState.UNKNOWN,
|
|
250
|
+
detail="insufficient permission",
|
|
251
|
+
)
|
|
230
252
|
if facts.secret_scanning_status == 404:
|
|
231
|
-
return RepoSignal(
|
|
253
|
+
return RepoSignal(
|
|
254
|
+
repo,
|
|
255
|
+
SignalType.SECRET_SCANNING,
|
|
256
|
+
RepoState.NAG,
|
|
257
|
+
detail="secret scanning disabled",
|
|
258
|
+
)
|
|
232
259
|
# Flat open count; stored as HIGH so ranking (more == worse) works and the
|
|
233
260
|
# repo-mode gate treats leaked secrets as serious, without conflating them
|
|
234
261
|
# with CRITICAL code findings (which would make --fail-threshold critical
|
|
@@ -238,12 +265,19 @@ def classify_secret_scanning(
|
|
|
238
265
|
# Positive evidence of leaked secrets is actionable even when the read was
|
|
239
266
|
# incomplete (a later page failed) or the enablement probe was indeterminate.
|
|
240
267
|
if facts.secret_scanning_open:
|
|
241
|
-
return RepoSignal(
|
|
268
|
+
return RepoSignal(
|
|
269
|
+
repo, SignalType.SECRET_SCANNING, RepoState.OFFENDER, counts=counts
|
|
270
|
+
)
|
|
242
271
|
# A zero count is only "clean" when both the enablement probe and the
|
|
243
272
|
# open-count read succeeded; an indeterminate status (5xx) or an unreadable
|
|
244
273
|
# sweep cannot confirm the repo is clean.
|
|
245
274
|
if facts.secret_scanning_status != 200 or facts.secret_scanning_open_status != 200:
|
|
246
|
-
return RepoSignal(
|
|
275
|
+
return RepoSignal(
|
|
276
|
+
repo,
|
|
277
|
+
SignalType.SECRET_SCANNING,
|
|
278
|
+
RepoState.UNKNOWN,
|
|
279
|
+
detail="alert data unavailable",
|
|
280
|
+
)
|
|
247
281
|
return RepoSignal(repo, SignalType.SECRET_SCANNING, RepoState.CLEAN, counts=counts)
|
|
248
282
|
|
|
249
283
|
|
|
@@ -252,13 +286,25 @@ def classify_dependabot(
|
|
|
252
286
|
) -> RepoSignal:
|
|
253
287
|
repo = facts.repo
|
|
254
288
|
if facts.dependabot_enabled is None:
|
|
255
|
-
return RepoSignal(
|
|
289
|
+
return RepoSignal(
|
|
290
|
+
repo, SignalType.DEPENDABOT, RepoState.UNKNOWN, detail="indeterminate"
|
|
291
|
+
)
|
|
256
292
|
if facts.dependabot_enabled is False:
|
|
257
|
-
return RepoSignal(
|
|
293
|
+
return RepoSignal(
|
|
294
|
+
repo,
|
|
295
|
+
SignalType.DEPENDABOT,
|
|
296
|
+
RepoState.NAG,
|
|
297
|
+
detail="Dependabot alerts disabled",
|
|
298
|
+
)
|
|
258
299
|
counts = count_dependabot(facts.dependabot_alerts)
|
|
259
300
|
# Enabled with no alerts is only "clean" when the alert read succeeded.
|
|
260
301
|
if counts.total == 0 and facts.dependabot_alerts_status != 200:
|
|
261
|
-
return RepoSignal(
|
|
302
|
+
return RepoSignal(
|
|
303
|
+
repo,
|
|
304
|
+
SignalType.DEPENDABOT,
|
|
305
|
+
RepoState.UNKNOWN,
|
|
306
|
+
detail="alert data unavailable",
|
|
307
|
+
)
|
|
262
308
|
failing = counts.at_or_above(_cutoff(SignalType.DEPENDABOT, fail_severities))
|
|
263
309
|
state = RepoState.OFFENDER if failing else RepoState.CLEAN
|
|
264
310
|
return RepoSignal(repo, SignalType.DEPENDABOT, state, counts=counts)
|
{github_security_report-0.7.0 → github_security_report-0.7.1}/src/github_security_report/cli.py
RENAMED
|
@@ -59,7 +59,10 @@ def _version_callback(value: bool) -> None:
|
|
|
59
59
|
@app.callback()
|
|
60
60
|
def main(
|
|
61
61
|
_version: bool = typer.Option(
|
|
62
|
-
False,
|
|
62
|
+
False,
|
|
63
|
+
"--version",
|
|
64
|
+
callback=_version_callback,
|
|
65
|
+
is_eager=True,
|
|
63
66
|
help="Show the version and exit.",
|
|
64
67
|
),
|
|
65
68
|
verbose: bool = typer.Option(False, "--verbose", "-v", help="Verbose logging."),
|
|
@@ -176,9 +179,7 @@ def _slack_show(
|
|
|
176
179
|
when any contributing org would show it on Slack -- mirroring the
|
|
177
180
|
most-generous ``top_n`` rule for the same grouping.
|
|
178
181
|
"""
|
|
179
|
-
return lambda key: any(
|
|
180
|
-
oc.report.shows_category(key, "slack") for oc, _ in items
|
|
181
|
-
)
|
|
182
|
+
return lambda key: any(oc.report.shows_category(key, "slack") for oc, _ in items)
|
|
182
183
|
|
|
183
184
|
|
|
184
185
|
def _write_org_files(
|
|
@@ -192,15 +193,11 @@ def _write_org_files(
|
|
|
192
193
|
org_dir = output_dir / slug
|
|
193
194
|
org_dir.mkdir(parents=True, exist_ok=True)
|
|
194
195
|
(org_dir / "report.md").write_text(
|
|
195
|
-
md_render.render_org(
|
|
196
|
-
org, top_n=top_n, show=_show(report_cfg, "markdown")
|
|
197
|
-
),
|
|
196
|
+
md_render.render_org(org, top_n=top_n, show=_show(report_cfg, "markdown")),
|
|
198
197
|
encoding="utf-8",
|
|
199
198
|
)
|
|
200
199
|
(org_dir / "report.html").write_text(
|
|
201
|
-
html_render.render_org_html(
|
|
202
|
-
org, top_n=top_n, show=_show(report_cfg, "html")
|
|
203
|
-
),
|
|
200
|
+
html_render.render_org_html(org, top_n=top_n, show=_show(report_cfg, "html")),
|
|
204
201
|
encoding="utf-8",
|
|
205
202
|
)
|
|
206
203
|
# report.json is the complete machine-readable dataset, so the per-output
|
|
@@ -255,36 +252,48 @@ def _abort_network(console: Console, exc: NetworkError) -> NoReturn:
|
|
|
255
252
|
raise typer.Exit(3)
|
|
256
253
|
|
|
257
254
|
|
|
258
|
-
async def _run_org(
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
255
|
+
async def _run_org(
|
|
256
|
+
cfg: Config,
|
|
257
|
+
*,
|
|
258
|
+
console: Console,
|
|
259
|
+
output_dir: Path | None,
|
|
260
|
+
pages_url: str | None,
|
|
261
|
+
top_n: int | None,
|
|
262
|
+
force_notify: bool,
|
|
263
|
+
slack_channel: str | None = None,
|
|
264
|
+
repo_min_age_days: int | None = None,
|
|
265
|
+
release_max_age_days: int | None = None,
|
|
266
|
+
releases_exclude: tuple[str, ...] | None = None,
|
|
267
|
+
top_n_report: int | None = None,
|
|
268
|
+
top_n_cli: int | None = None,
|
|
269
|
+
top_n_slack: int | None = None,
|
|
270
|
+
) -> int:
|
|
267
271
|
now = dt.datetime.now(dt.timezone.utc)
|
|
268
272
|
pairs: list[tuple[OrgConfig, OrgReport]] = []
|
|
269
273
|
for org_cfg in cfg.organizations:
|
|
270
274
|
token = config.resolve_token(org_cfg)
|
|
271
275
|
if not token:
|
|
272
|
-
console.print(
|
|
276
|
+
console.print(
|
|
277
|
+
f"[red]No token in ${org_cfg.token_env} for {org_cfg.name}[/red]"
|
|
278
|
+
)
|
|
273
279
|
return 2
|
|
274
280
|
# CLI overrides win over config for the Releases/Tagging controls.
|
|
275
281
|
report_cfg = org_cfg.report
|
|
276
282
|
if repo_min_age_days is not None:
|
|
277
283
|
report_cfg = replace(report_cfg, repo_min_age_days=repo_min_age_days)
|
|
278
284
|
if release_max_age_days is not None:
|
|
279
|
-
report_cfg = replace(
|
|
280
|
-
report_cfg, release_max_age_days=release_max_age_days
|
|
281
|
-
)
|
|
285
|
+
report_cfg = replace(report_cfg, release_max_age_days=release_max_age_days)
|
|
282
286
|
effective_cfg = org_cfg
|
|
283
287
|
if releases_exclude is not None:
|
|
284
288
|
effective_cfg = replace(org_cfg, releases_exclude=releases_exclude)
|
|
285
289
|
async with GitHubClient(token) as client:
|
|
286
290
|
pairs.append(
|
|
287
|
-
(
|
|
291
|
+
(
|
|
292
|
+
org_cfg,
|
|
293
|
+
await collect.collect_org(
|
|
294
|
+
client, effective_cfg, report_cfg, generated_at=now
|
|
295
|
+
),
|
|
296
|
+
)
|
|
288
297
|
)
|
|
289
298
|
org_reports = [report for _, report in pairs]
|
|
290
299
|
|
|
@@ -376,7 +385,10 @@ async def _run_org(cfg: Config, *, console: Console, output_dir: Path | None,
|
|
|
376
385
|
outputs["slack_payload"] = json.dumps(payloads[0])
|
|
377
386
|
if output_dir:
|
|
378
387
|
for payload in payloads:
|
|
379
|
-
dest =
|
|
388
|
+
dest = (
|
|
389
|
+
output_dir
|
|
390
|
+
/ f"slack-payload-{_safe_component(payload['channel'])}.json"
|
|
391
|
+
)
|
|
380
392
|
dest.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8")
|
|
381
393
|
runner.write_github_output(outputs)
|
|
382
394
|
# The job summary mirrors the GitHub Pages Markdown, so it uses the report
|
|
@@ -396,9 +408,15 @@ async def _run_org(cfg: Config, *, console: Console, output_dir: Path | None,
|
|
|
396
408
|
return 0
|
|
397
409
|
|
|
398
410
|
|
|
399
|
-
async def _run_repo(
|
|
400
|
-
|
|
401
|
-
|
|
411
|
+
async def _run_repo(
|
|
412
|
+
owner: str,
|
|
413
|
+
repo_name: str,
|
|
414
|
+
*,
|
|
415
|
+
token_env: str,
|
|
416
|
+
console: Console,
|
|
417
|
+
fail_threshold: str,
|
|
418
|
+
ruleset_workflows: Mapping[str, str] | None = None,
|
|
419
|
+
) -> int:
|
|
402
420
|
token = os.environ.get(token_env, "").strip()
|
|
403
421
|
if not token:
|
|
404
422
|
console.print(f"[red]No token in ${token_env}[/red]")
|
|
@@ -411,7 +429,9 @@ async def _run_repo(owner: str, repo_name: str, *, token_env: str, console: Cons
|
|
|
411
429
|
return 2
|
|
412
430
|
|
|
413
431
|
now = dt.datetime.now(dt.timezone.utc)
|
|
414
|
-
org = build_org_report(
|
|
432
|
+
org = build_org_report(
|
|
433
|
+
f"{owner}/{repo_name}", signals, repo_count=1, generated_at=now
|
|
434
|
+
)
|
|
415
435
|
term_render.render_org(org, console)
|
|
416
436
|
|
|
417
437
|
runner.append_step_summary(md_render.render_org(org))
|
|
@@ -466,7 +486,9 @@ async def _run_remediate(
|
|
|
466
486
|
|
|
467
487
|
def _repo_outputs(signals: list[RepoSignal], fail_threshold: str) -> dict[str, str]:
|
|
468
488
|
outputs = {s.signal.value + "_open": str(s.counts.total) for s in signals}
|
|
469
|
-
outputs["failed"] =
|
|
489
|
+
outputs["failed"] = (
|
|
490
|
+
"true" if runner.should_fail(signals, fail_threshold) else "false"
|
|
491
|
+
)
|
|
470
492
|
return outputs
|
|
471
493
|
|
|
472
494
|
|
|
@@ -475,24 +497,77 @@ def _repo_outputs(signals: list[RepoSignal], fail_threshold: str) -> dict[str, s
|
|
|
475
497
|
# --------------------------------------------------------------------------- #
|
|
476
498
|
@app.command()
|
|
477
499
|
def report(
|
|
478
|
-
config_file: str | None = typer.Option(
|
|
479
|
-
|
|
480
|
-
|
|
500
|
+
config_file: str | None = typer.Option(
|
|
501
|
+
None, "--config", "-c", help="Path to a JSON config file."
|
|
502
|
+
),
|
|
503
|
+
config_data: str | None = typer.Option(
|
|
504
|
+
None, "--config-data", help="Raw or base64 JSON config (vars/secrets)."
|
|
505
|
+
),
|
|
506
|
+
org: str | None = typer.Option(
|
|
507
|
+
None, "--org", help="Single organisation (shorthand for org mode)."
|
|
508
|
+
),
|
|
481
509
|
scope: str = typer.Option("auto", "--scope", help="auto | org | repo."),
|
|
482
|
-
repo: str | None = typer.Option(
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
510
|
+
repo: str | None = typer.Option(
|
|
511
|
+
None, "--repo", help="owner/name for repo mode (else git-detected)."
|
|
512
|
+
),
|
|
513
|
+
token_env: str = typer.Option(
|
|
514
|
+
"GITHUB_TOKEN", "--token-env", help="Env var holding the repo-mode token."
|
|
515
|
+
),
|
|
516
|
+
output_dir: str | None = typer.Option(
|
|
517
|
+
None, "--output-dir", "-o", help="Directory for Pages output (org mode)."
|
|
518
|
+
),
|
|
519
|
+
pages_url: str | None = typer.Option(
|
|
520
|
+
None, "--pages-url", help="GitHub Pages URL for the Slack link."
|
|
521
|
+
),
|
|
522
|
+
slack_channel: str | None = typer.Option(
|
|
523
|
+
None,
|
|
524
|
+
"--slack-channel",
|
|
525
|
+
help="Slack channel ID; overrides config slack.channel (e.g. SLACK_CHANNEL_ID).",
|
|
526
|
+
),
|
|
527
|
+
top_n: int | None = typer.Option(
|
|
528
|
+
None,
|
|
529
|
+
"--top-n",
|
|
530
|
+
help="Offenders shown per signal across all outputs (0 = no limit; default: config, else 10). Overridden per output by the flags below.",
|
|
531
|
+
),
|
|
532
|
+
top_n_report: int | None = typer.Option(
|
|
533
|
+
None,
|
|
534
|
+
"--top-n-report",
|
|
535
|
+
help="Offenders per signal in the GitHub Pages output (0 = no limit; overrides --top-n).",
|
|
536
|
+
),
|
|
537
|
+
top_n_cli: int | None = typer.Option(
|
|
538
|
+
None,
|
|
539
|
+
"--top-n-cli",
|
|
540
|
+
help="Offenders per signal in the terminal output (0 = no limit; overrides --top-n).",
|
|
541
|
+
),
|
|
542
|
+
top_n_slack: int | None = typer.Option(
|
|
543
|
+
None,
|
|
544
|
+
"--top-n-slack",
|
|
545
|
+
help="Offenders per signal in the Slack digest (0 = no limit; overrides --top-n).",
|
|
546
|
+
),
|
|
547
|
+
fail_threshold: str = typer.Option(
|
|
548
|
+
"none",
|
|
549
|
+
"--fail-threshold",
|
|
550
|
+
help="none|low|medium|high|critical|any (repo mode).",
|
|
551
|
+
),
|
|
552
|
+
force_notify: bool = typer.Option(
|
|
553
|
+
False, "--force-notify", help="Post to Slack regardless of report_day."
|
|
554
|
+
),
|
|
555
|
+
repo_min_age_days: int | None = typer.Option(
|
|
556
|
+
None,
|
|
557
|
+
"--repo-min-age-days",
|
|
558
|
+
"--release-min-age-days",
|
|
559
|
+
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.",
|
|
560
|
+
),
|
|
561
|
+
release_max_age_days: int | None = typer.Option(
|
|
562
|
+
None,
|
|
563
|
+
"--release-max-age-days",
|
|
564
|
+
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 60).",
|
|
565
|
+
),
|
|
566
|
+
releases_exclude: list[str] | None = typer.Option(
|
|
567
|
+
None,
|
|
568
|
+
"--releases-exclude",
|
|
569
|
+
help="Repository name to omit from the Releases/Tagging table (repeatable; overrides config).",
|
|
570
|
+
),
|
|
496
571
|
no_color: bool = typer.Option(False, "--no-color", help="Disable coloured output."),
|
|
497
572
|
) -> None:
|
|
498
573
|
"""Generate a security and quality report."""
|
|
@@ -548,13 +623,18 @@ def report(
|
|
|
548
623
|
try:
|
|
549
624
|
code = asyncio.run(
|
|
550
625
|
_run_org(
|
|
551
|
-
cfg,
|
|
626
|
+
cfg,
|
|
627
|
+
console=console,
|
|
552
628
|
output_dir=Path(output_dir) if output_dir else None,
|
|
553
|
-
pages_url=pages_url,
|
|
629
|
+
pages_url=pages_url,
|
|
630
|
+
top_n=top_n,
|
|
631
|
+
force_notify=force_notify,
|
|
554
632
|
slack_channel=slack_channel or None,
|
|
555
633
|
repo_min_age_days=repo_min_age_days,
|
|
556
634
|
release_max_age_days=release_max_age_days,
|
|
557
|
-
releases_exclude=tuple(releases_exclude)
|
|
635
|
+
releases_exclude=tuple(releases_exclude)
|
|
636
|
+
if releases_exclude
|
|
637
|
+
else None,
|
|
558
638
|
top_n_report=top_n_report,
|
|
559
639
|
top_n_cli=top_n_cli,
|
|
560
640
|
top_n_slack=top_n_slack,
|
|
@@ -571,8 +651,11 @@ def report(
|
|
|
571
651
|
try:
|
|
572
652
|
code = asyncio.run(
|
|
573
653
|
_run_repo(
|
|
574
|
-
detected[0],
|
|
575
|
-
|
|
654
|
+
detected[0],
|
|
655
|
+
detected[1],
|
|
656
|
+
token_env=token_env,
|
|
657
|
+
console=console,
|
|
658
|
+
fail_threshold=fail_threshold,
|
|
576
659
|
ruleset_workflows=rw,
|
|
577
660
|
)
|
|
578
661
|
)
|
|
@@ -583,13 +666,35 @@ def report(
|
|
|
583
666
|
|
|
584
667
|
@app.command()
|
|
585
668
|
def remediate(
|
|
586
|
-
config_file: str | None = typer.Option(
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
669
|
+
config_file: str | None = typer.Option(
|
|
670
|
+
None, "--config", "-c", help="Path to a JSON config file."
|
|
671
|
+
),
|
|
672
|
+
config_data: str | None = typer.Option(
|
|
673
|
+
None, "--config-data", help="Raw or base64 JSON config (vars/secrets)."
|
|
674
|
+
),
|
|
675
|
+
org: str | None = typer.Option(
|
|
676
|
+
None, "--org", help="Single organisation (shorthand for org mode)."
|
|
677
|
+
),
|
|
678
|
+
scope: str = typer.Option(
|
|
679
|
+
"org",
|
|
680
|
+
"--scope",
|
|
681
|
+
help="Only 'org' is supported; remediation is organisation-scoped.",
|
|
682
|
+
),
|
|
683
|
+
category: list[str] | None = typer.Option(
|
|
684
|
+
None,
|
|
685
|
+
"--category",
|
|
686
|
+
help="Remediable category to act on (repeatable; default: all). One of: codeql, secret_scanning, dependabot_alerts_enabled, dependabot_updates_enabled, private_vulnerability_reporting.",
|
|
687
|
+
),
|
|
688
|
+
token_env: str = typer.Option(
|
|
689
|
+
"GITHUB_TOKEN",
|
|
690
|
+
"--token-env",
|
|
691
|
+
help="Env var holding a WRITE-capable org-admin PAT. Used for both reading posture and enabling features across every configured org.",
|
|
692
|
+
),
|
|
693
|
+
apply: bool = typer.Option(
|
|
694
|
+
False,
|
|
695
|
+
"--apply",
|
|
696
|
+
help="Perform the writes. Without this flag remediate only previews (dry run).",
|
|
697
|
+
),
|
|
593
698
|
no_color: bool = typer.Option(False, "--no-color", help="Disable coloured output."),
|
|
594
699
|
) -> None:
|
|
595
700
|
"""Enable security features on repositories that lack them.
|