github-security-report 0.19.1__tar.gz → 0.20.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.
Files changed (123) hide show
  1. {github_security_report-0.19.1 → github_security_report-0.20.0}/PKG-INFO +341 -29
  2. {github_security_report-0.19.1 → github_security_report-0.20.0}/README.md +340 -28
  3. {github_security_report-0.19.1 → github_security_report-0.20.0}/src/github_security_report/_version.py +2 -2
  4. {github_security_report-0.19.1 → github_security_report-0.20.0}/src/github_security_report/categories/keys.py +4 -0
  5. {github_security_report-0.19.1 → github_security_report-0.20.0}/src/github_security_report/categories/tables.py +102 -3
  6. {github_security_report-0.19.1 → github_security_report-0.20.0}/src/github_security_report/cli/app.py +92 -51
  7. {github_security_report-0.19.1 → github_security_report-0.20.0}/src/github_security_report/cli/boundary.py +23 -0
  8. {github_security_report-0.19.1 → github_security_report-0.20.0}/src/github_security_report/cli/modes.py +132 -167
  9. {github_security_report-0.19.1 → github_security_report-0.20.0}/src/github_security_report/cli/options.py +42 -1
  10. {github_security_report-0.19.1 → github_security_report-0.20.0}/src/github_security_report/cli/outputs.py +44 -5
  11. github_security_report-0.20.0/src/github_security_report/cli/progress.py +356 -0
  12. {github_security_report-0.19.1 → github_security_report-0.20.0}/src/github_security_report/cli/publish.py +4 -0
  13. github_security_report-0.20.0/src/github_security_report/cli/remediation.py +172 -0
  14. {github_security_report-0.19.1 → github_security_report-0.20.0}/src/github_security_report/cli/serialise.py +16 -0
  15. github_security_report-0.20.0/src/github_security_report/cli/summary.py +167 -0
  16. {github_security_report-0.19.1 → github_security_report-0.20.0}/src/github_security_report/client/__init__.py +6 -1
  17. {github_security_report-0.19.1 → github_security_report-0.20.0}/src/github_security_report/client/accounting.py +103 -7
  18. {github_security_report-0.19.1 → github_security_report-0.20.0}/src/github_security_report/client/alerts.py +2 -2
  19. github_security_report-0.20.0/src/github_security_report/client/analyses.py +231 -0
  20. {github_security_report-0.19.1 → github_security_report-0.20.0}/src/github_security_report/client/batch_errors.py +30 -20
  21. github_security_report-0.20.0/src/github_security_report/client/events.py +71 -0
  22. {github_security_report-0.19.1 → github_security_report-0.20.0}/src/github_security_report/client/org_reads.py +15 -54
  23. github_security_report-0.20.0/src/github_security_report/client/paging.py +200 -0
  24. {github_security_report-0.19.1 → github_security_report-0.20.0}/src/github_security_report/client/parsers.py +17 -14
  25. {github_security_report-0.19.1 → github_security_report-0.20.0}/src/github_security_report/client/queries.py +40 -2
  26. {github_security_report-0.19.1 → github_security_report-0.20.0}/src/github_security_report/client/reads.py +11 -122
  27. github_security_report-0.20.0/src/github_security_report/client/repo_parser.py +54 -0
  28. {github_security_report-0.19.1 → github_security_report-0.20.0}/src/github_security_report/client/transport.py +84 -39
  29. github_security_report-0.20.0/src/github_security_report/client/workflow_runs.py +295 -0
  30. {github_security_report-0.19.1 → github_security_report-0.20.0}/src/github_security_report/client/writes.py +7 -6
  31. {github_security_report-0.19.1 → github_security_report-0.20.0}/src/github_security_report/codeql/__init__.py +2 -0
  32. {github_security_report-0.19.1 → github_security_report-0.20.0}/src/github_security_report/codeql/cleanup.py +41 -5
  33. {github_security_report-0.19.1 → github_security_report-0.20.0}/src/github_security_report/codeql/facts.py +24 -8
  34. {github_security_report-0.19.1 → github_security_report-0.20.0}/src/github_security_report/codeql/tables.py +39 -4
  35. {github_security_report-0.19.1 → github_security_report-0.20.0}/src/github_security_report/collect/__init__.py +6 -0
  36. github_security_report-0.20.0/src/github_security_report/collect/context.py +309 -0
  37. {github_security_report-0.19.1 → github_security_report-0.20.0}/src/github_security_report/collect/extras.py +87 -27
  38. github_security_report-0.20.0/src/github_security_report/collect/graph.py +48 -0
  39. {github_security_report-0.19.1 → github_security_report-0.20.0}/src/github_security_report/collect/org.py +65 -25
  40. {github_security_report-0.19.1 → github_security_report-0.20.0}/src/github_security_report/collect/protocols.py +28 -0
  41. github_security_report-0.20.0/src/github_security_report/collect/run_sync.py +319 -0
  42. github_security_report-0.20.0/src/github_security_report/collect/workflow_runs.py +320 -0
  43. {github_security_report-0.19.1 → github_security_report-0.20.0}/src/github_security_report/config/__init__.py +11 -2
  44. github_security_report-0.19.1/src/github_security_report/config/loader.py → github_security_report-0.20.0/src/github_security_report/config/blocks.py +60 -186
  45. github_security_report-0.20.0/src/github_security_report/config/loader.py +213 -0
  46. {github_security_report-0.19.1 → github_security_report-0.20.0}/src/github_security_report/config/models.py +96 -6
  47. {github_security_report-0.19.1 → github_security_report-0.20.0}/src/github_security_report/config/order.py +8 -4
  48. {github_security_report-0.19.1 → github_security_report-0.20.0}/src/github_security_report/config/schema.py +47 -0
  49. github_security_report-0.20.0/src/github_security_report/issue_presentation.py +186 -0
  50. github_security_report-0.20.0/src/github_security_report/issues.py +380 -0
  51. {github_security_report-0.19.1 → github_security_report-0.20.0}/src/github_security_report/layout.py +26 -14
  52. github_security_report-0.20.0/src/github_security_report/malware.py +168 -0
  53. {github_security_report-0.19.1 → github_security_report-0.20.0}/src/github_security_report/models.py +19 -5
  54. {github_security_report-0.19.1 → github_security_report-0.20.0}/src/github_security_report/remediate/__init__.py +9 -1
  55. {github_security_report-0.19.1 → github_security_report-0.20.0}/src/github_security_report/remediate/codeql.py +25 -4
  56. {github_security_report-0.19.1 → github_security_report-0.20.0}/src/github_security_report/remediate/model.py +11 -2
  57. {github_security_report-0.19.1 → github_security_report-0.20.0}/src/github_security_report/render/html.py +19 -1
  58. {github_security_report-0.19.1 → github_security_report-0.20.0}/src/github_security_report/render/markdown.py +33 -2
  59. {github_security_report-0.19.1 → github_security_report-0.20.0}/src/github_security_report/render/slack.py +29 -175
  60. github_security_report-0.20.0/src/github_security_report/render/slack_text.py +221 -0
  61. {github_security_report-0.19.1 → github_security_report-0.20.0}/src/github_security_report/render/terminal.py +23 -12
  62. {github_security_report-0.19.1 → github_security_report-0.20.0}/src/github_security_report/report/__init__.py +4 -0
  63. {github_security_report-0.19.1 → github_security_report-0.20.0}/src/github_security_report/report/aggregate.py +43 -2
  64. {github_security_report-0.19.1 → github_security_report-0.20.0}/src/github_security_report/report/display.py +18 -1
  65. {github_security_report-0.19.1 → github_security_report-0.20.0}/src/github_security_report/report/tables.py +25 -1
  66. {github_security_report-0.19.1 → github_security_report-0.20.0}/src/github_security_report/templates/report.html.j2 +1 -1
  67. github_security_report-0.20.0/src/github_security_report/workflow_runs/__init__.py +74 -0
  68. github_security_report-0.20.0/src/github_security_report/workflow_runs/aggregate.py +405 -0
  69. github_security_report-0.20.0/src/github_security_report/workflow_runs/models.py +184 -0
  70. github_security_report-0.20.0/src/github_security_report/workflow_runs/store.py +340 -0
  71. github_security_report-0.20.0/src/github_security_report/workflow_runs/store_codec.py +116 -0
  72. github_security_report-0.20.0/src/github_security_report/workflow_runs/tables.py +334 -0
  73. github_security_report-0.19.1/src/github_security_report/collect/context.py +0 -85
  74. github_security_report-0.19.1/src/github_security_report/collect/graph.py +0 -106
  75. github_security_report-0.19.1/src/github_security_report/issues.py +0 -370
  76. {github_security_report-0.19.1 → github_security_report-0.20.0}/.gitignore +0 -0
  77. {github_security_report-0.19.1 → github_security_report-0.20.0}/LICENSE +0 -0
  78. {github_security_report-0.19.1 → github_security_report-0.20.0}/LICENSES/Apache-2.0.txt +0 -0
  79. {github_security_report-0.19.1 → github_security_report-0.20.0}/pyproject.toml +0 -0
  80. {github_security_report-0.19.1 → github_security_report-0.20.0}/scripts/README.md +0 -0
  81. {github_security_report-0.19.1 → github_security_report-0.20.0}/src/github_security_report/__init__.py +0 -0
  82. {github_security_report-0.19.1 → github_security_report-0.20.0}/src/github_security_report/authors.py +0 -0
  83. {github_security_report-0.19.1 → github_security_report-0.20.0}/src/github_security_report/categories/__init__.py +0 -0
  84. {github_security_report-0.19.1 → github_security_report-0.20.0}/src/github_security_report/categories/signals.py +0 -0
  85. {github_security_report-0.19.1 → github_security_report-0.20.0}/src/github_security_report/classify.py +0 -0
  86. {github_security_report-0.19.1 → github_security_report-0.20.0}/src/github_security_report/cli/__init__.py +0 -0
  87. {github_security_report-0.19.1 → github_security_report-0.20.0}/src/github_security_report/cli/__main__.py +0 -0
  88. {github_security_report-0.19.1 → github_security_report-0.20.0}/src/github_security_report/client/codeql_parsers.py +0 -0
  89. {github_security_report-0.19.1 → github_security_report-0.20.0}/src/github_security_report/client/copilot.py +0 -0
  90. {github_security_report-0.19.1 → github_security_report-0.20.0}/src/github_security_report/client/endpoints.py +0 -0
  91. {github_security_report-0.19.1 → github_security_report-0.20.0}/src/github_security_report/client/errors.py +0 -0
  92. {github_security_report-0.19.1 → github_security_report-0.20.0}/src/github_security_report/client/failures.py +0 -0
  93. {github_security_report-0.19.1 → github_security_report-0.20.0}/src/github_security_report/client/reviews.py +0 -0
  94. {github_security_report-0.19.1 → github_security_report-0.20.0}/src/github_security_report/code_quality.py +0 -0
  95. {github_security_report-0.19.1 → github_security_report-0.20.0}/src/github_security_report/collect/codeql.py +0 -0
  96. {github_security_report-0.19.1 → github_security_report-0.20.0}/src/github_security_report/collect/repo.py +0 -0
  97. {github_security_report-0.19.1 → github_security_report-0.20.0}/src/github_security_report/collect/scoping.py +0 -0
  98. {github_security_report-0.19.1 → github_security_report-0.20.0}/src/github_security_report/gating.py +0 -0
  99. {github_security_report-0.19.1 → github_security_report-0.20.0}/src/github_security_report/gitctx.py +0 -0
  100. {github_security_report-0.19.1 → github_security_report-0.20.0}/src/github_security_report/ordering.py +0 -0
  101. {github_security_report-0.19.1 → github_security_report-0.20.0}/src/github_security_report/posture/__init__.py +0 -0
  102. {github_security_report-0.19.1 → github_security_report-0.20.0}/src/github_security_report/posture/enablement.py +0 -0
  103. {github_security_report-0.19.1 → github_security_report-0.20.0}/src/github_security_report/posture/facts.py +0 -0
  104. {github_security_report-0.19.1 → github_security_report-0.20.0}/src/github_security_report/posture/quality.py +0 -0
  105. {github_security_report-0.19.1 → github_security_report-0.20.0}/src/github_security_report/posture/releases.py +0 -0
  106. {github_security_report-0.19.1 → github_security_report-0.20.0}/src/github_security_report/pulls/__init__.py +0 -0
  107. {github_security_report-0.19.1 → github_security_report-0.20.0}/src/github_security_report/pulls/columns.py +0 -0
  108. {github_security_report-0.19.1 → github_security_report-0.20.0}/src/github_security_report/pulls/counting.py +0 -0
  109. {github_security_report-0.19.1 → github_security_report-0.20.0}/src/github_security_report/pulls/presentation.py +0 -0
  110. {github_security_report-0.19.1 → github_security_report-0.20.0}/src/github_security_report/pulls/table.py +0 -0
  111. {github_security_report-0.19.1 → github_security_report-0.20.0}/src/github_security_report/py.typed +0 -0
  112. {github_security_report-0.19.1 → github_security_report-0.20.0}/src/github_security_report/ranking.py +0 -0
  113. {github_security_report-0.19.1 → github_security_report-0.20.0}/src/github_security_report/render/__init__.py +0 -0
  114. {github_security_report-0.19.1 → github_security_report-0.20.0}/src/github_security_report/render/slack_limits.py +0 -0
  115. {github_security_report-0.19.1 → github_security_report-0.20.0}/src/github_security_report/render/terminal_notices.py +0 -0
  116. {github_security_report-0.19.1 → github_security_report-0.20.0}/src/github_security_report/report/signals.py +0 -0
  117. {github_security_report-0.19.1 → github_security_report-0.20.0}/src/github_security_report/rulesets.py +0 -0
  118. {github_security_report-0.19.1 → github_security_report-0.20.0}/src/github_security_report/runner.py +0 -0
  119. {github_security_report-0.19.1 → github_security_report-0.20.0}/src/github_security_report/scope.py +0 -0
  120. {github_security_report-0.19.1 → github_security_report-0.20.0}/src/github_security_report/secret_patterns.py +0 -0
  121. {github_security_report-0.19.1 → github_security_report-0.20.0}/src/github_security_report/severity.py +0 -0
  122. {github_security_report-0.19.1 → github_security_report-0.20.0}/src/github_security_report/summary.py +0 -0
  123. {github_security_report-0.19.1 → github_security_report-0.20.0}/src/github_security_report/templates/index.html.j2 +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: github-security-report
3
- Version: 0.19.1
3
+ Version: 0.20.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
@@ -114,11 +114,14 @@ tables (org mode):
114
114
  repository), the language, the last *successful* scan, and the cause:
115
115
  analyses failing (a run that errors still uploads, but its results do not
116
116
  count as a scan), default setup switched off, a workflow removed or
117
- disabled, or an advanced workflow
117
+ disabled, an advanced workflow
118
118
  **superseded** by default setup (GitHub rejects advanced CodeQL uploads
119
- while default setup is on). An *orphaned* configuration will never scan
120
- again; delete it from the tool status page once the live setup covers its
121
- language.
119
+ while default setup is on), or a configuration **superseded within its
120
+ workflow**: the workflow file still runs, but a change to its job id or
121
+ language matrix moved the language to another category, which a current
122
+ configuration from that same file now scans. An *orphaned* configuration
123
+ will never scan again; delete it from the tool status page once the live
124
+ setup covers its language.
122
125
  - **Language Coverage** lists repositories where GitHub detects a
123
126
  CodeQL-supported language that no current configuration scans — typically
124
127
  an advanced workflow whose language matrix omits one the repository
@@ -156,6 +159,28 @@ tables (org mode):
156
159
  window a reviewed dependency update sits in while the vulnerability it fixes
157
160
  stays unpatched. Read from the batched GraphQL prefetch, so it costs no extra
158
161
  request; hide it with the `auto_merge` render toggle.
162
+ - **Dependabot: Malware Findings** — one row per open Dependabot alert whose
163
+ advisory GitHub classifies as
164
+ [malware](https://docs.github.com/en/code-security/concepts/supply-chain-security/malware-alerts)
165
+ rather than a vulnerability: a dependency to remove, not upgrade. Each row
166
+ names the package (linked to the alert), its ecosystem and manifest, the
167
+ severity and the alert's age, most severe and then oldest first. The alerts
168
+ come from the organisation-wide Dependabot sweep the alerts signal already
169
+ makes, so the table costs no extra request. The footer counts repositories:
170
+ a repository with Dependabot alerts off, or whose state could not be read,
171
+ cannot raise one and counts as **Alerts off or unreadable**, never as
172
+ **No findings**. When the sweep is incomplete, the malware it did return is
173
+ still listed and fails its repository; only the repositories with none in
174
+ hand are unknown. GitHub's API does not expose whether the malware alerts
175
+ feature itself is switched on, neither per repository nor in a code-security
176
+ configuration, so a repository without findings has no malicious package
177
+ reported, which does not confirm coverage: the feature may be off. The
178
+ footer says so on every surface, and `remediate` cannot enable it. To switch
179
+ it on for every repository now, create or edit a custom security
180
+ configuration in the organisation's settings with *Dependabot → Malware
181
+ alerts: Enabled* and apply it to all repositories. The report cannot verify
182
+ the result. See [ADR-0008](docs/adr/0008-dependabot-malware-alerts.md); hide
183
+ the table with the `malware_findings` render toggle.
159
184
  - **Code Quality** — repositories where
160
185
  [GitHub Code Quality](https://docs.github.com/en/code-security/concepts/code-quality/code-quality)
161
186
  is **Disabled**, or **Partial**: configured, but without a language the
@@ -176,6 +201,43 @@ tables (org mode):
176
201
  `python 30/32 · go 5/5 · javascript-typescript 10/12`. See
177
202
  [ADR-0006](docs/adr/0006-github-code-quality.md); hide it with the
178
203
  `code_quality` render toggle.
204
+ - **Workflow Failures** — repositories whose GitHub Actions workflows failed
205
+ in the window, ranked by failed runs. The motivating case is the bulk
206
+ Dependabot merge whose integration tests need credentials, so they run
207
+ only once merged and never fail on the pull request; the table exists to
208
+ show which workflows fail, how often, and (in the detail table) whether a
209
+ pull request could have caught them. Each row gives **Failed** (runs that
210
+ concluded `failure`, `startup_failure`, `timed_out` or `action_required`),
211
+ **Runs** (every concluded run of every workflow in the repository),
212
+ **Fail %** (failed over failed *plus passed*, so skips never dilute it),
213
+ **Skipped** (skipped, cancelled, neutral and stale runs — a cancelled run
214
+ reached no verdict, and a bulk-merge round cancels hundreds of superseded
215
+ ones), **Retried** (runs that passed on a later attempt, a direct sign of
216
+ flakiness), **Workflows** (how many distinct workflows failed) and **Worst
217
+ workflow** (the one with the most failures, linked to its run history).
218
+ GitHub-managed workflows with no file in the repository to fix (Dependabot
219
+ updates, default-setup CodeQL, Copilot review) are left out unless
220
+ `workflow_runs.include_dynamic` is set. There is one table per time
221
+ window: seven days on the terminal and in Slack, and 7, 30 and 90 days on
222
+ GitHub Pages, where the later windows nest beneath the first. A repository
223
+ whose run listing could not be read this run (a token without Actions
224
+ read, a `5xx`) counts as **Unreadable**, never as failure-free, and a
225
+ footer note says when a window reaches further back than the stored
226
+ history does. The run history behind the table lives in a per-user
227
+ **run store**, so the first report against an organisation backfills the
228
+ longest window once and every later one asks GitHub only for what is new
229
+ — an unchanged repository costs a single conditional request that GitHub
230
+ answers `304` and does not charge. See
231
+ [Workflow run windows and store](#workflow-run-windows-and-store) and
232
+ [ADR-0007](docs/adr/0007-workflow-failures.md); hide it with the
233
+ `workflow_failures` render toggle. A per-workflow **detail** table
234
+ (`workflow_failures_detail`: workflow, file, failing job linked to its
235
+ newest log, pattern *persistent*/*intermittent*/*insufficient*, failing
236
+ triggers, and a **Reading** of *post-merge only*, *no PR runs*,
237
+ *everywhere* or *other*) is collected on every run but rendered nowhere
238
+ until `categories.workflow_failures_detail.enabled: true`; it stays off
239
+ Slack even then, being too wide for a chat digest, and is absent from
240
+ `report.json` while disabled.
179
241
 
180
242
  The four enablement categories count the repositories with and without the
181
243
  feature (plus any whose state could not be read), and by default name whichever
@@ -251,7 +313,7 @@ organisation and **Repository access** set to *All repositories*, then grant:
251
313
  | Secret scanning alerts | Open secret-scanning alerts, across every GitHub pattern category |
252
314
  | Issues | Open issues and their labels (GitHub Issues table) |
253
315
  | Administration | Dependabot enablement + security-updates status, and effective branch rules. *Optional* **write** reads the Code Quality setup, which GitHub gates behind write |
254
- | Actions | Workflow state behind a stale CodeQL configuration |
316
+ | Actions | Workflow state behind a stale CodeQL configuration, and the workflow run history behind the Workflow Failures tables (the run listing, single-run re-reads, and the GraphQL read of a failed run's check suite) |
255
317
 
256
318
  **Organization permissions:**
257
319
 
@@ -271,7 +333,7 @@ organisations. Grant these scopes:
271
333
 
272
334
  | Scope | Used for |
273
335
  | ----- | -------- |
274
- | `repo` | Repository data, including private repositories |
336
+ | `repo` | Repository data, including private repositories, and the workflow run history behind the Workflow Failures tables |
275
337
  | `security_events` | Code scanning, secret scanning, and Dependabot alerts (org-bulk and per-repo) |
276
338
  | `read:org` | Listing organisation repositories, and GitHub's secret-scanning pattern inventory (see below) |
277
339
  | `admin:org` | *Optional* — reading organisation rulesets for ruleset-based tool coverage. GitHub gates `GET /orgs/{org}/rulesets` behind the full `admin:org` scope; `read:org` and `write:org` return 404. Omit it to skip that one signal; everything else is unaffected. |
@@ -377,6 +439,9 @@ environment-variable name, never embedded.
377
439
  "issue_age_error_days": 60,
378
440
  "graph_batch": 10,
379
441
  "concurrency": 9,
442
+ "workflow_runs": {
443
+ "windows": { "cli": [7], "report": [7, 30, 90], "slack": [7] }
444
+ },
380
445
  "order": { "style": "auto" }
381
446
  },
382
447
  "organizations": [
@@ -492,8 +557,10 @@ Pages output. The valid category keys are: `codeql`,
492
557
  `zizmor`, `aislop`, `dependabot_alerts`, `secret_scanning`,
493
558
  `dependabot_alerts_enabled`, `dependabot_updates_enabled`,
494
559
  `dependabot_cooldown`, `releases`, `mutable_releases`,
495
- `private_vulnerability_reporting`, `auto_merge`, `code_quality`,
496
- `github_issues`, `pull_requests`, `pull_requests_assigned`. Like the other
560
+ `private_vulnerability_reporting`, `auto_merge`, `malware_findings`,
561
+ `code_quality`, `workflow_failures`, `workflow_failures_detail`, `github_issues`,
562
+ `github_issues_assigned`, `pull_requests`, `pull_requests_assigned`. Like the
563
+ other
497
564
  `report` settings, `categories` can be set globally and overridden per
498
565
  organisation (overrides merge key-by-key, so flipping one output leaves the
499
566
  rest untouched). The
@@ -695,9 +762,9 @@ By default the report sorts its sections into three bands:
695
762
 
696
763
  | Band | Contains | Why |
697
764
  | ---- | -------- | --- |
698
- | **Priority** | Secret Scanning, Dependabot: Security Alerts, CodeQL, Mutable Releases | Findings worth acting on today |
765
+ | **Priority** | Secret Scanning, Dependabot: Malware Findings, Dependabot: Security Alerts, CodeQL, Mutable Releases | Findings worth acting on today |
699
766
  | **Middle** | Everything else | Neither urgent nor constant |
700
- | **BAU** | OpenSSF Scorecard, GitHub Issues, Pull Requests, Assigned to Me | Carry data every run, so none of it is news |
767
+ | **BAU** | OpenSSF Scorecard, GitHub Issues, GitHub Issues: Assigned to Me, Pull Requests, Pull Requests: Assigned to Me | Carry data every run, so none of it is news |
701
768
 
702
769
  The bands are not fixed slots. **A band member with nothing to report moves
703
770
  into the middle**: a clean priority category is noise at the top of a page, and
@@ -771,6 +838,8 @@ cooldown) are not independently placeable. They render beneath the Dependabot
771
838
  Alerts signal on every surface and travel with it, since three near-identical
772
839
  headings adrift in the report would not say which signal they qualified.
773
840
  Naming one in an ordering list is rejected rather than accepted and ignored.
841
+ The later windows of the Workflow Failures tables (30 and 90 days on GitHub
842
+ Pages) nest beneath the 7-day table in the same way and share its position.
774
843
 
775
844
  ### GitHub Issues
776
845
 
@@ -1154,7 +1223,7 @@ scheduled run publishes this to Pages, whatever token it holds:
1154
1223
  This is not a configurable toggle. A published `Mine` is not a preference but a
1155
1224
  false statement, so there is nothing to opt into.
1156
1225
 
1157
- ### Assigned to Me
1226
+ ### Pull Requests: Assigned to Me
1158
1227
 
1159
1228
  The `pull_requests_assigned` category repeats the Pull Requests table narrowed
1160
1229
  to pull requests assigned to the account the report ran as — the same columns
@@ -1194,6 +1263,35 @@ name the category as well: `--hide` outranks this toggle and cannot be
1194
1263
  countermanded by it. The input is empty by default, so this only matters if you
1195
1264
  have set it.
1196
1265
 
1266
+ ### GitHub Issues: Assigned to Me
1267
+
1268
+ The `github_issues_assigned` category does the same for issues: the GitHub
1269
+ Issues table narrowed to the open issues assigned to the account the report ran
1270
+ as, with the same columns. Everything above applies unchanged: "mine" follows
1271
+ the token, not a configured name; a bot or App token leaves the section absent;
1272
+ and it is **terminal-only by default**, so publishing it takes the same
1273
+ deliberate opt-in:
1274
+
1275
+ ```json
1276
+ {
1277
+ "report": {
1278
+ "categories": {
1279
+ "github_issues_assigned": { "outputs": { "html": true } }
1280
+ }
1281
+ },
1282
+ "organizations": [{ "name": "lfreleng-actions" }]
1283
+ }
1284
+ ```
1285
+
1286
+ The table reads the same window of each repository's oldest 25 open issues as
1287
+ GitHub Issues, so an issue assigned to you beyond that window, in a repository
1288
+ with a larger backlog, is not counted. Total is therefore the count of your
1289
+ issues *within* the window rather than GitHub's exact total, and a repository
1290
+ whose backlog exceeds the window with none of yours inside it counts as unknown
1291
+ rather than clean. `Oldest` reads `unknown` when an issue the run could not read
1292
+ precedes your oldest one in the window, since the unread issue may be older and
1293
+ yours.
1294
+
1197
1295
  ### Hiding a category for one run
1198
1296
 
1199
1297
  `--hide <category>` (repeatable) suppresses a category on **every** output for
@@ -1212,9 +1310,10 @@ The Action exposes the same control through a `hide` input:
1212
1310
  ```
1213
1311
 
1214
1312
  It is **empty by default**, and does not need setting to keep the personal
1215
- queue out of shared artifacts — `pull_requests_assigned` already defaults to the
1216
- terminal only, so Pages, Markdown and Slack never render it, and `report.json`
1217
- omits it for the same reason (see below). Leaving the input empty also keeps the
1313
+ queues out of shared artifacts — `pull_requests_assigned` and
1314
+ `github_issues_assigned` already default to the terminal only, so Pages,
1315
+ Markdown and Slack never render them, and `report.json` omits them for the same
1316
+ reason (see below). Leaving the input empty also keeps the
1218
1317
  Action compatible with whichever published version the runtime pin installs: a
1219
1318
  non-empty default would be passed to releases predating the flag, which reject
1220
1319
  it outright.
@@ -1463,13 +1562,20 @@ one line of what the run cost, for example
1463
1562
  <!-- markdownlint-disable MD013 -->
1464
1563
 
1465
1564
  ```text
1466
- GitHub API for lfreleng-actions: 1402 requests (0 retries), 822 core points, 64 graphql points, 41.3 MiB received, 102.5 s
1565
+ GitHub API for lfreleng-actions: 1402 requests (REST 1338, GraphQL 64; 0 retries; 0 not modified), 822 core points, 64 graphql points, 0 rate-limit backoffs (0 s), 41.3 MiB received, 102.5 s
1467
1566
  ```
1468
1567
 
1469
1568
  <!-- markdownlint-enable MD013 -->
1470
1569
 
1471
1570
  The request count is every request the tool issued, including retries and
1472
- attempts that never got an answer. The points come from GitHub's own
1571
+ attempts that never got an answer; the split names what was asked (a REST
1572
+ endpoint, the GraphQL endpoint, or the external Scorecard API), and `not
1573
+ modified` is how many conditional reads the run store made that GitHub
1574
+ answered `304` at no cost. The backoffs are the pauses GitHub's rate limiting
1575
+ forced, with the seconds slept for them. The same figures appear in the run
1576
+ summary printed before each report (see
1577
+ [Progress and run summary](#progress-and-run-summary)). The points come from
1578
+ GitHub's own
1473
1579
  `x-ratelimit-*` response headers rather than from counting: GitHub meters a
1474
1580
  token across more than one bucket per resource (alike requests seconds apart
1475
1581
  can carry different `x-ratelimit-reset` values with independent counters), so
@@ -1481,6 +1587,150 @@ bill. Nothing of this line reaches the rendered report or the step summary.
1481
1587
  > As with `graph_batch`, an empty input is not passed to the tool, so set
1482
1588
  > `GSR_CONCURRENCY` only once the pinned release supports `--concurrency`.
1483
1589
 
1590
+ ### Workflow run windows and store
1591
+
1592
+ The Workflow Failures tables are the one part of the report built from
1593
+ history rather than from a repository's current state, so they have their
1594
+ own block, `report.workflow_runs`, settable globally or per organisation
1595
+ like every other `report` key. Every key with its default:
1596
+
1597
+ ```json
1598
+ {
1599
+ "report": {
1600
+ "workflow_runs": {
1601
+ "windows": { "cli": [7], "report": [7, 30, 90], "slack": [7] },
1602
+ "retention_days": 90,
1603
+ "backfill_days": 90,
1604
+ "include_dynamic": false,
1605
+ "exclude_workflows": [],
1606
+ "collapse_matrix": true,
1607
+ "persistent_threshold": 0.9,
1608
+ "min_runs": 3,
1609
+ "cache": true
1610
+ }
1611
+ },
1612
+ "organizations": [{ "name": "lfreleng-actions" }]
1613
+ }
1614
+ ```
1615
+
1616
+ `windows` maps each surface to the windows it shows, in days: `cli` is the
1617
+ terminal, `report` the GitHub Pages Markdown and HTML, and `slack` the
1618
+ digest. The terminal and Slack get the week; the published report adds the
1619
+ month and the quarter, where a reader has room to compare them. A table is
1620
+ built for every window any surface names, titled with its window (`Workflow
1621
+ Failures (30 days)`), and each surface draws its own. The map merges **per
1622
+ surface**, so an organisation that narrows the Slack windows keeps the
1623
+ inherited `report` ones; a list replaces the inherited list for that surface
1624
+ rather than adding to it.
1625
+
1626
+ `retention_days` is how long the run store keeps a record and
1627
+ `backfill_days` how far back a repository never seen before is read. Both
1628
+ default to the **longest configured window** (90 days with the default
1629
+ windows), which is the most any table can use; `backfill_days` is bounded by
1630
+ the retention. A `retention_days` shorter than the longest window is
1631
+ **rejected** at load time rather than accepted: the store would prune
1632
+ records a table still shows, and the window would quietly cover less than
1633
+ its title claims.
1634
+
1635
+ `include_dynamic` admits the GitHub-managed workflows (Dependabot updates,
1636
+ default-setup CodeQL, Copilot review), whose runs carry a `dynamic/` path
1637
+ and no file in the repository; they are left out by default because nobody
1638
+ can edit them. `exclude_workflows` is a list of globs matched against the
1639
+ workflow path (`.github/workflows/experimental-*.yaml`) for workflows whose
1640
+ failures are expected, such as a deliberately failing canary.
1641
+ `collapse_matrix` folds matrix legs (`Build (ubuntu-22.04)` and
1642
+ `Build (ubuntu-24.04)`) into one job name in the detail table.
1643
+ `persistent_threshold` and `min_runs` feed the detail table's **Pattern**
1644
+ column: a workflow failing at or above the threshold is *persistent* (fix
1645
+ the root cause), below it *intermittent* (harden the workflow), and with
1646
+ fewer than `min_runs` runs that reached a verdict *insufficient* to say. Both
1647
+ judge the same pass-or-fail population that Fail % does, so skips count
1648
+ towards neither.
1649
+
1650
+ `cache` and `cache_dir` govern the **run store**, a SQLite file per
1651
+ organisation at
1652
+
1653
+ ```text
1654
+ $XDG_CACHE_HOME/github-security-report/<api-host>/<org>/runs.sqlite
1655
+ # or, when XDG_CACHE_HOME is unset:
1656
+ ~/.cache/github-security-report/<api-host>/<org>/runs.sqlite
1657
+ ```
1658
+
1659
+ keyed by API host as well as organisation, since the same name can exist on
1660
+ github.com and on an enterprise instance. The store is a cache, never a
1661
+ source of truth: everything in it can be rebuilt from GitHub, a schema
1662
+ change between releases drops and recreates it rather than migrating it,
1663
+ and deleting the file costs exactly one backfill. `cache: false` keeps the
1664
+ store in memory for the run alone, so every window is re-read from GitHub
1665
+ and nothing is left behind; `cache_dir` relocates it.
1666
+
1667
+ Four flags adjust this for one run, each mirrored as an action input:
1668
+
1669
+ <!-- markdownlint-disable MD013 -->
1670
+
1671
+ | Flag | Input | Effect |
1672
+ | ---- | ----- | ------ |
1673
+ | `--run-windows 7,30,90` | `run_windows` | Replaces the windows of **every** surface with one list, the shape a one-off investigation wants ("show me 14 days") |
1674
+ | `--no-run-cache` | `no_run_cache` | Keeps the store in memory for this run (`cache: false`) |
1675
+ | `--run-cache-dir PATH` | `run_cache_dir` | Relocates the store (`cache_dir`) |
1676
+ | `--refresh-runs` | `refresh_runs` | Ignores what the store holds and re-reads every repository for the longest window, at the cost of one backfill |
1677
+
1678
+ <!-- markdownlint-enable MD013 -->
1679
+
1680
+ Repo mode reads one repository's runs afresh each time into an in-memory
1681
+ store, and reads the `cli` windows alone (a week, by default) rather than the
1682
+ published report's quarter: a pull request's CI or a developer's shell should
1683
+ leave nothing behind, and a 90-day backfill for one table nobody publishes
1684
+ from that mode would be waste. `--run-windows` applies there; the three store
1685
+ flags are refused rather than accepted and ignored.
1686
+
1687
+ In GitHub Actions the runner's home directory is discarded with the job, so
1688
+ the bundled `reporting.yaml` keeps the store in the Actions cache between
1689
+ runs, under a key unique to the run (`gsr-runs-${{ github.run_id }}`) with a
1690
+ `gsr-runs-` restore prefix that picks up the newest earlier store. A daily
1691
+ schedule keeps the entry inside GitHub's seven-day cache eviction. The save
1692
+ runs whatever the report step's outcome, because the store is written in
1693
+ transactions: a run that aborted part-way still leaves it consistent, and
1694
+ the repositories it did sync need not be re-read.
1695
+
1696
+ The store is cached **encrypted**. It names every in-scope repository's
1697
+ branches, workflow files and failing jobs, private repositories included
1698
+ when the token can read them, and GitHub lets a pull request from a fork
1699
+ restore any cache saved on the default branch, which the `production`
1700
+ environment's protection rules do not cover. The workflow therefore
1701
+ encrypts the store with `openssl enc -aes-256-cbc -pbkdf2` under a key held
1702
+ in the environment's secret `GSR_RUN_STORE_KEY` (any long random string;
1703
+ `openssl rand -base64 48` makes one) before saving, and decrypts it after
1704
+ restoring. Without the secret the store is neither restored nor saved, a
1705
+ notice says so, and each run reads its windows from GitHub again — correct,
1706
+ only slower and dearer. A store that fails to decrypt, after a key rotation,
1707
+ is discarded with a warning and rebuilt. A caller's own workflow needs the
1708
+ same steps, or every scheduled run pays the backfill.
1709
+
1710
+ The point of the store is that repeated runs are nearly free. Each sync
1711
+ presents the listing's ETag, so an unchanged repository answers `304`, which
1712
+ GitHub does not count against the rate limit; a changed one is read newest
1713
+ first down to the last run already held. Re-runs keep their original
1714
+ timestamp and so never cross that watermark, which is why each sync also
1715
+ re-reads the window's failure list (conditionally, with its own ETag) when
1716
+ the listing moved and re-reads any stored failure missing from it — a run
1717
+ leaves that list when a retry passes, and the table's **Retried** column is
1718
+ built from exactly those. Runs stored while still in progress are re-read
1719
+ until they conclude, at most fifty single-run reads per repository per sync.
1720
+ The failing jobs of each failed run come from one GraphQL query per fifty
1721
+ runs, costing a point apiece.
1722
+
1723
+ Measured on `lfreleng-actions`, warm runs in the same hour: the full report
1724
+ cost 1,830 requests, 1,558 core rate-limit points and 116 s before this
1725
+ work; 1,130 requests, 849 points and 95 s once the code-scanning and
1726
+ repository-feature probes were consolidated onto pages the run already
1727
+ read; and 1,136 requests, 838 points and 82 s once the collection phases
1728
+ overlapped. The run store adds about one request per repository to a warm
1729
+ run, mostly free `304`s. A cold 90-day backfill, the first run against an
1730
+ organisation or any run without the cache, is about 1,500 requests, once;
1731
+ `--refresh-runs` and deleting the store each cost the same again. The store
1732
+ is about 25 MB for a 140-repository organisation.
1733
+
1484
1734
  ### Pass/fail severity cutoff
1485
1735
 
1486
1736
  The severity-ranked signals (CodeQL, Scorecard, Zizmor, aislop, Dependabot
@@ -1560,6 +1810,11 @@ and the Slack **bot token** is consumed by the workflow, not the CLI.
1560
1810
  | `hide` | No | `""` | Category keys to suppress on every output (space- or comma-separated). Overrides config, and is one-way: it cannot re-enable a disabled category |
1561
1811
  | `graph_batch` | No | `""` | Repositories per batched GraphQL prefetch query (minimum `1`; default: config, else `10`). Pass a repository or organisation variable such as `${{ vars.GSR_GRAPH_BATCH }}` to adjust it from settings. A batch that still fails is halved automatically, so this sets the starting size (see [GraphQL prefetch batch size](#graphql-prefetch-batch-size)) |
1562
1812
  | `concurrency` | No | `""` | Concurrent GitHub API requests per organisation (`1` to `32`; default: config, else `9`). Pass a repository or organisation variable such as `${{ vars.GSR_CONCURRENCY }}` to adjust it from settings (see [Client concurrency](#client-concurrency)) |
1813
+ | `run_windows` | No | `""` | Time windows, in days, for the Workflow Failures tables on every output (comma-separated, e.g. `7,30,90`; default: config, else `7` on the terminal and Slack and `7,30,90` on GitHub Pages; see [Workflow run windows and store](#workflow-run-windows-and-store)) |
1814
+ | `run_cache_dir` | No | `""` | Directory for the workflow-run store (default: `$XDG_CACHE_HOME/github-security-report`, i.e. `~/.cache/github-security-report` on a runner). Restore and save it with `actions/cache`, as the bundled `reporting.yaml` does, so each scheduled run reads only what is new |
1815
+ | `refresh_runs` | No | `false` | Re-read every repository's workflow runs for the longest window, ignoring what the run store already holds |
1816
+ | `no_run_cache` | No | `false` | Keep the workflow-run store in memory for this run instead of on disk, so every window is re-read from GitHub and nothing is left behind |
1817
+ | `no_progress` | No | `false` | Do not show the live progress line while collecting. The action runs under `CI`, where the line is already off; set this when running the CLI from the action's source on a terminal (see [Progress and run summary](#progress-and-run-summary)) |
1563
1818
  | `tool_version` | No | `""` | Published PyPI version to install. Empty (the default) uses the Dependabot-managed pin in `.github/runtime-pin/requirements.txt`; set a specific version to override. Ignored on pull requests or when `use_local_source` is `true` (both run from source) |
1564
1819
  | `use_local_source` | No | `false` | Run from the checked-out source instead of PyPI (for testing unreleased code from any event) |
1565
1820
 
@@ -1587,6 +1842,59 @@ uvx github-security-report report
1587
1842
  uvx github-security-report report --org lfreleng-actions
1588
1843
  ```
1589
1844
 
1845
+ An org-mode run keeps its workflow-run store under
1846
+ `$XDG_CACHE_HOME/github-security-report` (`~/.cache/github-security-report`
1847
+ by default), so the first run against an organisation is slower than the
1848
+ rest; see [Workflow run windows and store](#workflow-run-windows-and-store).
1849
+ Repo mode leaves nothing behind.
1850
+
1851
+ ### Progress and run summary
1852
+
1853
+ On an interactive terminal an org-mode run keeps one live line at the bottom
1854
+ of the screen while it collects: the organisation, the phase it is in, the
1855
+ repository count where the phase works per repository, the REST and GraphQL
1856
+ request counts with how many are in flight, and the elapsed time. A second
1857
+ line appears while GitHub has the run paused for a rate limit, and while the
1858
+ workflow-run store is being built for the first time, since that one-off
1859
+ dominates a cold run. Warnings logged in the meantime are held and replayed,
1860
+ in order, once the line comes down.
1861
+
1862
+ <!-- markdownlint-disable MD013 -->
1863
+
1864
+ ```text
1865
+ ⠧ lfreleng-actions · probing repositories · 41/120 repos · REST 812 (9 in flight) · GraphQL 24 · 38.2 s
1866
+ ⏳ rate limited by GitHub (core); resuming in 42 s
1867
+ ```
1868
+
1869
+ <!-- markdownlint-enable MD013 -->
1870
+
1871
+ Before each organisation's report, a dim three-line summary says what the
1872
+ run cost and what the store holds, so a slow run explains itself without
1873
+ `--verbose`:
1874
+
1875
+ <!-- markdownlint-disable MD013 -->
1876
+
1877
+ ```text
1878
+ Run summary for lfreleng-actions
1879
+ requests: 1 234 requests (REST 1 100 · GraphQL 120 · external 14 · 300 not modified) · 12.3 MiB · 95.2 s
1880
+ rate limit: 980 core points · 230 graphql points · 0 backoffs
1881
+ workflow-run store: 4.2 MiB · 12 340 runs · 180 repositories · this run: 180 synced, 160 unchanged, 340 written
1882
+ ```
1883
+
1884
+ <!-- markdownlint-enable MD013 -->
1885
+
1886
+ The rate-limit line turns yellow when a backoff happened or a resource's
1887
+ remaining budget fell under a tenth of its limit. A first-time build of the
1888
+ store on disk is marked `built (first run)`; a run with `--no-run-cache`
1889
+ reads `in memory (not cached)` and, since its store is empty every time, is
1890
+ never a first run. The summary is for the terminal only: nothing of it
1891
+ reaches the Markdown, HTML, JSON or Slack output.
1892
+
1893
+ The live line is off under `--no-progress`, when `CI` is set, when stdout is
1894
+ not a terminal, and with `--verbose`, whose log lines would fight it for the
1895
+ screen. The summary prints in every case. The same figures reach the log at
1896
+ `INFO` under `--verbose`.
1897
+
1590
1898
  ### GitHub Enterprise Server
1591
1899
 
1592
1900
  The API endpoints honour the standard environment variables that GitHub
@@ -1696,7 +2004,10 @@ endpoint:
1696
2004
  | `codeql_stale_configurations` | Stale CodeQL cleanup (named only; deletes) |
1697
2005
 
1698
2006
  Qualitative findings (Scorecard, zizmor, open Dependabot alerts, cooldown,
1699
- release freshness/mutability) are reported but not auto-remediated. Remediation
2007
+ release freshness/mutability) are reported but not auto-remediated. Dependabot
2008
+ malware alerts are not remediable either: GitHub exposes no API that reads or
2009
+ switches on the feature, so `remediate` leaves it out until one exists (see
2010
+ [ADR-0008](docs/adr/0008-dependabot-malware-alerts.md)). Remediation
1700
2011
  is organisation-scoped (`--scope org`, the default and only supported scope).
1701
2012
 
1702
2013
  The last two run **only when named** with `--category`, and the no-argument
@@ -1752,9 +2063,9 @@ cause:
1752
2063
 
1753
2064
  | Cause | Action |
1754
2065
  | ----- | ------ |
1755
- | Default setup disabled; workflow removed; superseded by default setup; language removed from default setup | **Delete** the configuration: it can never upload again |
2066
+ | Default setup disabled; workflow removed; superseded by default setup; superseded within its workflow; language removed from default setup | **Delete** the configuration: it can never upload again |
1756
2067
  | Workflow disabled after inactivity | **Re-enable** the workflow, restoring the scan |
1757
- | Analyses failing, default setup changing state or not uploading, uploaded outside GitHub Actions, workflow disabled by hand, active but not uploading, or state unreadable | Reported only; needs a person |
2068
+ | Analyses failing, default setup changing state or not uploading, uploaded outside GitHub Actions, workflow disabled by hand, active but not uploading, or state unreadable | Listed as skipped, with its cause; needs a person |
1758
2069
 
1759
2070
  <!-- markdownlint-enable MD013 -->
1760
2071
 
@@ -1773,15 +2084,16 @@ when:
1773
2084
  blocks nothing;
1774
2085
  - the repository's open alerts cannot be read.
1775
2086
 
1776
- Refusals are listed beside the work done and do not fail the run. Deletion is
1777
- paced at one request a second across the whole run, as GitHub asks of
1778
- mutating requests, and is resumable: an interrupted run's next pass picks up
1779
- the remaining analyses. GitHub answers requests it will not authorise with
1780
- `404`, so a deletion's `404` counts as already done only once the analysis
1781
- also reads back as gone; a token that may not delete fails instead of
1782
- reporting a cleanup that never happened. The token needs the classic `repo`
1783
- scope. See [ADR-0005](docs/adr/0005-codeql-configuration-cleanup.md) for the
1784
- reasoning.
2087
+ Refusals are listed beside the work done and do not fail the run. So is every
2088
+ row left for a person: each is listed as skipped, with its cause, so an
2089
+ operator can see what remains. Deletion is paced at one request a second
2090
+ across the whole run, as GitHub asks of mutating requests, and is resumable:
2091
+ an interrupted run's next pass picks up the remaining analyses. GitHub answers
2092
+ requests it will not authorise with `404`, so a deletion's `404` counts as
2093
+ already done only once the analysis also reads back as gone; a token that may
2094
+ not delete fails instead of reporting a cleanup that never happened. The token
2095
+ needs the classic `repo` scope. See
2096
+ [ADR-0005](docs/adr/0005-codeql-configuration-cleanup.md) for the reasoning.
1785
2097
 
1786
2098
  ## Bulk Remediation Scripts
1787
2099