github-security-report 0.6.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.6.0 → github_security_report-0.7.1}/PKG-INFO +67 -20
- {github_security_report-0.6.0 → github_security_report-0.7.1}/README.md +65 -18
- {github_security_report-0.6.0 → github_security_report-0.7.1}/pyproject.toml +2 -2
- {github_security_report-0.6.0 → github_security_report-0.7.1}/src/github_security_report/_version.py +2 -2
- {github_security_report-0.6.0 → github_security_report-0.7.1}/src/github_security_report/categories.py +15 -0
- {github_security_report-0.6.0 → github_security_report-0.7.1}/src/github_security_report/classify.py +121 -38
- {github_security_report-0.6.0 → github_security_report-0.7.1}/src/github_security_report/cli.py +167 -59
- {github_security_report-0.6.0 → github_security_report-0.7.1}/src/github_security_report/client.py +144 -78
- {github_security_report-0.6.0 → github_security_report-0.7.1}/src/github_security_report/collect.py +87 -27
- {github_security_report-0.6.0 → github_security_report-0.7.1}/src/github_security_report/config.py +25 -15
- github_security_report-0.7.1/src/github_security_report/gating.py +208 -0
- {github_security_report-0.6.0 → github_security_report-0.7.1}/src/github_security_report/models.py +20 -8
- {github_security_report-0.6.0 → github_security_report-0.7.1}/src/github_security_report/posture.py +1 -4
- {github_security_report-0.6.0 → github_security_report-0.7.1}/src/github_security_report/remediate.py +3 -9
- {github_security_report-0.6.0 → github_security_report-0.7.1}/src/github_security_report/render/html.py +28 -8
- {github_security_report-0.6.0 → github_security_report-0.7.1}/src/github_security_report/render/markdown.py +33 -13
- {github_security_report-0.6.0 → github_security_report-0.7.1}/src/github_security_report/render/slack.py +34 -11
- {github_security_report-0.6.0 → github_security_report-0.7.1}/src/github_security_report/render/terminal.py +27 -15
- {github_security_report-0.6.0 → github_security_report-0.7.1}/src/github_security_report/report.py +27 -9
- {github_security_report-0.6.0 → github_security_report-0.7.1}/src/github_security_report/rulesets.py +12 -0
- {github_security_report-0.6.0 → github_security_report-0.7.1}/src/github_security_report/severity.py +7 -7
- {github_security_report-0.6.0 → github_security_report-0.7.1}/src/github_security_report/templates/report.html.j2 +4 -0
- {github_security_report-0.6.0 → github_security_report-0.7.1}/.gitignore +0 -0
- {github_security_report-0.6.0 → github_security_report-0.7.1}/LICENSE +0 -0
- {github_security_report-0.6.0 → github_security_report-0.7.1}/LICENSES/Apache-2.0.txt +0 -0
- {github_security_report-0.6.0 → github_security_report-0.7.1}/scripts/README.md +0 -0
- {github_security_report-0.6.0 → github_security_report-0.7.1}/src/github_security_report/__init__.py +0 -0
- {github_security_report-0.6.0 → github_security_report-0.7.1}/src/github_security_report/gitctx.py +0 -0
- {github_security_report-0.6.0 → github_security_report-0.7.1}/src/github_security_report/py.typed +0 -0
- {github_security_report-0.6.0 → github_security_report-0.7.1}/src/github_security_report/render/__init__.py +0 -0
- {github_security_report-0.6.0 → github_security_report-0.7.1}/src/github_security_report/runner.py +0 -0
- {github_security_report-0.6.0 → github_security_report-0.7.1}/src/github_security_report/scope.py +0 -0
- {github_security_report-0.6.0 → github_security_report-0.7.1}/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.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
|
|
@@ -56,8 +56,8 @@ Description-Content-Type: text/markdown
|
|
|
56
56
|
|
|
57
57
|
> Security and quality **reporting** (not scanning) across GitHub
|
|
58
58
|
> organisations. Aggregates existing signals — CodeQL, OpenSSF Scorecard,
|
|
59
|
-
> zizmor, Dependabot, and secret scanning — and ranks the
|
|
60
|
-
> remediation effort goes where it is needed.
|
|
59
|
+
> zizmor, aislop (AI slop), Dependabot, and secret scanning — and ranks the
|
|
60
|
+
> worst offenders so remediation effort goes where it is needed.
|
|
61
61
|
|
|
62
62
|
## 🗒️ Published reports
|
|
63
63
|
|
|
@@ -82,10 +82,21 @@ stay brevity-first; the explanatory per-category description and documentation
|
|
|
82
82
|
link are shown only on the richer Markdown and HTML (GitHub Pages) outputs.
|
|
83
83
|
|
|
84
84
|
The single GitHub code-scanning feed is partitioned by `tool.name` into CodeQL,
|
|
85
|
-
Scorecard, and
|
|
86
|
-
back to code-scanning findings. See [`docs/BRIEF.md`](docs/BRIEF.md)
|
|
87
|
-
[`docs/phase0-findings.md`](docs/phase0-findings.md) for the full design and
|
|
88
|
-
API research it is built on.
|
|
85
|
+
Scorecard, zizmor, and aislop; Scorecard prefers the external aggregate score
|
|
86
|
+
and falls back to code-scanning findings. See [`docs/BRIEF.md`](docs/BRIEF.md)
|
|
87
|
+
and [`docs/phase0-findings.md`](docs/phase0-findings.md) for the full design and
|
|
88
|
+
the API research it is built on.
|
|
89
|
+
|
|
90
|
+
The workflow-driven signals (OpenSSF Scorecard, zizmor, aislop) only produce
|
|
91
|
+
data when an organisation has deployed supporting workflows. The tool checks
|
|
92
|
+
for that support cheaply before collecting (**feature gating**): an
|
|
93
|
+
organisation with no evidence of a tool — no ruleset requiring its workflow,
|
|
94
|
+
no alerts, no analyses on a sample of repositories — gets a single
|
|
95
|
+
`⏩ Skipping feature: organisation support missing` line for that section
|
|
96
|
+
instead of a nag list. See the
|
|
97
|
+
[organisation scan setup guide](docs/org-scan-setup.md) for the required
|
|
98
|
+
workflows, and disable the check with `report.gating: false` if you want to
|
|
99
|
+
probe everything regardless.
|
|
89
100
|
|
|
90
101
|
Further sections report **configuration posture** and **freshness** as plain
|
|
91
102
|
tables (org mode):
|
|
@@ -153,7 +164,7 @@ organisation and **Repository access** set to *All repositories*, then grant:
|
|
|
153
164
|
| Metadata | Mandatory baseline; listing organisation repositories |
|
|
154
165
|
| Contents | `.github/dependabot.yml`, latest release, and tag dates |
|
|
155
166
|
| Dependabot alerts | Open Dependabot vulnerability alerts |
|
|
156
|
-
| Code scanning alerts | CodeQL / Scorecard / zizmor findings |
|
|
167
|
+
| Code scanning alerts | CodeQL / Scorecard / zizmor / aislop findings |
|
|
157
168
|
| Secret scanning alerts | Open secret-scanning alerts |
|
|
158
169
|
| Administration | Dependabot enablement + security-updates status, and effective branch rules |
|
|
159
170
|
|
|
@@ -311,7 +322,7 @@ both true.
|
|
|
311
322
|
The example above hides Zizmor on every surface, and keeps Releases / Tagging
|
|
312
323
|
out of the terminal and Slack while still publishing it to the Markdown and HTML
|
|
313
324
|
Pages output. The valid category keys are: `codeql`, `scorecard`, `zizmor`,
|
|
314
|
-
`dependabot_alerts`, `secret_scanning`, `dependabot_alerts_enabled`,
|
|
325
|
+
`aislop`, `dependabot_alerts`, `secret_scanning`, `dependabot_alerts_enabled`,
|
|
315
326
|
`dependabot_updates_enabled`, `dependabot_cooldown`, `releases`,
|
|
316
327
|
`mutable_releases`, `private_vulnerability_reporting`. Like the other `report`
|
|
317
328
|
settings, `categories` can be set
|
|
@@ -328,22 +339,49 @@ unless every org sharing that channel also disables it (this mirrors the
|
|
|
328
339
|
most-generous `top_n` rule applied to the same grouping). The terminal, Markdown
|
|
329
340
|
and HTML surfaces are per-org and are not affected by this union.
|
|
330
341
|
|
|
342
|
+
### Organisation feature gating
|
|
343
|
+
|
|
344
|
+
The workflow-driven signals (OpenSSF Scorecard, zizmor, aislop) need
|
|
345
|
+
organisation-deployed workflows before they produce any data (see the
|
|
346
|
+
[organisation scan setup guide](docs/org-scan-setup.md)). By default the tool
|
|
347
|
+
runs a cheap support check per organisation before collecting each of them:
|
|
348
|
+
evidence is an org ruleset requiring the tool's workflow, existing
|
|
349
|
+
code-scanning alerts from the tool, analyses on a sample of repositories, or
|
|
350
|
+
(for Scorecard) an external scorecard.dev score. A signal with no evidence is
|
|
351
|
+
**skipped** — not probed per repository, not classified — and its section
|
|
352
|
+
shows a single `⏩ Skipping feature: organisation support missing` line
|
|
353
|
+
linking the setup guide, on every output surface. Set `report.gating` to
|
|
354
|
+
`false` (globally or per organisation) to always probe everything:
|
|
355
|
+
|
|
356
|
+
```json
|
|
357
|
+
{
|
|
358
|
+
"report": { "gating": false },
|
|
359
|
+
"organizations": [{ "name": "lfreleng-actions" }]
|
|
360
|
+
}
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
Gating decides **collection**; the per-category render toggles above decide
|
|
364
|
+
**presentation**. A skipped section still renders (as the one-line notice)
|
|
365
|
+
unless its category is also disabled.
|
|
366
|
+
|
|
331
367
|
### Pass/fail severity cutoff
|
|
332
368
|
|
|
333
|
-
The severity-ranked signals (CodeQL, Scorecard, Zizmor, Dependabot
|
|
334
|
-
`fail_severity` cutoff to decide when a repository counts as a
|
|
335
|
-
repository is flagged as an offender only when it carries a finding
|
|
336
|
-
above** the cutoff; findings below it fold into the clean count.
|
|
337
|
-
(lowest to highest) `informational`, `low`, `medium`, `high`,
|
|
338
|
-
`informational` being the sub-low rung for SARIF `none` findings
|
|
339
|
-
unclassifiable alerts. Zizmor's SARIF `note` findings normalise to `low`
|
|
369
|
+
The severity-ranked signals (CodeQL, Scorecard, Zizmor, aislop, Dependabot
|
|
370
|
+
alerts) use a `fail_severity` cutoff to decide when a repository counts as a
|
|
371
|
+
failure. A repository is flagged as an offender only when it carries a finding
|
|
372
|
+
**at or above** the cutoff; findings below it fold into the clean count.
|
|
373
|
+
Severities run (lowest to highest) `informational`, `low`, `medium`, `high`,
|
|
374
|
+
`critical` — `informational` being the sub-low rung for SARIF `none` findings
|
|
375
|
+
and unclassifiable alerts. Zizmor's SARIF `note` findings normalise to `low`
|
|
340
376
|
(zizmor emits its Low findings at `note`, and the organisation scan pipeline's
|
|
341
377
|
`--min-severity low` floor keeps informational findings out of the uploaded
|
|
342
378
|
SARIF), matching the ruleset-enforced PR gate that blocks on note-and-above.
|
|
379
|
+
aislop populates the same SARIF level axis and normalises identically.
|
|
343
380
|
|
|
344
381
|
The global default cutoff is `medium`, so `low` and `informational` findings
|
|
345
|
-
pass. Zizmor
|
|
346
|
-
cutoff per category under
|
|
382
|
+
pass. Zizmor and aislop default to `low` (only `informational` passes).
|
|
383
|
+
Override the cutoff per category under
|
|
384
|
+
`report.categories.<key>.fail_severity`:
|
|
347
385
|
|
|
348
386
|
```json
|
|
349
387
|
{
|
|
@@ -422,13 +460,22 @@ The tool is published to PyPI and runs with `uvx`. Inside a Git checkout with a
|
|
|
422
460
|
`upstream` remote, then `origin`) and prints a Rich table report:
|
|
423
461
|
|
|
424
462
|
```bash
|
|
425
|
-
export GITHUB_TOKEN=
|
|
463
|
+
export GITHUB_TOKEN="your-token"
|
|
426
464
|
uvx github-security-report report
|
|
427
465
|
|
|
428
466
|
# Or org mode locally with a PAT:
|
|
429
467
|
uvx github-security-report report --org lfreleng-actions
|
|
430
468
|
```
|
|
431
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
|
+
|
|
432
479
|
## Remediation
|
|
433
480
|
|
|
434
481
|
The `remediate` subcommand is the in-tool counterpart to the report: it runs the
|
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
|
|
13
13
|
> Security and quality **reporting** (not scanning) across GitHub
|
|
14
14
|
> organisations. Aggregates existing signals — CodeQL, OpenSSF Scorecard,
|
|
15
|
-
> zizmor, Dependabot, and secret scanning — and ranks the
|
|
16
|
-
> remediation effort goes where it is needed.
|
|
15
|
+
> zizmor, aislop (AI slop), Dependabot, and secret scanning — and ranks the
|
|
16
|
+
> worst offenders so remediation effort goes where it is needed.
|
|
17
17
|
|
|
18
18
|
## 🗒️ Published reports
|
|
19
19
|
|
|
@@ -38,10 +38,21 @@ stay brevity-first; the explanatory per-category description and documentation
|
|
|
38
38
|
link are shown only on the richer Markdown and HTML (GitHub Pages) outputs.
|
|
39
39
|
|
|
40
40
|
The single GitHub code-scanning feed is partitioned by `tool.name` into CodeQL,
|
|
41
|
-
Scorecard, and
|
|
42
|
-
back to code-scanning findings. See [`docs/BRIEF.md`](docs/BRIEF.md)
|
|
43
|
-
[`docs/phase0-findings.md`](docs/phase0-findings.md) for the full design and
|
|
44
|
-
API research it is built on.
|
|
41
|
+
Scorecard, zizmor, and aislop; Scorecard prefers the external aggregate score
|
|
42
|
+
and falls back to code-scanning findings. See [`docs/BRIEF.md`](docs/BRIEF.md)
|
|
43
|
+
and [`docs/phase0-findings.md`](docs/phase0-findings.md) for the full design and
|
|
44
|
+
the API research it is built on.
|
|
45
|
+
|
|
46
|
+
The workflow-driven signals (OpenSSF Scorecard, zizmor, aislop) only produce
|
|
47
|
+
data when an organisation has deployed supporting workflows. The tool checks
|
|
48
|
+
for that support cheaply before collecting (**feature gating**): an
|
|
49
|
+
organisation with no evidence of a tool — no ruleset requiring its workflow,
|
|
50
|
+
no alerts, no analyses on a sample of repositories — gets a single
|
|
51
|
+
`⏩ Skipping feature: organisation support missing` line for that section
|
|
52
|
+
instead of a nag list. See the
|
|
53
|
+
[organisation scan setup guide](docs/org-scan-setup.md) for the required
|
|
54
|
+
workflows, and disable the check with `report.gating: false` if you want to
|
|
55
|
+
probe everything regardless.
|
|
45
56
|
|
|
46
57
|
Further sections report **configuration posture** and **freshness** as plain
|
|
47
58
|
tables (org mode):
|
|
@@ -109,7 +120,7 @@ organisation and **Repository access** set to *All repositories*, then grant:
|
|
|
109
120
|
| Metadata | Mandatory baseline; listing organisation repositories |
|
|
110
121
|
| Contents | `.github/dependabot.yml`, latest release, and tag dates |
|
|
111
122
|
| Dependabot alerts | Open Dependabot vulnerability alerts |
|
|
112
|
-
| Code scanning alerts | CodeQL / Scorecard / zizmor findings |
|
|
123
|
+
| Code scanning alerts | CodeQL / Scorecard / zizmor / aislop findings |
|
|
113
124
|
| Secret scanning alerts | Open secret-scanning alerts |
|
|
114
125
|
| Administration | Dependabot enablement + security-updates status, and effective branch rules |
|
|
115
126
|
|
|
@@ -267,7 +278,7 @@ both true.
|
|
|
267
278
|
The example above hides Zizmor on every surface, and keeps Releases / Tagging
|
|
268
279
|
out of the terminal and Slack while still publishing it to the Markdown and HTML
|
|
269
280
|
Pages output. The valid category keys are: `codeql`, `scorecard`, `zizmor`,
|
|
270
|
-
`dependabot_alerts`, `secret_scanning`, `dependabot_alerts_enabled`,
|
|
281
|
+
`aislop`, `dependabot_alerts`, `secret_scanning`, `dependabot_alerts_enabled`,
|
|
271
282
|
`dependabot_updates_enabled`, `dependabot_cooldown`, `releases`,
|
|
272
283
|
`mutable_releases`, `private_vulnerability_reporting`. Like the other `report`
|
|
273
284
|
settings, `categories` can be set
|
|
@@ -284,22 +295,49 @@ unless every org sharing that channel also disables it (this mirrors the
|
|
|
284
295
|
most-generous `top_n` rule applied to the same grouping). The terminal, Markdown
|
|
285
296
|
and HTML surfaces are per-org and are not affected by this union.
|
|
286
297
|
|
|
298
|
+
### Organisation feature gating
|
|
299
|
+
|
|
300
|
+
The workflow-driven signals (OpenSSF Scorecard, zizmor, aislop) need
|
|
301
|
+
organisation-deployed workflows before they produce any data (see the
|
|
302
|
+
[organisation scan setup guide](docs/org-scan-setup.md)). By default the tool
|
|
303
|
+
runs a cheap support check per organisation before collecting each of them:
|
|
304
|
+
evidence is an org ruleset requiring the tool's workflow, existing
|
|
305
|
+
code-scanning alerts from the tool, analyses on a sample of repositories, or
|
|
306
|
+
(for Scorecard) an external scorecard.dev score. A signal with no evidence is
|
|
307
|
+
**skipped** — not probed per repository, not classified — and its section
|
|
308
|
+
shows a single `⏩ Skipping feature: organisation support missing` line
|
|
309
|
+
linking the setup guide, on every output surface. Set `report.gating` to
|
|
310
|
+
`false` (globally or per organisation) to always probe everything:
|
|
311
|
+
|
|
312
|
+
```json
|
|
313
|
+
{
|
|
314
|
+
"report": { "gating": false },
|
|
315
|
+
"organizations": [{ "name": "lfreleng-actions" }]
|
|
316
|
+
}
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
Gating decides **collection**; the per-category render toggles above decide
|
|
320
|
+
**presentation**. A skipped section still renders (as the one-line notice)
|
|
321
|
+
unless its category is also disabled.
|
|
322
|
+
|
|
287
323
|
### Pass/fail severity cutoff
|
|
288
324
|
|
|
289
|
-
The severity-ranked signals (CodeQL, Scorecard, Zizmor, Dependabot
|
|
290
|
-
`fail_severity` cutoff to decide when a repository counts as a
|
|
291
|
-
repository is flagged as an offender only when it carries a finding
|
|
292
|
-
above** the cutoff; findings below it fold into the clean count.
|
|
293
|
-
(lowest to highest) `informational`, `low`, `medium`, `high`,
|
|
294
|
-
`informational` being the sub-low rung for SARIF `none` findings
|
|
295
|
-
unclassifiable alerts. Zizmor's SARIF `note` findings normalise to `low`
|
|
325
|
+
The severity-ranked signals (CodeQL, Scorecard, Zizmor, aislop, Dependabot
|
|
326
|
+
alerts) use a `fail_severity` cutoff to decide when a repository counts as a
|
|
327
|
+
failure. A repository is flagged as an offender only when it carries a finding
|
|
328
|
+
**at or above** the cutoff; findings below it fold into the clean count.
|
|
329
|
+
Severities run (lowest to highest) `informational`, `low`, `medium`, `high`,
|
|
330
|
+
`critical` — `informational` being the sub-low rung for SARIF `none` findings
|
|
331
|
+
and unclassifiable alerts. Zizmor's SARIF `note` findings normalise to `low`
|
|
296
332
|
(zizmor emits its Low findings at `note`, and the organisation scan pipeline's
|
|
297
333
|
`--min-severity low` floor keeps informational findings out of the uploaded
|
|
298
334
|
SARIF), matching the ruleset-enforced PR gate that blocks on note-and-above.
|
|
335
|
+
aislop populates the same SARIF level axis and normalises identically.
|
|
299
336
|
|
|
300
337
|
The global default cutoff is `medium`, so `low` and `informational` findings
|
|
301
|
-
pass. Zizmor
|
|
302
|
-
cutoff per category under
|
|
338
|
+
pass. Zizmor and aislop default to `low` (only `informational` passes).
|
|
339
|
+
Override the cutoff per category under
|
|
340
|
+
`report.categories.<key>.fail_severity`:
|
|
303
341
|
|
|
304
342
|
```json
|
|
305
343
|
{
|
|
@@ -378,13 +416,22 @@ The tool is published to PyPI and runs with `uvx`. Inside a Git checkout with a
|
|
|
378
416
|
`upstream` remote, then `origin`) and prints a Rich table report:
|
|
379
417
|
|
|
380
418
|
```bash
|
|
381
|
-
export GITHUB_TOKEN=
|
|
419
|
+
export GITHUB_TOKEN="your-token"
|
|
382
420
|
uvx github-security-report report
|
|
383
421
|
|
|
384
422
|
# Or org mode locally with a PAT:
|
|
385
423
|
uvx github-security-report report --org lfreleng-actions
|
|
386
424
|
```
|
|
387
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
|
+
|
|
388
435
|
## Remediation
|
|
389
436
|
|
|
390
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.6.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.
|
|
22
|
-
__version_tuple__ = version_tuple = (0,
|
|
21
|
+
__version__ = version = '0.7.1'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 7, 1)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -31,6 +31,7 @@ class CategoryKey(str, Enum):
|
|
|
31
31
|
CODEQL = "codeql"
|
|
32
32
|
SCORECARD = "scorecard"
|
|
33
33
|
ZIZMOR = "zizmor"
|
|
34
|
+
AISLOP = "aislop"
|
|
34
35
|
DEPENDABOT_ALERTS = "dependabot_alerts"
|
|
35
36
|
SECRET_SCANNING = "secret_scanning"
|
|
36
37
|
DEPENDABOT_ALERTS_ENABLED = "dependabot_alerts_enabled"
|
|
@@ -110,6 +111,20 @@ _CATEGORIES: dict[CategoryKey, CategoryMeta] = {
|
|
|
110
111
|
# matching the ruleset-enforced PR gate that blocks on note-and-above.
|
|
111
112
|
fail_severity=Severity.LOW,
|
|
112
113
|
),
|
|
114
|
+
CategoryKey.AISLOP: CategoryMeta(
|
|
115
|
+
key=CategoryKey.AISLOP,
|
|
116
|
+
title="AI Slop Analysis",
|
|
117
|
+
pass_label="Clean",
|
|
118
|
+
fail_label=None,
|
|
119
|
+
url="https://github.com/scanaislop/aislop",
|
|
120
|
+
description=(
|
|
121
|
+
"aislop AI-slop / code-quality findings, ranked worst-first by severity."
|
|
122
|
+
),
|
|
123
|
+
# aislop, like zizmor, populates only the SARIF level axis
|
|
124
|
+
# (error/warning/note); "note" normalises to LOW (see severity.py), so
|
|
125
|
+
# any aislop finding fails -- matching the ruleset-enforced PR gate.
|
|
126
|
+
fail_severity=Severity.LOW,
|
|
127
|
+
),
|
|
113
128
|
CategoryKey.DEPENDABOT_ALERTS: CategoryMeta(
|
|
114
129
|
key=CategoryKey.DEPENDABOT_ALERTS,
|
|
115
130
|
title="Dependabot: Security Alerts",
|
{github_security_report-0.6.0 → github_security_report-0.7.1}/src/github_security_report/classify.py
RENAMED
|
@@ -6,8 +6,8 @@ Pure, transport-free logic encoding every Phase 0 finding
|
|
|
6
6
|
(``docs/phase0-findings.md``):
|
|
7
7
|
|
|
8
8
|
- the single code-scanning feed is partitioned by ``tool.name`` into CodeQL,
|
|
9
|
-
Scorecard and
|
|
10
|
-
- CodeQL/Scorecard/zizmor enablement is the presence of that tool in
|
|
9
|
+
Scorecard, zizmor and aislop -- counts are filtered per tool;
|
|
10
|
+
- CodeQL/Scorecard/zizmor/aislop enablement is the presence of that tool in
|
|
11
11
|
``code-scanning/analyses`` (not ``default-setup``); a 404 on code scanning
|
|
12
12
|
means it is disabled entirely;
|
|
13
13
|
- secret scanning 404 = disabled, 200 [] = enabled-clean;
|
|
@@ -18,11 +18,12 @@ Pure, transport-free logic encoding every Phase 0 finding
|
|
|
18
18
|
|
|
19
19
|
from __future__ import annotations
|
|
20
20
|
|
|
21
|
-
from collections.abc import Mapping
|
|
21
|
+
from collections.abc import Callable, Mapping, Set
|
|
22
22
|
from dataclasses import dataclass, field
|
|
23
23
|
|
|
24
24
|
from github_security_report import severity
|
|
25
25
|
from github_security_report.models import (
|
|
26
|
+
CODE_SCANNING_TOOLS,
|
|
26
27
|
Repo,
|
|
27
28
|
RepoSignal,
|
|
28
29
|
RepoState,
|
|
@@ -36,9 +37,7 @@ from github_security_report.severity import Severity
|
|
|
36
37
|
FailSeverities = Mapping[SignalType, Severity]
|
|
37
38
|
|
|
38
39
|
|
|
39
|
-
def _cutoff(
|
|
40
|
-
signal: SignalType, fail_severities: FailSeverities | None
|
|
41
|
-
) -> Severity:
|
|
40
|
+
def _cutoff(signal: SignalType, fail_severities: FailSeverities | None) -> Severity:
|
|
42
41
|
"""The fail-severity cutoff for ``signal`` (config override or default)."""
|
|
43
42
|
if fail_severities is not None and signal in fail_severities:
|
|
44
43
|
return fail_severities[signal]
|
|
@@ -50,7 +49,7 @@ class RepoFacts:
|
|
|
50
49
|
"""Raw per-repository facts gathered by the client, pre-classification."""
|
|
51
50
|
|
|
52
51
|
repo: Repo
|
|
53
|
-
# Code scanning (covers CodeQL, Scorecard, zizmor).
|
|
52
|
+
# Code scanning (covers CodeQL, Scorecard, zizmor, aislop).
|
|
54
53
|
code_scanning_status: int = 200 # 200 ok, 404 disabled, 403 forbidden
|
|
55
54
|
code_scanning_tools: set[str] = field(default_factory=set) # analyses tool names
|
|
56
55
|
code_scanning_alerts: list[dict] = field(default_factory=list) # all tools
|
|
@@ -130,13 +129,19 @@ def _code_scanning_tool_signal(
|
|
|
130
129
|
facts.code_scanning_status == 200 and tool_name in facts.code_scanning_tools
|
|
131
130
|
)
|
|
132
131
|
if not enabled:
|
|
133
|
-
detail =
|
|
132
|
+
detail = (
|
|
133
|
+
"code scanning disabled"
|
|
134
|
+
if facts.code_scanning_status == 404
|
|
135
|
+
else f"{tool_name} not enabled"
|
|
136
|
+
)
|
|
134
137
|
return RepoSignal(repo, signal, RepoState.NAG, detail=detail)
|
|
135
138
|
counts = count_code_scanning(facts.code_scanning_alerts, tool_name)
|
|
136
139
|
# Enabled with no findings is only "clean" when the alert read succeeded;
|
|
137
140
|
# an unreadable sweep (e.g. org-bulk 403/5xx) must not masquerade as clean.
|
|
138
141
|
if counts.total == 0 and facts.code_scanning_alerts_status != 200:
|
|
139
|
-
return RepoSignal(
|
|
142
|
+
return RepoSignal(
|
|
143
|
+
repo, signal, RepoState.UNKNOWN, detail="alert data unavailable"
|
|
144
|
+
)
|
|
140
145
|
# Only findings at or above the category's fail-severity cutoff make the
|
|
141
146
|
# repo an offender; sub-threshold findings (e.g. informational) fold into
|
|
142
147
|
# the clean count.
|
|
@@ -149,7 +154,10 @@ def classify_codeql(
|
|
|
149
154
|
facts: RepoFacts, fail_severities: FailSeverities | None = None
|
|
150
155
|
) -> RepoSignal:
|
|
151
156
|
return _code_scanning_tool_signal(
|
|
152
|
-
facts,
|
|
157
|
+
facts,
|
|
158
|
+
SignalType.CODEQL,
|
|
159
|
+
CODE_SCANNING_TOOLS[SignalType.CODEQL],
|
|
160
|
+
fail_severities,
|
|
153
161
|
)
|
|
154
162
|
|
|
155
163
|
|
|
@@ -157,7 +165,21 @@ def classify_zizmor(
|
|
|
157
165
|
facts: RepoFacts, fail_severities: FailSeverities | None = None
|
|
158
166
|
) -> RepoSignal:
|
|
159
167
|
return _code_scanning_tool_signal(
|
|
160
|
-
facts,
|
|
168
|
+
facts,
|
|
169
|
+
SignalType.ZIZMOR,
|
|
170
|
+
CODE_SCANNING_TOOLS[SignalType.ZIZMOR],
|
|
171
|
+
fail_severities,
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def classify_aislop(
|
|
176
|
+
facts: RepoFacts, fail_severities: FailSeverities | None = None
|
|
177
|
+
) -> RepoSignal:
|
|
178
|
+
return _code_scanning_tool_signal(
|
|
179
|
+
facts,
|
|
180
|
+
SignalType.AISLOP,
|
|
181
|
+
CODE_SCANNING_TOOLS[SignalType.AISLOP],
|
|
182
|
+
fail_severities,
|
|
161
183
|
)
|
|
162
184
|
|
|
163
185
|
|
|
@@ -166,8 +188,13 @@ def classify_scorecard(
|
|
|
166
188
|
) -> RepoSignal:
|
|
167
189
|
"""Scorecard: prefer the external aggregate score, else code-scanning findings."""
|
|
168
190
|
repo = facts.repo
|
|
169
|
-
counts = count_code_scanning(
|
|
170
|
-
|
|
191
|
+
counts = count_code_scanning(
|
|
192
|
+
facts.code_scanning_alerts, CODE_SCANNING_TOOLS[SignalType.SCORECARD]
|
|
193
|
+
)
|
|
194
|
+
has_cs = (
|
|
195
|
+
CODE_SCANNING_TOOLS[SignalType.SCORECARD] in facts.code_scanning_tools
|
|
196
|
+
and facts.code_scanning_status == 200
|
|
197
|
+
)
|
|
171
198
|
cutoff = _cutoff(SignalType.SCORECARD, fail_severities)
|
|
172
199
|
|
|
173
200
|
if facts.scorecard_status == 200 and facts.scorecard_score is not None:
|
|
@@ -178,21 +205,34 @@ def classify_scorecard(
|
|
|
178
205
|
# the code-scanning cutoff logic used elsewhere in this file.
|
|
179
206
|
clean = facts.scorecard_score >= 10.0 and not counts.at_or_above(cutoff)
|
|
180
207
|
state = RepoState.CLEAN if clean else RepoState.OFFENDER
|
|
181
|
-
return RepoSignal(
|
|
208
|
+
return RepoSignal(
|
|
209
|
+
repo,
|
|
210
|
+
SignalType.SCORECARD,
|
|
211
|
+
state,
|
|
212
|
+
counts=counts,
|
|
213
|
+
score=facts.scorecard_score,
|
|
214
|
+
)
|
|
182
215
|
if has_cs:
|
|
183
216
|
if counts.total == 0 and facts.code_scanning_alerts_status != 200:
|
|
184
|
-
return RepoSignal(
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
|
188
224
|
return RepoSignal(repo, SignalType.SCORECARD, state, counts=counts)
|
|
189
225
|
# An indeterminate external request (a 403 forbidden/blocked, a 5xx
|
|
190
226
|
# including the synthetic 503 a transport failure produces, or a forbidden
|
|
191
227
|
# code-scanning probe) is unknown, not a definitive nag. Only a clean 404
|
|
192
228
|
# with no code-scanning Scorecard data means "no results".
|
|
193
229
|
if facts.code_scanning_status == 403 or facts.scorecard_status not in (200, 404):
|
|
194
|
-
return RepoSignal(
|
|
195
|
-
|
|
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
|
+
)
|
|
196
236
|
|
|
197
237
|
|
|
198
238
|
def classify_secret_scanning(
|
|
@@ -203,9 +243,19 @@ def classify_secret_scanning(
|
|
|
203
243
|
# other classifiers (they are dispatched positionally) and is unused here.
|
|
204
244
|
repo = facts.repo
|
|
205
245
|
if facts.secret_scanning_status == 403:
|
|
206
|
-
return RepoSignal(
|
|
246
|
+
return RepoSignal(
|
|
247
|
+
repo,
|
|
248
|
+
SignalType.SECRET_SCANNING,
|
|
249
|
+
RepoState.UNKNOWN,
|
|
250
|
+
detail="insufficient permission",
|
|
251
|
+
)
|
|
207
252
|
if facts.secret_scanning_status == 404:
|
|
208
|
-
return RepoSignal(
|
|
253
|
+
return RepoSignal(
|
|
254
|
+
repo,
|
|
255
|
+
SignalType.SECRET_SCANNING,
|
|
256
|
+
RepoState.NAG,
|
|
257
|
+
detail="secret scanning disabled",
|
|
258
|
+
)
|
|
209
259
|
# Flat open count; stored as HIGH so ranking (more == worse) works and the
|
|
210
260
|
# repo-mode gate treats leaked secrets as serious, without conflating them
|
|
211
261
|
# with CRITICAL code findings (which would make --fail-threshold critical
|
|
@@ -215,12 +265,19 @@ def classify_secret_scanning(
|
|
|
215
265
|
# Positive evidence of leaked secrets is actionable even when the read was
|
|
216
266
|
# incomplete (a later page failed) or the enablement probe was indeterminate.
|
|
217
267
|
if facts.secret_scanning_open:
|
|
218
|
-
return RepoSignal(
|
|
268
|
+
return RepoSignal(
|
|
269
|
+
repo, SignalType.SECRET_SCANNING, RepoState.OFFENDER, counts=counts
|
|
270
|
+
)
|
|
219
271
|
# A zero count is only "clean" when both the enablement probe and the
|
|
220
272
|
# open-count read succeeded; an indeterminate status (5xx) or an unreadable
|
|
221
273
|
# sweep cannot confirm the repo is clean.
|
|
222
274
|
if facts.secret_scanning_status != 200 or facts.secret_scanning_open_status != 200:
|
|
223
|
-
return RepoSignal(
|
|
275
|
+
return RepoSignal(
|
|
276
|
+
repo,
|
|
277
|
+
SignalType.SECRET_SCANNING,
|
|
278
|
+
RepoState.UNKNOWN,
|
|
279
|
+
detail="alert data unavailable",
|
|
280
|
+
)
|
|
224
281
|
return RepoSignal(repo, SignalType.SECRET_SCANNING, RepoState.CLEAN, counts=counts)
|
|
225
282
|
|
|
226
283
|
|
|
@@ -229,34 +286,60 @@ def classify_dependabot(
|
|
|
229
286
|
) -> RepoSignal:
|
|
230
287
|
repo = facts.repo
|
|
231
288
|
if facts.dependabot_enabled is None:
|
|
232
|
-
return RepoSignal(
|
|
289
|
+
return RepoSignal(
|
|
290
|
+
repo, SignalType.DEPENDABOT, RepoState.UNKNOWN, detail="indeterminate"
|
|
291
|
+
)
|
|
233
292
|
if facts.dependabot_enabled is False:
|
|
234
|
-
return RepoSignal(
|
|
293
|
+
return RepoSignal(
|
|
294
|
+
repo,
|
|
295
|
+
SignalType.DEPENDABOT,
|
|
296
|
+
RepoState.NAG,
|
|
297
|
+
detail="Dependabot alerts disabled",
|
|
298
|
+
)
|
|
235
299
|
counts = count_dependabot(facts.dependabot_alerts)
|
|
236
300
|
# Enabled with no alerts is only "clean" when the alert read succeeded.
|
|
237
301
|
if counts.total == 0 and facts.dependabot_alerts_status != 200:
|
|
238
|
-
return RepoSignal(
|
|
302
|
+
return RepoSignal(
|
|
303
|
+
repo,
|
|
304
|
+
SignalType.DEPENDABOT,
|
|
305
|
+
RepoState.UNKNOWN,
|
|
306
|
+
detail="alert data unavailable",
|
|
307
|
+
)
|
|
239
308
|
failing = counts.at_or_above(_cutoff(SignalType.DEPENDABOT, fail_severities))
|
|
240
309
|
state = RepoState.OFFENDER if failing else RepoState.CLEAN
|
|
241
310
|
return RepoSignal(repo, SignalType.DEPENDABOT, state, counts=counts)
|
|
242
311
|
|
|
243
312
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
313
|
+
# One classifier per signal, keyed so orchestration can skip gated-out signals.
|
|
314
|
+
_Classifier = Callable[["RepoFacts", "FailSeverities | None"], RepoSignal]
|
|
315
|
+
|
|
316
|
+
_CLASSIFIERS: dict[SignalType, _Classifier] = {
|
|
317
|
+
SignalType.CODEQL: classify_codeql,
|
|
318
|
+
SignalType.SCORECARD: classify_scorecard,
|
|
319
|
+
SignalType.ZIZMOR: classify_zizmor,
|
|
320
|
+
SignalType.AISLOP: classify_aislop,
|
|
321
|
+
SignalType.DEPENDABOT: classify_dependabot,
|
|
322
|
+
SignalType.SECRET_SCANNING: classify_secret_scanning,
|
|
323
|
+
}
|
|
251
324
|
|
|
252
325
|
|
|
253
326
|
def classify_repo(
|
|
254
|
-
facts: RepoFacts,
|
|
327
|
+
facts: RepoFacts,
|
|
328
|
+
fail_severities: FailSeverities | None = None,
|
|
329
|
+
*,
|
|
330
|
+
skip: Set[SignalType] = frozenset(),
|
|
255
331
|
) -> list[RepoSignal]:
|
|
256
|
-
"""Classify a repository across all
|
|
332
|
+
"""Classify a repository across all signals (minus any in ``skip``).
|
|
257
333
|
|
|
258
334
|
``fail_severities`` optionally overrides the per-signal fail-severity cutoff
|
|
259
335
|
(otherwise each signal uses its category default); it governs which findings
|
|
260
|
-
are severe enough to mark a repository as an offender.
|
|
336
|
+
are severe enough to mark a repository as an offender. ``skip`` names
|
|
337
|
+
signals to leave unclassified entirely -- used when organisation feature
|
|
338
|
+
gating (:mod:`gating`) found no support for a workflow-driven tool, so the
|
|
339
|
+
repository is neither nagged nor counted for that signal.
|
|
261
340
|
"""
|
|
262
|
-
return [
|
|
341
|
+
return [
|
|
342
|
+
classifier(facts, fail_severities)
|
|
343
|
+
for signal, classifier in _CLASSIFIERS.items()
|
|
344
|
+
if signal not in skip
|
|
345
|
+
]
|