github-security-report 0.10.0__tar.gz → 0.11.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.10.0 → github_security_report-0.11.0}/PKG-INFO +194 -5
- {github_security_report-0.10.0 → github_security_report-0.11.0}/README.md +190 -1
- {github_security_report-0.10.0 → github_security_report-0.11.0}/pyproject.toml +3 -3
- {github_security_report-0.10.0 → github_security_report-0.11.0}/src/github_security_report/_version.py +2 -2
- {github_security_report-0.10.0 → github_security_report-0.11.0}/src/github_security_report/categories.py +25 -1
- github_security_report-0.11.0/src/github_security_report/cli/__init__.py +66 -0
- github_security_report-0.11.0/src/github_security_report/cli/__main__.py +16 -0
- github_security_report-0.11.0/src/github_security_report/cli/app.py +338 -0
- github_security_report-0.11.0/src/github_security_report/cli/modes.py +376 -0
- github_security_report-0.11.0/src/github_security_report/cli/outputs.py +158 -0
- github_security_report-0.11.0/src/github_security_report/cli/serialise.py +87 -0
- github_security_report-0.11.0/src/github_security_report/client/__init__.py +84 -0
- github_security_report-0.11.0/src/github_security_report/client/endpoints.py +90 -0
- github_security_report-0.11.0/src/github_security_report/client/parsers.py +275 -0
- github_security_report-0.11.0/src/github_security_report/client/queries.py +88 -0
- github_security_report-0.11.0/src/github_security_report/client/reads.py +391 -0
- github_security_report-0.11.0/src/github_security_report/client/transport.py +288 -0
- github_security_report-0.11.0/src/github_security_report/client/writes.py +135 -0
- github_security_report-0.11.0/src/github_security_report/collect/__init__.py +44 -0
- github_security_report-0.11.0/src/github_security_report/collect/context.py +89 -0
- github_security_report-0.11.0/src/github_security_report/collect/extras.py +108 -0
- github_security_report-0.11.0/src/github_security_report/collect/org.py +384 -0
- github_security_report-0.11.0/src/github_security_report/collect/protocols.py +108 -0
- github_security_report-0.11.0/src/github_security_report/collect/repo.py +88 -0
- github_security_report-0.11.0/src/github_security_report/config/__init__.py +82 -0
- github_security_report-0.11.0/src/github_security_report/config/loader.py +349 -0
- github_security_report-0.11.0/src/github_security_report/config/models.py +234 -0
- github_security_report-0.11.0/src/github_security_report/config/schema.py +178 -0
- github_security_report-0.11.0/src/github_security_report/issues.py +247 -0
- {github_security_report-0.10.0 → github_security_report-0.11.0}/src/github_security_report/models.py +45 -0
- github_security_report-0.11.0/src/github_security_report/ordering.py +222 -0
- {github_security_report-0.10.0 → github_security_report-0.11.0}/src/github_security_report/posture.py +10 -3
- {github_security_report-0.10.0 → github_security_report-0.11.0}/src/github_security_report/render/html.py +32 -34
- {github_security_report-0.10.0 → github_security_report-0.11.0}/src/github_security_report/render/markdown.py +39 -30
- {github_security_report-0.10.0 → github_security_report-0.11.0}/src/github_security_report/render/slack.py +39 -25
- {github_security_report-0.10.0 → github_security_report-0.11.0}/src/github_security_report/render/terminal.py +40 -26
- {github_security_report-0.10.0 → github_security_report-0.11.0}/src/github_security_report/report.py +124 -81
- github_security_report-0.11.0/src/github_security_report/summary.py +95 -0
- {github_security_report-0.10.0 → github_security_report-0.11.0}/src/github_security_report/templates/report.html.j2 +7 -0
- github_security_report-0.10.0/src/github_security_report/cli.py +0 -760
- github_security_report-0.10.0/src/github_security_report/client.py +0 -1013
- github_security_report-0.10.0/src/github_security_report/collect.py +0 -536
- github_security_report-0.10.0/src/github_security_report/config.py +0 -569
- {github_security_report-0.10.0 → github_security_report-0.11.0}/.gitignore +0 -0
- {github_security_report-0.10.0 → github_security_report-0.11.0}/LICENSE +0 -0
- {github_security_report-0.10.0 → github_security_report-0.11.0}/LICENSES/Apache-2.0.txt +0 -0
- {github_security_report-0.10.0 → github_security_report-0.11.0}/scripts/README.md +0 -0
- {github_security_report-0.10.0 → github_security_report-0.11.0}/src/github_security_report/__init__.py +0 -0
- {github_security_report-0.10.0 → github_security_report-0.11.0}/src/github_security_report/classify.py +0 -0
- {github_security_report-0.10.0 → github_security_report-0.11.0}/src/github_security_report/gating.py +0 -0
- {github_security_report-0.10.0 → github_security_report-0.11.0}/src/github_security_report/gitctx.py +0 -0
- {github_security_report-0.10.0 → github_security_report-0.11.0}/src/github_security_report/py.typed +0 -0
- {github_security_report-0.10.0 → github_security_report-0.11.0}/src/github_security_report/remediate.py +0 -0
- {github_security_report-0.10.0 → github_security_report-0.11.0}/src/github_security_report/render/__init__.py +0 -0
- {github_security_report-0.10.0 → github_security_report-0.11.0}/src/github_security_report/rulesets.py +0 -0
- {github_security_report-0.10.0 → github_security_report-0.11.0}/src/github_security_report/runner.py +0 -0
- {github_security_report-0.10.0 → github_security_report-0.11.0}/src/github_security_report/scope.py +0 -0
- {github_security_report-0.10.0 → github_security_report-0.11.0}/src/github_security_report/severity.py +0 -0
- {github_security_report-0.10.0 → github_security_report-0.11.0}/src/github_security_report/templates/index.html.j2 +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: github-security-report
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.11.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
|
|
@@ -29,14 +29,14 @@ Requires-Dist: jinja2==3.1.6
|
|
|
29
29
|
Requires-Dist: jsonschema==4.26.0
|
|
30
30
|
Requires-Dist: pyyaml==6.0.3
|
|
31
31
|
Requires-Dist: rich==15.0.0
|
|
32
|
-
Requires-Dist: typer==0.27.
|
|
32
|
+
Requires-Dist: typer==0.27.1
|
|
33
33
|
Provides-Extra: dev
|
|
34
34
|
Requires-Dist: mypy==2.3.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
37
|
Requires-Dist: pytest==9.1.1; extra == 'dev'
|
|
38
38
|
Requires-Dist: respx==0.23.1; extra == 'dev'
|
|
39
|
-
Requires-Dist: ruff==0.
|
|
39
|
+
Requires-Dist: ruff==0.16.2; extra == 'dev'
|
|
40
40
|
Requires-Dist: syrupy==5.5.3; 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'
|
|
@@ -151,6 +151,20 @@ signal is skipped and every other part of the report is unaffected, so the
|
|
|
151
151
|
minimal tokens below omit it. Grant the org-admin permission only if you want
|
|
152
152
|
ruleset-based tool coverage.
|
|
153
153
|
|
|
154
|
+
A token without that permission gets a `404` from the endpoint, which the tool
|
|
155
|
+
reports at **INFO** — so it is invisible unless you pass `--verbose`:
|
|
156
|
+
|
|
157
|
+
```text
|
|
158
|
+
org rulesets not readable for <org> (status 404); expected unless the token
|
|
159
|
+
carries the optional org-admin permission ...
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
That line is informational, not a defect. Tools are still detected from the
|
|
163
|
+
code-scanning analyses they upload, so the report is identical unless a
|
|
164
|
+
repository is covered *solely* by a required-workflow ruleset whose workflow has
|
|
165
|
+
never run. A genuinely unexpected failure to read the rulesets (for example a
|
|
166
|
+
`5xx`) is still logged as a warning.
|
|
167
|
+
|
|
154
168
|
### Single organisation — fine-grained PAT
|
|
155
169
|
|
|
156
170
|
A fine-grained PAT is bound to one resource owner, so it works for a report
|
|
@@ -166,6 +180,7 @@ organisation and **Repository access** set to *All repositories*, then grant:
|
|
|
166
180
|
| Dependabot alerts | Open Dependabot vulnerability alerts |
|
|
167
181
|
| Code scanning alerts | CodeQL / Scorecard / zizmor / aislop findings |
|
|
168
182
|
| Secret scanning alerts | Open secret-scanning alerts |
|
|
183
|
+
| Issues | Open issues and their labels (GitHub Issues table) |
|
|
169
184
|
| Administration | Dependabot enablement + security-updates status, and effective branch rules |
|
|
170
185
|
|
|
171
186
|
**Organization permissions:**
|
|
@@ -324,7 +339,8 @@ out of the terminal and Slack while still publishing it to the Markdown and HTML
|
|
|
324
339
|
Pages output. The valid category keys are: `codeql`, `scorecard`, `zizmor`,
|
|
325
340
|
`aislop`, `dependabot_alerts`, `secret_scanning`, `dependabot_alerts_enabled`,
|
|
326
341
|
`dependabot_updates_enabled`, `dependabot_cooldown`, `releases`,
|
|
327
|
-
`mutable_releases`, `private_vulnerability_reporting`. Like the
|
|
342
|
+
`mutable_releases`, `private_vulnerability_reporting`, `github_issues`. Like the
|
|
343
|
+
other `report`
|
|
328
344
|
settings, `categories` can be set
|
|
329
345
|
globally and overridden per organisation (overrides merge key-by-key, so
|
|
330
346
|
flipping one output leaves the rest untouched). The machine-readable
|
|
@@ -339,6 +355,179 @@ unless every org sharing that channel also disables it (this mirrors the
|
|
|
339
355
|
most-generous `top_n` rule applied to the same grouping). The terminal, Markdown
|
|
340
356
|
and HTML surfaces are per-org and are not affected by this union.
|
|
341
357
|
|
|
358
|
+
### Per-category row limits
|
|
359
|
+
|
|
360
|
+
A category can also set its own `top_n`, capping that one table independently of
|
|
361
|
+
every other. Reach for this when one category is worth showing in full while the
|
|
362
|
+
rest stay short — set it to `0` for no limit at all:
|
|
363
|
+
|
|
364
|
+
```json
|
|
365
|
+
{
|
|
366
|
+
"report": {
|
|
367
|
+
"top_n": 10,
|
|
368
|
+
"categories": {
|
|
369
|
+
"releases": { "top_n": 0 },
|
|
370
|
+
"codeql": { "top_n": 3 }
|
|
371
|
+
}
|
|
372
|
+
},
|
|
373
|
+
"organizations": [{ "name": "lfreleng-actions" }]
|
|
374
|
+
}
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
Here Releases / Tagging lists every repository, CodeQL shows its worst three, and
|
|
378
|
+
every other category keeps the shared limit of 10. A category's `top_n` applies
|
|
379
|
+
to all four surfaces at once; combine it with `top_n_report` / `top_n_cli` /
|
|
380
|
+
`top_n_slack` to vary the fallback per surface.
|
|
381
|
+
|
|
382
|
+
The resolution order for one category on one surface, most specific first:
|
|
383
|
+
|
|
384
|
+
1. `--top-n-report` / `--top-n-cli` / `--top-n-slack` (command line)
|
|
385
|
+
2. `--top-n` (command line)
|
|
386
|
+
3. `report.categories.<key>.top_n` (config)
|
|
387
|
+
4. `report.top_n_report` / `top_n_cli` / `top_n_slack` (config)
|
|
388
|
+
5. `report.top_n` (config, default `10`)
|
|
389
|
+
|
|
390
|
+
Command-line flags deliberately outrank the per-category configuration: a flag is
|
|
391
|
+
a decision about a single run, so `--top-n 5` caps every category even where the
|
|
392
|
+
config asked for an uncapped one. `0` means "no limit" at every level. In a
|
|
393
|
+
shared Slack channel the most generous value any contributing org configured for
|
|
394
|
+
that category wins, matching the visibility rule above.
|
|
395
|
+
|
|
396
|
+
### Per-category row ordering
|
|
397
|
+
|
|
398
|
+
Each table ships a sensible default ordering — largest backlog first, stalest
|
|
399
|
+
release first, and so on. `report.categories.<key>.sort` overrides it with a list
|
|
400
|
+
of column names, evaluated left to right:
|
|
401
|
+
|
|
402
|
+
```json
|
|
403
|
+
{
|
|
404
|
+
"report": {
|
|
405
|
+
"categories": {
|
|
406
|
+
"github_issues": { "sort": ["untriaged", "bug", "total", "oldest"] }
|
|
407
|
+
}
|
|
408
|
+
},
|
|
409
|
+
"organizations": [{ "name": "lfreleng-actions" }]
|
|
410
|
+
}
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
That ranks the Issues table by untriaged count, breaking ties on Bug, then on
|
|
414
|
+
total open issues, then on the oldest issue.
|
|
415
|
+
|
|
416
|
+
- Names match column headers **case-insensitively**, so `untriaged` finds
|
|
417
|
+
`Untriaged` and a custom `issue_labels` column such as `Regression` works with
|
|
418
|
+
no extra configuration. `repository` sorts by repository name.
|
|
419
|
+
- **Direction is implicit by type**: numeric columns descend (most first, which
|
|
420
|
+
is also oldest-first for an age column) and text columns ascend.
|
|
421
|
+
- A leading **`-` forces descending** and **`+` forces ascending**, so
|
|
422
|
+
`["+total"]` lists the smallest backlogs first.
|
|
423
|
+
- A cell with **no value to sort on** — an `Oldest` of `unknown`, say — stays at
|
|
424
|
+
the bottom whichever direction you choose. Missing is not the same as small.
|
|
425
|
+
- The repository name is always applied as the final tiebreaker, so rows that
|
|
426
|
+
are equal under every configured term still order deterministically.
|
|
427
|
+
- An unrecognised column name is logged and skipped rather than failing the run.
|
|
428
|
+
- Omitting `sort` keeps the table's own default ordering. This matters: some
|
|
429
|
+
defaults rank on values that are never displayed as a column — Releases /
|
|
430
|
+
Tagging ranks on *missing* release and tag signals — so they cannot be
|
|
431
|
+
expressed as a column list.
|
|
432
|
+
|
|
433
|
+
Ordering is resolved once, when the report is built, so every surface and
|
|
434
|
+
`report.json` agree. It applies to the generic tables (GitHub Issues, Releases /
|
|
435
|
+
Tagging, Mutable Releases, the Dependabot posture tables). The severity signal
|
|
436
|
+
tables keep their own ranking, which encodes domain logic a column sort would
|
|
437
|
+
flatten — Scorecard cascades through the worst populated severity rung so a lone
|
|
438
|
+
Critical is never buried by a weaker repository with a lower score.
|
|
439
|
+
|
|
440
|
+
### GitHub Issues
|
|
441
|
+
|
|
442
|
+
The `github_issues` category counts each repository's **open issues**, split by
|
|
443
|
+
label into columns. It reads from the same batched GraphQL prefetch as the
|
|
444
|
+
release and Dependabot data, so it costs no extra API requests:
|
|
445
|
+
|
|
446
|
+
```text
|
|
447
|
+
GitHub Issues
|
|
448
|
+
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━┳━━━━━━━━━┳━━━━━━┳━━━━━━━┳━━━━━━━━━━━┳━━━━━━━┳━━━━━━━━━┓
|
|
449
|
+
┃ Repository ┃ Bug ┃ Feature ┃ Docs ┃ Other ┃ Untriaged ┃ Total ┃ Oldest ┃
|
|
450
|
+
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━╇━━━━━━━━━╇━━━━━━╇━━━━━━━╇━━━━━━━━━━━╇━━━━━━━╇━━━━━━━━━┩
|
|
451
|
+
│ .github │ 1 │ 0 │ 0 │ 0 │ 10 │ 11 │ 16 days │
|
|
452
|
+
│ tag-validate-action │ 0 │ 0 │ 0 │ 0 │ 8 │ 8 │ 25 days │
|
|
453
|
+
│ security-workflows │ 0 │ 5 │ 1 │ 0 │ 0 │ 6 │ today │
|
|
454
|
+
│ github-security-report-action │ 0 │ 0 │ 0 │ 3 │ 1 │ 4 │ 52 days │
|
|
455
|
+
│ dependamerge │ 0 │ 1 │ 0 │ 1 │ 1 │ 3 │ 52 days │
|
|
456
|
+
├───────────────────────────────┼─────┼─────────┼──────┼───────┼───────────┼───────┼─────────┤
|
|
457
|
+
│ Total │ 1 │ 6 │ 1 │ 4 │ 20 │ 32 │ │
|
|
458
|
+
└───────────────────────────────┴─────┴─────────┴──────┴───────┴───────────┴───────┴─────────┘
|
|
459
|
+
… and 11 more
|
|
460
|
+
❌ 16 With open issues
|
|
461
|
+
✅ 87 No open issues
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
That is a real run of `lfreleng-actions` under `top_n: 5`. The totals row sums
|
|
465
|
+
the rows actually **displayed**, matching the offender tables, so it stays
|
|
466
|
+
consistent with a truncated view; `… and 11 more` plus the five listed rows
|
|
467
|
+
reconcile with the `❌ 16` in the footer.
|
|
468
|
+
|
|
469
|
+
Two columns are always present and are not configurable:
|
|
470
|
+
|
|
471
|
+
- **Other** — the issue is labelled, but with nothing you asked about.
|
|
472
|
+
- **Untriaged** — the issue has no labels at all. This is the column to watch:
|
|
473
|
+
an unlabelled issue is one nobody has categorised.
|
|
474
|
+
|
|
475
|
+
Both names are reserved, as are `Repository`, `Total` and `Oldest`: configuring a
|
|
476
|
+
column with one of those names is rejected, because it would either share a
|
|
477
|
+
counter with the implicit column — stopping the class columns summing to `Total`
|
|
478
|
+
— or duplicate a header, which would also make `sort: ["repository"]` resolve to
|
|
479
|
+
a count column instead of the repository name. Column names must additionally be
|
|
480
|
+
non-blank, unpadded, distinct case-insensitively (`sort` matches them that way,
|
|
481
|
+
and strips its terms), and free of `|`, backticks and control characters, which
|
|
482
|
+
would corrupt the Markdown table or Slack code fence they are rendered into.
|
|
483
|
+
|
|
484
|
+
The remaining columns come from `report.issue_labels`, which maps a column name
|
|
485
|
+
to the issue labels that count towards it. An issue counts **once**, under the
|
|
486
|
+
first column whose labels it carries, so the columns always sum to the classified
|
|
487
|
+
total. Matching is case-insensitive on the whole label name, so `docs` does not
|
|
488
|
+
swallow an unrelated `docs-needed`. The default is:
|
|
489
|
+
|
|
490
|
+
```json
|
|
491
|
+
{
|
|
492
|
+
"report": {
|
|
493
|
+
"issue_labels": {
|
|
494
|
+
"Bug": ["bug", "defect"],
|
|
495
|
+
"Feature": ["feature", "enhancement"],
|
|
496
|
+
"Docs": ["documentation", "docs"]
|
|
497
|
+
}
|
|
498
|
+
},
|
|
499
|
+
"organizations": [{ "name": "lfreleng-actions" }]
|
|
500
|
+
}
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
Unlike `ruleset_workflows`, a configured `issue_labels` **replaces** the default
|
|
504
|
+
rather than merging into it — the mapping defines a coherent set of table
|
|
505
|
+
columns, so merging would leave behind default columns you deliberately left out.
|
|
506
|
+
|
|
507
|
+
Repositories with no open issues are counted in the `✅ No open issues` footer
|
|
508
|
+
rather than listed. Rows rank by total open issues, then by Untriaged. Pull
|
|
509
|
+
requests are **not** counted: the GraphQL `issues` connection excludes them.
|
|
510
|
+
|
|
511
|
+
> **Accuracy note.** `Total` is exact at any backlog size, as is `Oldest`
|
|
512
|
+
> wherever an age is shown. The label columns are computed from a bounded,
|
|
513
|
+
> oldest-first window of each repository's open issues (25 issues, 5 labels
|
|
514
|
+
> each) that keeps the query well inside GitHub's GraphQL rate-limit budget. A
|
|
515
|
+
> repository whose label breakdown is partial shows a trailing `+` on its
|
|
516
|
+
> `Oldest` cell. That covers a backlog exceeding the issue window, and any issue
|
|
517
|
+
> whose classification a label beyond the label window could have changed —
|
|
518
|
+
> which is every classification except a match on the *first* configured column,
|
|
519
|
+
> since columns are matched in declaration order and an unseen label could
|
|
520
|
+
> belong to an earlier one. An issue whose labels could not be read at all is
|
|
521
|
+
> left out of the class columns entirely rather than counted as `Untriaged`. An
|
|
522
|
+
> `Oldest` of `unknown` means the oldest issue came back unreadable or undated;
|
|
523
|
+
> it can still carry the `+`.
|
|
524
|
+
|
|
525
|
+
**Permissions.** A fine-grained PAT needs **Issues: read** for this table; a
|
|
526
|
+
classic PAT's `repo` scope already covers it. Without it GitHub serves the query
|
|
527
|
+
with HTTP 200 and this one field null, so affected repositories are reported as
|
|
528
|
+
`❓ Unknown` rather than counted as having no open issues — an unreadable backlog
|
|
529
|
+
is never presented as a clean one.
|
|
530
|
+
|
|
342
531
|
### Organisation feature gating
|
|
343
532
|
|
|
344
533
|
The workflow-driven signals (OpenSSF Scorecard, zizmor, aislop) need
|
|
@@ -107,6 +107,20 @@ signal is skipped and every other part of the report is unaffected, so the
|
|
|
107
107
|
minimal tokens below omit it. Grant the org-admin permission only if you want
|
|
108
108
|
ruleset-based tool coverage.
|
|
109
109
|
|
|
110
|
+
A token without that permission gets a `404` from the endpoint, which the tool
|
|
111
|
+
reports at **INFO** — so it is invisible unless you pass `--verbose`:
|
|
112
|
+
|
|
113
|
+
```text
|
|
114
|
+
org rulesets not readable for <org> (status 404); expected unless the token
|
|
115
|
+
carries the optional org-admin permission ...
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
That line is informational, not a defect. Tools are still detected from the
|
|
119
|
+
code-scanning analyses they upload, so the report is identical unless a
|
|
120
|
+
repository is covered *solely* by a required-workflow ruleset whose workflow has
|
|
121
|
+
never run. A genuinely unexpected failure to read the rulesets (for example a
|
|
122
|
+
`5xx`) is still logged as a warning.
|
|
123
|
+
|
|
110
124
|
### Single organisation — fine-grained PAT
|
|
111
125
|
|
|
112
126
|
A fine-grained PAT is bound to one resource owner, so it works for a report
|
|
@@ -122,6 +136,7 @@ organisation and **Repository access** set to *All repositories*, then grant:
|
|
|
122
136
|
| Dependabot alerts | Open Dependabot vulnerability alerts |
|
|
123
137
|
| Code scanning alerts | CodeQL / Scorecard / zizmor / aislop findings |
|
|
124
138
|
| Secret scanning alerts | Open secret-scanning alerts |
|
|
139
|
+
| Issues | Open issues and their labels (GitHub Issues table) |
|
|
125
140
|
| Administration | Dependabot enablement + security-updates status, and effective branch rules |
|
|
126
141
|
|
|
127
142
|
**Organization permissions:**
|
|
@@ -280,7 +295,8 @@ out of the terminal and Slack while still publishing it to the Markdown and HTML
|
|
|
280
295
|
Pages output. The valid category keys are: `codeql`, `scorecard`, `zizmor`,
|
|
281
296
|
`aislop`, `dependabot_alerts`, `secret_scanning`, `dependabot_alerts_enabled`,
|
|
282
297
|
`dependabot_updates_enabled`, `dependabot_cooldown`, `releases`,
|
|
283
|
-
`mutable_releases`, `private_vulnerability_reporting`. Like the
|
|
298
|
+
`mutable_releases`, `private_vulnerability_reporting`, `github_issues`. Like the
|
|
299
|
+
other `report`
|
|
284
300
|
settings, `categories` can be set
|
|
285
301
|
globally and overridden per organisation (overrides merge key-by-key, so
|
|
286
302
|
flipping one output leaves the rest untouched). The machine-readable
|
|
@@ -295,6 +311,179 @@ unless every org sharing that channel also disables it (this mirrors the
|
|
|
295
311
|
most-generous `top_n` rule applied to the same grouping). The terminal, Markdown
|
|
296
312
|
and HTML surfaces are per-org and are not affected by this union.
|
|
297
313
|
|
|
314
|
+
### Per-category row limits
|
|
315
|
+
|
|
316
|
+
A category can also set its own `top_n`, capping that one table independently of
|
|
317
|
+
every other. Reach for this when one category is worth showing in full while the
|
|
318
|
+
rest stay short — set it to `0` for no limit at all:
|
|
319
|
+
|
|
320
|
+
```json
|
|
321
|
+
{
|
|
322
|
+
"report": {
|
|
323
|
+
"top_n": 10,
|
|
324
|
+
"categories": {
|
|
325
|
+
"releases": { "top_n": 0 },
|
|
326
|
+
"codeql": { "top_n": 3 }
|
|
327
|
+
}
|
|
328
|
+
},
|
|
329
|
+
"organizations": [{ "name": "lfreleng-actions" }]
|
|
330
|
+
}
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
Here Releases / Tagging lists every repository, CodeQL shows its worst three, and
|
|
334
|
+
every other category keeps the shared limit of 10. A category's `top_n` applies
|
|
335
|
+
to all four surfaces at once; combine it with `top_n_report` / `top_n_cli` /
|
|
336
|
+
`top_n_slack` to vary the fallback per surface.
|
|
337
|
+
|
|
338
|
+
The resolution order for one category on one surface, most specific first:
|
|
339
|
+
|
|
340
|
+
1. `--top-n-report` / `--top-n-cli` / `--top-n-slack` (command line)
|
|
341
|
+
2. `--top-n` (command line)
|
|
342
|
+
3. `report.categories.<key>.top_n` (config)
|
|
343
|
+
4. `report.top_n_report` / `top_n_cli` / `top_n_slack` (config)
|
|
344
|
+
5. `report.top_n` (config, default `10`)
|
|
345
|
+
|
|
346
|
+
Command-line flags deliberately outrank the per-category configuration: a flag is
|
|
347
|
+
a decision about a single run, so `--top-n 5` caps every category even where the
|
|
348
|
+
config asked for an uncapped one. `0` means "no limit" at every level. In a
|
|
349
|
+
shared Slack channel the most generous value any contributing org configured for
|
|
350
|
+
that category wins, matching the visibility rule above.
|
|
351
|
+
|
|
352
|
+
### Per-category row ordering
|
|
353
|
+
|
|
354
|
+
Each table ships a sensible default ordering — largest backlog first, stalest
|
|
355
|
+
release first, and so on. `report.categories.<key>.sort` overrides it with a list
|
|
356
|
+
of column names, evaluated left to right:
|
|
357
|
+
|
|
358
|
+
```json
|
|
359
|
+
{
|
|
360
|
+
"report": {
|
|
361
|
+
"categories": {
|
|
362
|
+
"github_issues": { "sort": ["untriaged", "bug", "total", "oldest"] }
|
|
363
|
+
}
|
|
364
|
+
},
|
|
365
|
+
"organizations": [{ "name": "lfreleng-actions" }]
|
|
366
|
+
}
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
That ranks the Issues table by untriaged count, breaking ties on Bug, then on
|
|
370
|
+
total open issues, then on the oldest issue.
|
|
371
|
+
|
|
372
|
+
- Names match column headers **case-insensitively**, so `untriaged` finds
|
|
373
|
+
`Untriaged` and a custom `issue_labels` column such as `Regression` works with
|
|
374
|
+
no extra configuration. `repository` sorts by repository name.
|
|
375
|
+
- **Direction is implicit by type**: numeric columns descend (most first, which
|
|
376
|
+
is also oldest-first for an age column) and text columns ascend.
|
|
377
|
+
- A leading **`-` forces descending** and **`+` forces ascending**, so
|
|
378
|
+
`["+total"]` lists the smallest backlogs first.
|
|
379
|
+
- A cell with **no value to sort on** — an `Oldest` of `unknown`, say — stays at
|
|
380
|
+
the bottom whichever direction you choose. Missing is not the same as small.
|
|
381
|
+
- The repository name is always applied as the final tiebreaker, so rows that
|
|
382
|
+
are equal under every configured term still order deterministically.
|
|
383
|
+
- An unrecognised column name is logged and skipped rather than failing the run.
|
|
384
|
+
- Omitting `sort` keeps the table's own default ordering. This matters: some
|
|
385
|
+
defaults rank on values that are never displayed as a column — Releases /
|
|
386
|
+
Tagging ranks on *missing* release and tag signals — so they cannot be
|
|
387
|
+
expressed as a column list.
|
|
388
|
+
|
|
389
|
+
Ordering is resolved once, when the report is built, so every surface and
|
|
390
|
+
`report.json` agree. It applies to the generic tables (GitHub Issues, Releases /
|
|
391
|
+
Tagging, Mutable Releases, the Dependabot posture tables). The severity signal
|
|
392
|
+
tables keep their own ranking, which encodes domain logic a column sort would
|
|
393
|
+
flatten — Scorecard cascades through the worst populated severity rung so a lone
|
|
394
|
+
Critical is never buried by a weaker repository with a lower score.
|
|
395
|
+
|
|
396
|
+
### GitHub Issues
|
|
397
|
+
|
|
398
|
+
The `github_issues` category counts each repository's **open issues**, split by
|
|
399
|
+
label into columns. It reads from the same batched GraphQL prefetch as the
|
|
400
|
+
release and Dependabot data, so it costs no extra API requests:
|
|
401
|
+
|
|
402
|
+
```text
|
|
403
|
+
GitHub Issues
|
|
404
|
+
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━┳━━━━━━━━━┳━━━━━━┳━━━━━━━┳━━━━━━━━━━━┳━━━━━━━┳━━━━━━━━━┓
|
|
405
|
+
┃ Repository ┃ Bug ┃ Feature ┃ Docs ┃ Other ┃ Untriaged ┃ Total ┃ Oldest ┃
|
|
406
|
+
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━╇━━━━━━━━━╇━━━━━━╇━━━━━━━╇━━━━━━━━━━━╇━━━━━━━╇━━━━━━━━━┩
|
|
407
|
+
│ .github │ 1 │ 0 │ 0 │ 0 │ 10 │ 11 │ 16 days │
|
|
408
|
+
│ tag-validate-action │ 0 │ 0 │ 0 │ 0 │ 8 │ 8 │ 25 days │
|
|
409
|
+
│ security-workflows │ 0 │ 5 │ 1 │ 0 │ 0 │ 6 │ today │
|
|
410
|
+
│ github-security-report-action │ 0 │ 0 │ 0 │ 3 │ 1 │ 4 │ 52 days │
|
|
411
|
+
│ dependamerge │ 0 │ 1 │ 0 │ 1 │ 1 │ 3 │ 52 days │
|
|
412
|
+
├───────────────────────────────┼─────┼─────────┼──────┼───────┼───────────┼───────┼─────────┤
|
|
413
|
+
│ Total │ 1 │ 6 │ 1 │ 4 │ 20 │ 32 │ │
|
|
414
|
+
└───────────────────────────────┴─────┴─────────┴──────┴───────┴───────────┴───────┴─────────┘
|
|
415
|
+
… and 11 more
|
|
416
|
+
❌ 16 With open issues
|
|
417
|
+
✅ 87 No open issues
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
That is a real run of `lfreleng-actions` under `top_n: 5`. The totals row sums
|
|
421
|
+
the rows actually **displayed**, matching the offender tables, so it stays
|
|
422
|
+
consistent with a truncated view; `… and 11 more` plus the five listed rows
|
|
423
|
+
reconcile with the `❌ 16` in the footer.
|
|
424
|
+
|
|
425
|
+
Two columns are always present and are not configurable:
|
|
426
|
+
|
|
427
|
+
- **Other** — the issue is labelled, but with nothing you asked about.
|
|
428
|
+
- **Untriaged** — the issue has no labels at all. This is the column to watch:
|
|
429
|
+
an unlabelled issue is one nobody has categorised.
|
|
430
|
+
|
|
431
|
+
Both names are reserved, as are `Repository`, `Total` and `Oldest`: configuring a
|
|
432
|
+
column with one of those names is rejected, because it would either share a
|
|
433
|
+
counter with the implicit column — stopping the class columns summing to `Total`
|
|
434
|
+
— or duplicate a header, which would also make `sort: ["repository"]` resolve to
|
|
435
|
+
a count column instead of the repository name. Column names must additionally be
|
|
436
|
+
non-blank, unpadded, distinct case-insensitively (`sort` matches them that way,
|
|
437
|
+
and strips its terms), and free of `|`, backticks and control characters, which
|
|
438
|
+
would corrupt the Markdown table or Slack code fence they are rendered into.
|
|
439
|
+
|
|
440
|
+
The remaining columns come from `report.issue_labels`, which maps a column name
|
|
441
|
+
to the issue labels that count towards it. An issue counts **once**, under the
|
|
442
|
+
first column whose labels it carries, so the columns always sum to the classified
|
|
443
|
+
total. Matching is case-insensitive on the whole label name, so `docs` does not
|
|
444
|
+
swallow an unrelated `docs-needed`. The default is:
|
|
445
|
+
|
|
446
|
+
```json
|
|
447
|
+
{
|
|
448
|
+
"report": {
|
|
449
|
+
"issue_labels": {
|
|
450
|
+
"Bug": ["bug", "defect"],
|
|
451
|
+
"Feature": ["feature", "enhancement"],
|
|
452
|
+
"Docs": ["documentation", "docs"]
|
|
453
|
+
}
|
|
454
|
+
},
|
|
455
|
+
"organizations": [{ "name": "lfreleng-actions" }]
|
|
456
|
+
}
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
Unlike `ruleset_workflows`, a configured `issue_labels` **replaces** the default
|
|
460
|
+
rather than merging into it — the mapping defines a coherent set of table
|
|
461
|
+
columns, so merging would leave behind default columns you deliberately left out.
|
|
462
|
+
|
|
463
|
+
Repositories with no open issues are counted in the `✅ No open issues` footer
|
|
464
|
+
rather than listed. Rows rank by total open issues, then by Untriaged. Pull
|
|
465
|
+
requests are **not** counted: the GraphQL `issues` connection excludes them.
|
|
466
|
+
|
|
467
|
+
> **Accuracy note.** `Total` is exact at any backlog size, as is `Oldest`
|
|
468
|
+
> wherever an age is shown. The label columns are computed from a bounded,
|
|
469
|
+
> oldest-first window of each repository's open issues (25 issues, 5 labels
|
|
470
|
+
> each) that keeps the query well inside GitHub's GraphQL rate-limit budget. A
|
|
471
|
+
> repository whose label breakdown is partial shows a trailing `+` on its
|
|
472
|
+
> `Oldest` cell. That covers a backlog exceeding the issue window, and any issue
|
|
473
|
+
> whose classification a label beyond the label window could have changed —
|
|
474
|
+
> which is every classification except a match on the *first* configured column,
|
|
475
|
+
> since columns are matched in declaration order and an unseen label could
|
|
476
|
+
> belong to an earlier one. An issue whose labels could not be read at all is
|
|
477
|
+
> left out of the class columns entirely rather than counted as `Untriaged`. An
|
|
478
|
+
> `Oldest` of `unknown` means the oldest issue came back unreadable or undated;
|
|
479
|
+
> it can still carry the `+`.
|
|
480
|
+
|
|
481
|
+
**Permissions.** A fine-grained PAT needs **Issues: read** for this table; a
|
|
482
|
+
classic PAT's `repo` scope already covers it. Without it GitHub serves the query
|
|
483
|
+
with HTTP 200 and this one field null, so affected repositories are reported as
|
|
484
|
+
`❓ Unknown` rather than counted as having no open issues — an unreadable backlog
|
|
485
|
+
is never presented as a clean one.
|
|
486
|
+
|
|
298
487
|
### Organisation feature gating
|
|
299
488
|
|
|
300
489
|
The workflow-driven signals (OpenSSF Scorecard, zizmor, aislop) need
|
|
@@ -45,7 +45,7 @@ keywords = [
|
|
|
45
45
|
]
|
|
46
46
|
dependencies = [
|
|
47
47
|
"httpx[http2]==0.28.1",
|
|
48
|
-
"typer==0.27.
|
|
48
|
+
"typer==0.27.1",
|
|
49
49
|
"rich==15.0.0",
|
|
50
50
|
"jinja2==3.1.6",
|
|
51
51
|
"jsonschema==4.26.0",
|
|
@@ -65,7 +65,7 @@ dev = [
|
|
|
65
65
|
"respx==0.23.1",
|
|
66
66
|
"syrupy==5.5.3",
|
|
67
67
|
"mypy==2.3.0",
|
|
68
|
-
"ruff==0.
|
|
68
|
+
"ruff==0.16.2",
|
|
69
69
|
"types-jsonschema==4.26.0.20260518",
|
|
70
70
|
"types-PyYAML==6.0.12.20250915",
|
|
71
71
|
]
|
|
@@ -97,7 +97,7 @@ dev = [
|
|
|
97
97
|
"respx==0.23.1",
|
|
98
98
|
"syrupy==5.5.3",
|
|
99
99
|
"mypy==2.3.0",
|
|
100
|
-
"ruff==0.
|
|
100
|
+
"ruff==0.16.2",
|
|
101
101
|
"types-jsonschema==4.26.0.20260518",
|
|
102
102
|
"types-PyYAML==6.0.12.20250915",
|
|
103
103
|
]
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '0.
|
|
22
|
-
__version_tuple__ = version_tuple = (0,
|
|
21
|
+
__version__ = version = '0.11.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 11, 0)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -40,6 +40,7 @@ class CategoryKey(str, Enum):
|
|
|
40
40
|
RELEASES = "releases"
|
|
41
41
|
MUTABLE_RELEASES = "mutable_releases"
|
|
42
42
|
PRIVATE_VULNERABILITY_REPORTING = "private_vulnerability_reporting"
|
|
43
|
+
GITHUB_ISSUES = "github_issues"
|
|
43
44
|
|
|
44
45
|
|
|
45
46
|
@dataclass(frozen=True)
|
|
@@ -48,7 +49,10 @@ class CategoryMeta:
|
|
|
48
49
|
|
|
49
50
|
``pass_label`` names the healthy state (e.g. ``"Clean"``, ``"Immutable"``)
|
|
50
51
|
and is what the summary footer reports as ``All <pass_label>`` when nothing
|
|
51
|
-
needs attention.
|
|
52
|
+
needs attention. That collapse wants an adjectival label; a category whose
|
|
53
|
+
counted wording is a noun phrase ("12 No open issues") sets
|
|
54
|
+
``pass_all_label`` to the word that reads correctly after "All" instead.
|
|
55
|
+
``fail_label`` names the actionable state for categories
|
|
52
56
|
with a binary pass/fail axis (enablement, cooldown, mutability, release
|
|
53
57
|
freshness); it is ``None`` for the severity-ranked signals, whose offenders
|
|
54
58
|
are enumerated in the table itself rather than as a single failure count.
|
|
@@ -63,6 +67,9 @@ class CategoryMeta:
|
|
|
63
67
|
fail_label: str | None
|
|
64
68
|
url: str
|
|
65
69
|
description: str = ""
|
|
70
|
+
# Alternative pass wording for the collapsed "All <label>" footer line,
|
|
71
|
+
# when the counted wording would not read grammatically after "All".
|
|
72
|
+
pass_all_label: str | None = None
|
|
66
73
|
# The lowest finding severity that counts as a failure for this category.
|
|
67
74
|
# A repository fails (appears as an offender) only when it carries a finding
|
|
68
75
|
# at or above this rung; findings below it fold into the clean count. The
|
|
@@ -252,6 +259,23 @@ _CATEGORIES: dict[CategoryKey, CategoryMeta] = {
|
|
|
252
259
|
"instead of disclosing them publicly."
|
|
253
260
|
),
|
|
254
261
|
),
|
|
262
|
+
CategoryKey.GITHUB_ISSUES: CategoryMeta(
|
|
263
|
+
key=CategoryKey.GITHUB_ISSUES,
|
|
264
|
+
title="GitHub Issues",
|
|
265
|
+
pass_label="No open issues",
|
|
266
|
+
# "All No open issues" does not parse; the collapsed line reads
|
|
267
|
+
# "All Clean", matching the other categories' vocabulary.
|
|
268
|
+
pass_all_label="Clean",
|
|
269
|
+
fail_label="With open issues",
|
|
270
|
+
url="https://docs.github.com/en/issues",
|
|
271
|
+
description=(
|
|
272
|
+
"Open issues per repository, split by label into the configured "
|
|
273
|
+
"classes. Issues carrying none of the configured labels count as "
|
|
274
|
+
"Other; issues with no labels at all count as Untriaged, which is "
|
|
275
|
+
"the column to watch -- an unlabelled issue has not been triaged. "
|
|
276
|
+
"Ranked by total open issues, then by Untriaged."
|
|
277
|
+
),
|
|
278
|
+
),
|
|
255
279
|
}
|
|
256
280
|
|
|
257
281
|
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
# SPDX-FileCopyrightText: 2026 The Linux Foundation
|
|
3
|
+
"""Command-line entry point.
|
|
4
|
+
|
|
5
|
+
Wires configuration, scope/mode resolution, collection, and rendering into the
|
|
6
|
+
``github-security-report`` command. Org mode produces Pages/Slack/terminal
|
|
7
|
+
output; repo mode is a degraded PR gate emitting a job summary and outputs.
|
|
8
|
+
See ``docs/BRIEF.md`` sections 9-12.
|
|
9
|
+
|
|
10
|
+
The command surface is split across:
|
|
11
|
+
|
|
12
|
+
- :mod:`cli.app` -- the Typer application, options and validation
|
|
13
|
+
- :mod:`cli.modes` -- the org, repo and remediate run modes
|
|
14
|
+
- :mod:`cli.outputs` -- offender limits, category visibility, file writing
|
|
15
|
+
- :mod:`cli.serialise` -- the machine-readable ``report.json`` shape
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from github_security_report.cli.app import (
|
|
21
|
+
_version_callback,
|
|
22
|
+
app,
|
|
23
|
+
main,
|
|
24
|
+
remediate,
|
|
25
|
+
report,
|
|
26
|
+
)
|
|
27
|
+
from github_security_report.cli.modes import (
|
|
28
|
+
OrgRunOptions,
|
|
29
|
+
ReleaseOverrides,
|
|
30
|
+
_abort_network,
|
|
31
|
+
_load_config,
|
|
32
|
+
_run_org,
|
|
33
|
+
_run_remediate,
|
|
34
|
+
_run_repo,
|
|
35
|
+
)
|
|
36
|
+
from github_security_report.cli.outputs import (
|
|
37
|
+
TopNLimits,
|
|
38
|
+
_safe_component,
|
|
39
|
+
repo_outputs,
|
|
40
|
+
write_org_files,
|
|
41
|
+
)
|
|
42
|
+
from github_security_report.cli.serialise import _org_to_dict, _table_to_dict
|
|
43
|
+
|
|
44
|
+
# The package façade: every name previously importable from
|
|
45
|
+
# ``github_security_report.cli`` still is, including the private helpers the
|
|
46
|
+
# tests reach for, so splitting the module changed no caller's import path.
|
|
47
|
+
__all__ = [
|
|
48
|
+
"OrgRunOptions",
|
|
49
|
+
"ReleaseOverrides",
|
|
50
|
+
"TopNLimits",
|
|
51
|
+
"_abort_network",
|
|
52
|
+
"_load_config",
|
|
53
|
+
"_org_to_dict",
|
|
54
|
+
"_run_org",
|
|
55
|
+
"_run_remediate",
|
|
56
|
+
"_run_repo",
|
|
57
|
+
"_safe_component",
|
|
58
|
+
"_table_to_dict",
|
|
59
|
+
"_version_callback",
|
|
60
|
+
"app",
|
|
61
|
+
"main",
|
|
62
|
+
"remediate",
|
|
63
|
+
"repo_outputs",
|
|
64
|
+
"report",
|
|
65
|
+
"write_org_files",
|
|
66
|
+
]
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
# SPDX-FileCopyrightText: 2026 The Linux Foundation
|
|
3
|
+
"""Support ``python -m github_security_report.cli``.
|
|
4
|
+
|
|
5
|
+
The console script declared in ``pyproject.toml`` is the usual entry point, but
|
|
6
|
+
the module form worked while the CLI was a single module and is preserved here:
|
|
7
|
+
a package cannot be executed through the ``if __name__ == "__main__"`` guard in
|
|
8
|
+
its ``__init__``, so it needs this file instead.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from github_security_report.cli.app import app
|
|
14
|
+
|
|
15
|
+
if __name__ == "__main__": # pragma: no cover
|
|
16
|
+
app()
|