github-security-report 0.22.0__tar.gz → 0.23.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.
Files changed (120) hide show
  1. github_security_report-0.22.0/README.md → github_security_report-0.23.1/PKG-INFO +95 -6
  2. github_security_report-0.22.0/PKG-INFO → github_security_report-0.23.1/README.md +51 -50
  3. {github_security_report-0.22.0 → github_security_report-0.23.1}/pyproject.toml +6 -5
  4. {github_security_report-0.22.0 → github_security_report-0.23.1}/src/github_security_report/_version.py +2 -2
  5. {github_security_report-0.22.0 → github_security_report-0.23.1}/src/github_security_report/categories/__init__.py +10 -0
  6. {github_security_report-0.22.0 → github_security_report-0.23.1}/src/github_security_report/cli/modes.py +2 -2
  7. {github_security_report-0.22.0 → github_security_report-0.23.1}/src/github_security_report/cli/remediation.py +1 -1
  8. {github_security_report-0.22.0 → github_security_report-0.23.1}/src/github_security_report/cli/serialise.py +9 -0
  9. {github_security_report-0.22.0 → github_security_report-0.23.1}/src/github_security_report/client/failures.py +1 -1
  10. {github_security_report-0.22.0 → github_security_report-0.23.1}/src/github_security_report/client/parsers.py +2 -2
  11. {github_security_report-0.22.0 → github_security_report-0.23.1}/src/github_security_report/codeql/cleanup.py +11 -2
  12. {github_security_report-0.22.0 → github_security_report-0.23.1}/src/github_security_report/codeql/facts.py +20 -4
  13. {github_security_report-0.22.0 → github_security_report-0.23.1}/src/github_security_report/codeql/tables.py +14 -8
  14. {github_security_report-0.22.0 → github_security_report-0.23.1}/src/github_security_report/collect/extras.py +30 -3
  15. {github_security_report-0.22.0 → github_security_report-0.23.1}/src/github_security_report/collect/run_sync.py +1 -1
  16. {github_security_report-0.22.0 → github_security_report-0.23.1}/src/github_security_report/config/blocks.py +27 -0
  17. {github_security_report-0.22.0 → github_security_report-0.23.1}/src/github_security_report/config/models.py +17 -1
  18. {github_security_report-0.22.0 → github_security_report-0.23.1}/src/github_security_report/config/schema.py +13 -2
  19. {github_security_report-0.22.0 → github_security_report-0.23.1}/src/github_security_report/malware.py +1 -1
  20. {github_security_report-0.22.0 → github_security_report-0.23.1}/src/github_security_report/posture/releases.py +1 -1
  21. {github_security_report-0.22.0 → github_security_report-0.23.1}/src/github_security_report/report/aggregate.py +6 -6
  22. {github_security_report-0.22.0 → github_security_report-0.23.1}/src/github_security_report/workflow_runs/store_codec.py +1 -1
  23. {github_security_report-0.22.0 → github_security_report-0.23.1}/.gitignore +0 -0
  24. {github_security_report-0.22.0 → github_security_report-0.23.1}/LICENSE +0 -0
  25. {github_security_report-0.22.0 → github_security_report-0.23.1}/LICENSES/Apache-2.0.txt +0 -0
  26. {github_security_report-0.22.0 → github_security_report-0.23.1}/scripts/README.md +0 -0
  27. {github_security_report-0.22.0 → github_security_report-0.23.1}/src/github_security_report/__init__.py +0 -0
  28. {github_security_report-0.22.0 → github_security_report-0.23.1}/src/github_security_report/authors.py +0 -0
  29. {github_security_report-0.22.0 → github_security_report-0.23.1}/src/github_security_report/categories/keys.py +0 -0
  30. {github_security_report-0.22.0 → github_security_report-0.23.1}/src/github_security_report/categories/signals.py +0 -0
  31. {github_security_report-0.22.0 → github_security_report-0.23.1}/src/github_security_report/categories/tables.py +0 -0
  32. {github_security_report-0.22.0 → github_security_report-0.23.1}/src/github_security_report/classify.py +0 -0
  33. {github_security_report-0.22.0 → github_security_report-0.23.1}/src/github_security_report/cli/__init__.py +0 -0
  34. {github_security_report-0.22.0 → github_security_report-0.23.1}/src/github_security_report/cli/__main__.py +0 -0
  35. {github_security_report-0.22.0 → github_security_report-0.23.1}/src/github_security_report/cli/app.py +0 -0
  36. {github_security_report-0.22.0 → github_security_report-0.23.1}/src/github_security_report/cli/boundary.py +0 -0
  37. {github_security_report-0.22.0 → github_security_report-0.23.1}/src/github_security_report/cli/options.py +0 -0
  38. {github_security_report-0.22.0 → github_security_report-0.23.1}/src/github_security_report/cli/outputs.py +0 -0
  39. {github_security_report-0.22.0 → github_security_report-0.23.1}/src/github_security_report/cli/progress.py +0 -0
  40. {github_security_report-0.22.0 → github_security_report-0.23.1}/src/github_security_report/cli/publish.py +0 -0
  41. {github_security_report-0.22.0 → github_security_report-0.23.1}/src/github_security_report/cli/summary.py +0 -0
  42. {github_security_report-0.22.0 → github_security_report-0.23.1}/src/github_security_report/client/__init__.py +0 -0
  43. {github_security_report-0.22.0 → github_security_report-0.23.1}/src/github_security_report/client/accounting.py +0 -0
  44. {github_security_report-0.22.0 → github_security_report-0.23.1}/src/github_security_report/client/alerts.py +0 -0
  45. {github_security_report-0.22.0 → github_security_report-0.23.1}/src/github_security_report/client/analyses.py +0 -0
  46. {github_security_report-0.22.0 → github_security_report-0.23.1}/src/github_security_report/client/batch_errors.py +0 -0
  47. {github_security_report-0.22.0 → github_security_report-0.23.1}/src/github_security_report/client/codeql_parsers.py +0 -0
  48. {github_security_report-0.22.0 → github_security_report-0.23.1}/src/github_security_report/client/copilot.py +0 -0
  49. {github_security_report-0.22.0 → github_security_report-0.23.1}/src/github_security_report/client/endpoints.py +0 -0
  50. {github_security_report-0.22.0 → github_security_report-0.23.1}/src/github_security_report/client/errors.py +0 -0
  51. {github_security_report-0.22.0 → github_security_report-0.23.1}/src/github_security_report/client/events.py +0 -0
  52. {github_security_report-0.22.0 → github_security_report-0.23.1}/src/github_security_report/client/org_reads.py +0 -0
  53. {github_security_report-0.22.0 → github_security_report-0.23.1}/src/github_security_report/client/paging.py +0 -0
  54. {github_security_report-0.22.0 → github_security_report-0.23.1}/src/github_security_report/client/queries.py +0 -0
  55. {github_security_report-0.22.0 → github_security_report-0.23.1}/src/github_security_report/client/reads.py +0 -0
  56. {github_security_report-0.22.0 → github_security_report-0.23.1}/src/github_security_report/client/repo_parser.py +0 -0
  57. {github_security_report-0.22.0 → github_security_report-0.23.1}/src/github_security_report/client/reviews.py +0 -0
  58. {github_security_report-0.22.0 → github_security_report-0.23.1}/src/github_security_report/client/transport.py +0 -0
  59. {github_security_report-0.22.0 → github_security_report-0.23.1}/src/github_security_report/client/workflow_runs.py +0 -0
  60. {github_security_report-0.22.0 → github_security_report-0.23.1}/src/github_security_report/client/writes.py +0 -0
  61. {github_security_report-0.22.0 → github_security_report-0.23.1}/src/github_security_report/code_quality.py +0 -0
  62. {github_security_report-0.22.0 → github_security_report-0.23.1}/src/github_security_report/codeql/__init__.py +0 -0
  63. {github_security_report-0.22.0 → github_security_report-0.23.1}/src/github_security_report/collect/__init__.py +0 -0
  64. {github_security_report-0.22.0 → github_security_report-0.23.1}/src/github_security_report/collect/codeql.py +0 -0
  65. {github_security_report-0.22.0 → github_security_report-0.23.1}/src/github_security_report/collect/context.py +0 -0
  66. {github_security_report-0.22.0 → github_security_report-0.23.1}/src/github_security_report/collect/graph.py +0 -0
  67. {github_security_report-0.22.0 → github_security_report-0.23.1}/src/github_security_report/collect/org.py +0 -0
  68. {github_security_report-0.22.0 → github_security_report-0.23.1}/src/github_security_report/collect/protocols.py +0 -0
  69. {github_security_report-0.22.0 → github_security_report-0.23.1}/src/github_security_report/collect/repo.py +0 -0
  70. {github_security_report-0.22.0 → github_security_report-0.23.1}/src/github_security_report/collect/scoping.py +0 -0
  71. {github_security_report-0.22.0 → github_security_report-0.23.1}/src/github_security_report/collect/workflow_runs.py +0 -0
  72. {github_security_report-0.22.0 → github_security_report-0.23.1}/src/github_security_report/config/__init__.py +0 -0
  73. {github_security_report-0.22.0 → github_security_report-0.23.1}/src/github_security_report/config/loader.py +0 -0
  74. {github_security_report-0.22.0 → github_security_report-0.23.1}/src/github_security_report/config/order.py +0 -0
  75. {github_security_report-0.22.0 → github_security_report-0.23.1}/src/github_security_report/gating.py +0 -0
  76. {github_security_report-0.22.0 → github_security_report-0.23.1}/src/github_security_report/gitctx.py +0 -0
  77. {github_security_report-0.22.0 → github_security_report-0.23.1}/src/github_security_report/issue_presentation.py +0 -0
  78. {github_security_report-0.22.0 → github_security_report-0.23.1}/src/github_security_report/issues.py +0 -0
  79. {github_security_report-0.22.0 → github_security_report-0.23.1}/src/github_security_report/layout.py +0 -0
  80. {github_security_report-0.22.0 → github_security_report-0.23.1}/src/github_security_report/models.py +0 -0
  81. {github_security_report-0.22.0 → github_security_report-0.23.1}/src/github_security_report/ordering.py +0 -0
  82. {github_security_report-0.22.0 → github_security_report-0.23.1}/src/github_security_report/posture/__init__.py +0 -0
  83. {github_security_report-0.22.0 → github_security_report-0.23.1}/src/github_security_report/posture/enablement.py +0 -0
  84. {github_security_report-0.22.0 → github_security_report-0.23.1}/src/github_security_report/posture/facts.py +0 -0
  85. {github_security_report-0.22.0 → github_security_report-0.23.1}/src/github_security_report/posture/quality.py +0 -0
  86. {github_security_report-0.22.0 → github_security_report-0.23.1}/src/github_security_report/pulls/__init__.py +0 -0
  87. {github_security_report-0.22.0 → github_security_report-0.23.1}/src/github_security_report/pulls/columns.py +0 -0
  88. {github_security_report-0.22.0 → github_security_report-0.23.1}/src/github_security_report/pulls/counting.py +0 -0
  89. {github_security_report-0.22.0 → github_security_report-0.23.1}/src/github_security_report/pulls/presentation.py +0 -0
  90. {github_security_report-0.22.0 → github_security_report-0.23.1}/src/github_security_report/pulls/table.py +0 -0
  91. {github_security_report-0.22.0 → github_security_report-0.23.1}/src/github_security_report/py.typed +0 -0
  92. {github_security_report-0.22.0 → github_security_report-0.23.1}/src/github_security_report/ranking.py +0 -0
  93. {github_security_report-0.22.0 → github_security_report-0.23.1}/src/github_security_report/remediate/__init__.py +0 -0
  94. {github_security_report-0.22.0 → github_security_report-0.23.1}/src/github_security_report/remediate/codeql.py +0 -0
  95. {github_security_report-0.22.0 → github_security_report-0.23.1}/src/github_security_report/remediate/model.py +0 -0
  96. {github_security_report-0.22.0 → github_security_report-0.23.1}/src/github_security_report/render/__init__.py +0 -0
  97. {github_security_report-0.22.0 → github_security_report-0.23.1}/src/github_security_report/render/html.py +0 -0
  98. {github_security_report-0.22.0 → github_security_report-0.23.1}/src/github_security_report/render/markdown.py +0 -0
  99. {github_security_report-0.22.0 → github_security_report-0.23.1}/src/github_security_report/render/slack.py +0 -0
  100. {github_security_report-0.22.0 → github_security_report-0.23.1}/src/github_security_report/render/slack_limits.py +0 -0
  101. {github_security_report-0.22.0 → github_security_report-0.23.1}/src/github_security_report/render/slack_text.py +0 -0
  102. {github_security_report-0.22.0 → github_security_report-0.23.1}/src/github_security_report/render/terminal.py +0 -0
  103. {github_security_report-0.22.0 → github_security_report-0.23.1}/src/github_security_report/render/terminal_notices.py +0 -0
  104. {github_security_report-0.22.0 → github_security_report-0.23.1}/src/github_security_report/report/__init__.py +0 -0
  105. {github_security_report-0.22.0 → github_security_report-0.23.1}/src/github_security_report/report/display.py +0 -0
  106. {github_security_report-0.22.0 → github_security_report-0.23.1}/src/github_security_report/report/signals.py +0 -0
  107. {github_security_report-0.22.0 → github_security_report-0.23.1}/src/github_security_report/report/tables.py +0 -0
  108. {github_security_report-0.22.0 → github_security_report-0.23.1}/src/github_security_report/rulesets.py +0 -0
  109. {github_security_report-0.22.0 → github_security_report-0.23.1}/src/github_security_report/runner.py +0 -0
  110. {github_security_report-0.22.0 → github_security_report-0.23.1}/src/github_security_report/scope.py +0 -0
  111. {github_security_report-0.22.0 → github_security_report-0.23.1}/src/github_security_report/secret_patterns.py +0 -0
  112. {github_security_report-0.22.0 → github_security_report-0.23.1}/src/github_security_report/severity.py +0 -0
  113. {github_security_report-0.22.0 → github_security_report-0.23.1}/src/github_security_report/summary.py +0 -0
  114. {github_security_report-0.22.0 → github_security_report-0.23.1}/src/github_security_report/templates/index.html.j2 +0 -0
  115. {github_security_report-0.22.0 → github_security_report-0.23.1}/src/github_security_report/templates/report.html.j2 +0 -0
  116. {github_security_report-0.22.0 → github_security_report-0.23.1}/src/github_security_report/workflow_runs/__init__.py +0 -0
  117. {github_security_report-0.22.0 → github_security_report-0.23.1}/src/github_security_report/workflow_runs/aggregate.py +0 -0
  118. {github_security_report-0.22.0 → github_security_report-0.23.1}/src/github_security_report/workflow_runs/models.py +0 -0
  119. {github_security_report-0.22.0 → github_security_report-0.23.1}/src/github_security_report/workflow_runs/store.py +0 -0
  120. {github_security_report-0.22.0 → github_security_report-0.23.1}/src/github_security_report/workflow_runs/tables.py +0 -0
@@ -1,3 +1,47 @@
1
+ Metadata-Version: 2.5
2
+ Name: github-security-report
3
+ Version: 0.23.1
4
+ Summary: Security and quality reporting across GitHub organisations
5
+ Project-URL: Homepage, https://github.com/lfreleng-actions/github-security-report-action
6
+ Project-URL: Repository, https://github.com/lfreleng-actions/github-security-report-action
7
+ Project-URL: Issues, https://github.com/lfreleng-actions/github-security-report-action/issues
8
+ Author-email: The Linux Foundation <mwatkins@linuxfoundation.org>
9
+ License-Expression: Apache-2.0
10
+ License-File: LICENSE
11
+ Keywords: codeql,dependabot,github,github-actions,linux-foundation,reporting,scorecard,secret-scanning,security,zizmor
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3 :: Only
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Programming Language :: Python :: 3.14
22
+ Classifier: Programming Language :: Python :: 3.15
23
+ Classifier: Topic :: Security
24
+ Classifier: Topic :: Software Development :: Quality Assurance
25
+ Classifier: Typing :: Typed
26
+ Requires-Python: >=3.11
27
+ Requires-Dist: httpx[http2]==0.28.1
28
+ Requires-Dist: jinja2==3.1.6
29
+ Requires-Dist: jsonschema==4.26.0
30
+ Requires-Dist: pyyaml==6.0.3
31
+ Requires-Dist: rich==15.0.0
32
+ Requires-Dist: typer==0.27.3
33
+ Provides-Extra: dev
34
+ Requires-Dist: mypy==2.4.0; extra == 'dev'
35
+ Requires-Dist: pytest-asyncio==1.4.1; extra == 'dev'
36
+ Requires-Dist: pytest-cov==7.1.0; extra == 'dev'
37
+ Requires-Dist: pytest==9.1.1; extra == 'dev'
38
+ Requires-Dist: respx==0.23.1; extra == 'dev'
39
+ Requires-Dist: ruff==0.17.3; extra == 'dev'
40
+ Requires-Dist: syrupy==6.1.1; extra == 'dev'
41
+ Requires-Dist: types-jsonschema==4.26.0.20261006; extra == 'dev'
42
+ Requires-Dist: types-pyyaml==6.0.12.20250915; extra == 'dev'
43
+ Description-Content-Type: text/markdown
44
+
1
45
  <!--
2
46
  # SPDX-License-Identifier: Apache-2.0
3
47
  # SPDX-FileCopyrightText: 2026 The Linux Foundation
@@ -156,7 +200,8 @@ tables (org mode):
156
200
  enabled count out of the applicable count per language, such as
157
201
  `python 30/32 · go 5/5 · javascript-typescript 10/12`. See
158
202
  [ADR-0006](docs/adr/0006-github-code-quality.md); hide it with the
159
- `code_quality` render toggle.
203
+ `code_quality` render toggle, or leave fixture repositories out of it alone
204
+ with its `exclude` patterns (see [Per-category exclusions](#per-category-exclusions)).
160
205
  - **Workflow Failures** — repositories whose GitHub Actions workflows failed
161
206
  in the window, ranked by failed runs. The motivating case is the bulk
162
207
  Dependabot merge whose integration tests need credentials, so they run
@@ -488,10 +533,11 @@ governs that line on every surface:
488
533
 
489
534
  Hiding the line changes nothing else: every count is unaffected, and the report
490
535
  header's repository count already leaves excluded repositories out. So a clean
491
- category reads `All Clean` once no Excluded line qualifies it. Today every
492
- category reports the organisation's own list, so `conditional-hide` currently
493
- hides the same lines `always-hide` does. It differs once a category excludes
494
- repositories of its own. The `report.json` artifact always lists the exclusions.
536
+ category reads `All Clean` once no Excluded line qualifies it. Every category
537
+ reports the organisation's own list unless it excludes repositories of its own
538
+ (see [Per-category exclusions](#per-category-exclusions)), so with
539
+ `conditional-hide` the line survives only beneath such a category. The
540
+ `report.json` artifact always lists the exclusions.
495
541
 
496
542
  Archived and test repositories are excluded from analysis by default. Opt them
497
543
  back in with `report.include_archived` / `report.include_test` in the config, or
@@ -642,6 +688,40 @@ table with qualitative columns has no enabled list to name. These categories
642
688
  render as a name list rather than a one-column table on every surface. The
643
689
  `report.json` artifact is unaffected.
644
690
 
691
+ ### Per-category exclusions
692
+
693
+ A category can leave repositories out that every other category still reports,
694
+ with `report.categories.<key>.exclude`: a list of regular expressions searched
695
+ against the repository name, case-insensitively, so `^test-` drops every
696
+ repository whose name starts with `test-` and `^fixture$` drops exactly
697
+ `fixture`. The motivating case is Code Quality, which is billed per committer:
698
+ an organisation that reports its test repositories (`include_test`) still has
699
+ no reason to pay for scanning them, and wants the section to list only the
700
+ repositories worth switching on:
701
+
702
+ ```json
703
+ {
704
+ "report": {
705
+ "include_test": true,
706
+ "categories": {
707
+ "code_quality": { "exclude": ["^test-"] }
708
+ }
709
+ },
710
+ "organizations": [{ "name": "lfreleng-actions" }]
711
+ }
712
+ ```
713
+
714
+ An excluded repository is neither a row nor a count in that category, and
715
+ `remediate --category code_quality` never writes to it. It is not dropped
716
+ silently: the category's **Excluded** footer line names it alongside the
717
+ organisation's own `exclude` list, and `report.json` carries the category's full
718
+ list under `category_excluded`. Under `excluded_display: conditional-hide` that
719
+ footer line is exactly the one that stays. An organisation's own `exclude` list
720
+ replaces the inherited one rather than adding to it, as every other
721
+ `categories` key does; a pattern that does not compile is a configuration
722
+ error. `exclude` applies to `code_quality` only; setting it on any other
723
+ category is a configuration error, since no other table yet applies it.
724
+
645
725
  ### Per-category row ordering
646
726
 
647
727
  Each table ships a sensible default ordering — largest backlog first, stalest
@@ -2048,6 +2128,13 @@ cause:
2048
2128
 
2049
2129
  <!-- markdownlint-enable MD013 -->
2050
2130
 
2131
+ "Analyses failing" is reported only while the setup still exists and still
2132
+ runs. When the setup is gone or blocked (default setup off or no longer
2133
+ listing the language, workflow removed, superseded by default setup) that
2134
+ cause stands even if the newest attempt errored: default setup drops a
2135
+ language whose last run found no code for it, and that run uploads an error,
2136
+ but nothing can upload under the category again, so there is no run to fix.
2137
+
2051
2138
  Deleting removes a configuration's analyses, which clears GitHub's *"Code
2052
2139
  Scanning results may be out of date"* warning but also removes its alert
2053
2140
  history. A deletion is therefore **refused**, in a dry run as in an apply,
@@ -2055,7 +2142,9 @@ when:
2055
2142
 
2056
2143
  - no current configuration scans its language: the stale results are then
2057
2144
  the only record of it, so add scanning for the language first (the refusal
2058
- names it, and **CodeQL: Language Coverage** lists the gap);
2145
+ names it, and **CodeQL: Language Coverage** lists the gap). A language
2146
+ default setup itself dropped is exempt: nothing lists it as a gap, so
2147
+ nothing would ever call for scanning it again;
2059
2148
  - it would help close an open alert. This is judged across **every deletion
2060
2149
  planned in the repository**, not one at a time: an alert held by two stale
2061
2150
  configurations would close if both went, so both are refused, although
@@ -1,47 +1,3 @@
1
- Metadata-Version: 2.5
2
- Name: github-security-report
3
- Version: 0.22.0
4
- Summary: Security and quality reporting across GitHub organisations
5
- Project-URL: Homepage, https://github.com/lfreleng-actions/github-security-report-action
6
- Project-URL: Repository, https://github.com/lfreleng-actions/github-security-report-action
7
- Project-URL: Issues, https://github.com/lfreleng-actions/github-security-report-action/issues
8
- Author-email: The Linux Foundation <mwatkins@linuxfoundation.org>
9
- License-Expression: Apache-2.0
10
- License-File: LICENSE
11
- Keywords: codeql,dependabot,github,github-actions,linux-foundation,reporting,scorecard,secret-scanning,security,zizmor
12
- Classifier: Development Status :: 3 - Alpha
13
- Classifier: Intended Audience :: Developers
14
- Classifier: Operating System :: OS Independent
15
- Classifier: Programming Language :: Python
16
- Classifier: Programming Language :: Python :: 3
17
- Classifier: Programming Language :: Python :: 3 :: Only
18
- Classifier: Programming Language :: Python :: 3.10
19
- Classifier: Programming Language :: Python :: 3.11
20
- Classifier: Programming Language :: Python :: 3.12
21
- Classifier: Programming Language :: Python :: 3.13
22
- Classifier: Programming Language :: Python :: 3.14
23
- Classifier: Topic :: Security
24
- Classifier: Topic :: Software Development :: Quality Assurance
25
- Classifier: Typing :: Typed
26
- Requires-Python: >=3.10
27
- Requires-Dist: httpx[http2]==0.28.1
28
- Requires-Dist: jinja2==3.1.6
29
- Requires-Dist: jsonschema==4.26.0
30
- Requires-Dist: pyyaml==6.0.3
31
- Requires-Dist: rich==15.0.0
32
- Requires-Dist: typer==0.27.3
33
- Provides-Extra: dev
34
- Requires-Dist: mypy==2.4.0; extra == 'dev'
35
- Requires-Dist: pytest-asyncio==1.4.1; extra == 'dev'
36
- Requires-Dist: pytest-cov==7.1.0; extra == 'dev'
37
- Requires-Dist: pytest==9.1.1; extra == 'dev'
38
- Requires-Dist: respx==0.23.1; extra == 'dev'
39
- Requires-Dist: ruff==0.17.3; extra == 'dev'
40
- Requires-Dist: syrupy==6.1.1; extra == 'dev'
41
- Requires-Dist: types-jsonschema==4.26.0.20261006; extra == 'dev'
42
- Requires-Dist: types-pyyaml==6.0.12.20250915; extra == 'dev'
43
- Description-Content-Type: text/markdown
44
-
45
1
  <!--
46
2
  # SPDX-License-Identifier: Apache-2.0
47
3
  # SPDX-FileCopyrightText: 2026 The Linux Foundation
@@ -200,7 +156,8 @@ tables (org mode):
200
156
  enabled count out of the applicable count per language, such as
201
157
  `python 30/32 · go 5/5 · javascript-typescript 10/12`. See
202
158
  [ADR-0006](docs/adr/0006-github-code-quality.md); hide it with the
203
- `code_quality` render toggle.
159
+ `code_quality` render toggle, or leave fixture repositories out of it alone
160
+ with its `exclude` patterns (see [Per-category exclusions](#per-category-exclusions)).
204
161
  - **Workflow Failures** — repositories whose GitHub Actions workflows failed
205
162
  in the window, ranked by failed runs. The motivating case is the bulk
206
163
  Dependabot merge whose integration tests need credentials, so they run
@@ -532,10 +489,11 @@ governs that line on every surface:
532
489
 
533
490
  Hiding the line changes nothing else: every count is unaffected, and the report
534
491
  header's repository count already leaves excluded repositories out. So a clean
535
- category reads `All Clean` once no Excluded line qualifies it. Today every
536
- category reports the organisation's own list, so `conditional-hide` currently
537
- hides the same lines `always-hide` does. It differs once a category excludes
538
- repositories of its own. The `report.json` artifact always lists the exclusions.
492
+ category reads `All Clean` once no Excluded line qualifies it. Every category
493
+ reports the organisation's own list unless it excludes repositories of its own
494
+ (see [Per-category exclusions](#per-category-exclusions)), so with
495
+ `conditional-hide` the line survives only beneath such a category. The
496
+ `report.json` artifact always lists the exclusions.
539
497
 
540
498
  Archived and test repositories are excluded from analysis by default. Opt them
541
499
  back in with `report.include_archived` / `report.include_test` in the config, or
@@ -686,6 +644,40 @@ table with qualitative columns has no enabled list to name. These categories
686
644
  render as a name list rather than a one-column table on every surface. The
687
645
  `report.json` artifact is unaffected.
688
646
 
647
+ ### Per-category exclusions
648
+
649
+ A category can leave repositories out that every other category still reports,
650
+ with `report.categories.<key>.exclude`: a list of regular expressions searched
651
+ against the repository name, case-insensitively, so `^test-` drops every
652
+ repository whose name starts with `test-` and `^fixture$` drops exactly
653
+ `fixture`. The motivating case is Code Quality, which is billed per committer:
654
+ an organisation that reports its test repositories (`include_test`) still has
655
+ no reason to pay for scanning them, and wants the section to list only the
656
+ repositories worth switching on:
657
+
658
+ ```json
659
+ {
660
+ "report": {
661
+ "include_test": true,
662
+ "categories": {
663
+ "code_quality": { "exclude": ["^test-"] }
664
+ }
665
+ },
666
+ "organizations": [{ "name": "lfreleng-actions" }]
667
+ }
668
+ ```
669
+
670
+ An excluded repository is neither a row nor a count in that category, and
671
+ `remediate --category code_quality` never writes to it. It is not dropped
672
+ silently: the category's **Excluded** footer line names it alongside the
673
+ organisation's own `exclude` list, and `report.json` carries the category's full
674
+ list under `category_excluded`. Under `excluded_display: conditional-hide` that
675
+ footer line is exactly the one that stays. An organisation's own `exclude` list
676
+ replaces the inherited one rather than adding to it, as every other
677
+ `categories` key does; a pattern that does not compile is a configuration
678
+ error. `exclude` applies to `code_quality` only; setting it on any other
679
+ category is a configuration error, since no other table yet applies it.
680
+
689
681
  ### Per-category row ordering
690
682
 
691
683
  Each table ships a sensible default ordering — largest backlog first, stalest
@@ -2092,6 +2084,13 @@ cause:
2092
2084
 
2093
2085
  <!-- markdownlint-enable MD013 -->
2094
2086
 
2087
+ "Analyses failing" is reported only while the setup still exists and still
2088
+ runs. When the setup is gone or blocked (default setup off or no longer
2089
+ listing the language, workflow removed, superseded by default setup) that
2090
+ cause stands even if the newest attempt errored: default setup drops a
2091
+ language whose last run found no code for it, and that run uploads an error,
2092
+ but nothing can upload under the category again, so there is no run to fix.
2093
+
2095
2094
  Deleting removes a configuration's analyses, which clears GitHub's *"Code
2096
2095
  Scanning results may be out of date"* warning but also removes its alert
2097
2096
  history. A deletion is therefore **refused**, in a dry run as in an apply,
@@ -2099,7 +2098,9 @@ when:
2099
2098
 
2100
2099
  - no current configuration scans its language: the stale results are then
2101
2100
  the only record of it, so add scanning for the language first (the refusal
2102
- names it, and **CodeQL: Language Coverage** lists the gap);
2101
+ names it, and **CodeQL: Language Coverage** lists the gap). A language
2102
+ default setup itself dropped is exempt: nothing lists it as a gap, so
2103
+ nothing would ever call for scanning it again;
2103
2104
  - it would help close an open alert. This is judged across **every deletion
2104
2105
  planned in the repository**, not one at a time: an alert held by two stale
2105
2106
  configurations would close if both went, so both are refused, although
@@ -14,7 +14,7 @@ authors = [
14
14
  ]
15
15
  license = "Apache-2.0"
16
16
  readme = "README.md"
17
- requires-python = ">=3.10"
17
+ requires-python = ">=3.11"
18
18
  classifiers = [
19
19
  "Development Status :: 3 - Alpha",
20
20
  "Intended Audience :: Developers",
@@ -22,11 +22,11 @@ classifiers = [
22
22
  "Programming Language :: Python",
23
23
  "Programming Language :: Python :: 3",
24
24
  "Programming Language :: Python :: 3 :: Only",
25
- "Programming Language :: Python :: 3.10",
26
25
  "Programming Language :: Python :: 3.11",
27
26
  "Programming Language :: Python :: 3.12",
28
27
  "Programming Language :: Python :: 3.13",
29
28
  "Programming Language :: Python :: 3.14",
29
+ "Programming Language :: Python :: 3.15",
30
30
  "Topic :: Software Development :: Quality Assurance",
31
31
  "Topic :: Security",
32
32
  "Typing :: Typed",
@@ -144,7 +144,7 @@ show_missing = true
144
144
  skip_empty = true
145
145
 
146
146
  [tool.mypy]
147
- python_version = "3.10"
147
+ python_version = "3.11"
148
148
  # The whole first-party tree, so a bare 'mypy' checks what the pre-commit hook
149
149
  # checks. Tests are deliberately included: they are held to the same standard
150
150
  # as the package.
@@ -156,7 +156,7 @@ ignore_missing_imports = true
156
156
  exclude = ["src/github_security_report/_version.py"]
157
157
 
158
158
  [tool.basedpyright]
159
- pythonVersion = "3.10"
159
+ pythonVersion = "3.11"
160
160
  pythonPlatform = "All"
161
161
  typeCheckingMode = "standard"
162
162
  exclude = ["src/github_security_report/_version.py"]
@@ -190,7 +190,7 @@ reportIndexIssue = false
190
190
  reportArgumentType = false
191
191
 
192
192
  [tool.ruff]
193
- target-version = "py310"
193
+ target-version = "py311"
194
194
  line-length = 88
195
195
  exclude = ["src/github_security_report/_version.py"]
196
196
 
@@ -199,6 +199,7 @@ select = ["E", "W", "F", "I", "B", "C4", "UP", "SIM"]
199
199
  ignore = [
200
200
  "E501", # line length handled by formatter
201
201
  "B008", # function calls in argument defaults (Typer pattern)
202
+ "UP042", # StrEnum changes str()/format() of members; keep (str, Enum)
202
203
  ]
203
204
 
204
205
  [tool.ruff.lint.per-file-ignores]
@@ -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.0'
22
- __version_tuple__ = version_tuple = (0, 22, 0)
21
+ __version__ = version = '0.23.1'
22
+ __version_tuple__ = version_tuple = (0, 23, 1)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -24,6 +24,7 @@ from github_security_report.categories.signals import SIGNAL_CATEGORIES
24
24
  from github_security_report.categories.tables import TABLE_CATEGORIES
25
25
 
26
26
  __all__ = [
27
+ "EXCLUDABLE_CATEGORIES",
27
28
  "NESTED_CATEGORIES",
28
29
  "REPO_LIST_CATEGORIES",
29
30
  "CategoryKey",
@@ -76,6 +77,15 @@ REPO_LIST_CATEGORIES: frozenset[CategoryKey] = frozenset(
76
77
  )
77
78
 
78
79
 
80
+ # Categories that take an ``exclude`` list of their own, dropping repositories
81
+ # from that one category while every other still reports them. A repository
82
+ # excluded this way is counted on the category's Excluded footer line, so the
83
+ # omission stays visible, and remediation never writes it. Only the categories
84
+ # whose builders apply the list are named: the schema refuses it anywhere else,
85
+ # where it would validate and then do nothing.
86
+ EXCLUDABLE_CATEGORIES: frozenset[CategoryKey] = frozenset({CategoryKey.CODE_QUALITY})
87
+
88
+
79
89
  def orderable_categories() -> tuple[CategoryMeta, ...]:
80
90
  """Categories an ordering list may name, in registry order.
81
91
 
@@ -214,7 +214,7 @@ async def _run_org(cfg: Config, options: OrgRunOptions, *, console: Console) ->
214
214
  summaries as the collections ended would have stacked them all above the
215
215
  first report.
216
216
  """
217
- now = dt.datetime.now(dt.timezone.utc)
217
+ now = dt.datetime.now(dt.UTC)
218
218
  collected = await _collect_reports(cfg, console=console, now=now, options=options)
219
219
  if collected is None:
220
220
  return 2
@@ -285,7 +285,7 @@ async def _run_repo(
285
285
  if not token:
286
286
  console.print(f"[red]No token in ${token_env}[/red]")
287
287
  return 2
288
- now = dt.datetime.now(dt.timezone.utc)
288
+ now = dt.datetime.now(dt.UTC)
289
289
  # One repository, so the live line has no counter to drive; the spinner
290
290
  # and the request counts still cover the probes and the run listing.
291
291
  tracker = make_tracker(console, no_progress=no_progress)
@@ -55,7 +55,7 @@ async def _run_remediate(
55
55
  auto-discovered config file, printed under each org's divider. Returns 2
56
56
  for a bad selection, 1 when any write failed, else 0.
57
57
  """
58
- now = dt.datetime.now(dt.timezone.utc)
58
+ now = dt.datetime.now(dt.UTC)
59
59
  failures = 0
60
60
  # One live line per organisation, kept up through the writes too: the
61
61
  # paced CodeQL cleanup is otherwise a silent second per deletion.
@@ -83,6 +83,15 @@ def _org_to_dict(org: OrgReport, hidden: Collection[CategoryKey] = ()) -> dict:
83
83
  "partial": org.partial,
84
84
  # Repositories explicitly excluded from analysis (per-org exclude list).
85
85
  "excluded": [r.full_name for r in org.excluded_repos],
86
+ # Categories excluding repositories of their own (a category's
87
+ # `exclude` patterns), each with its full list: the organisation's
88
+ # exclusions above plus the category's. Absent categories report the
89
+ # organisation's list alone.
90
+ "category_excluded": {
91
+ key.value: [r.full_name for r in repos]
92
+ for key, repos in org.category_excluded.items()
93
+ if key not in suppressed
94
+ },
86
95
  # Every category below, in the order the rendered surfaces drew it.
87
96
  # The keyed structure that follows is stable regardless, but a consumer
88
97
  # building its own view can reproduce the published layout instead of
@@ -52,7 +52,7 @@ async def _endpoint_diagnostics(url: str) -> str:
52
52
  )
53
53
  ips = sorted({str(info[4][0]) for info in infos})
54
54
  addr = ", ".join(ips) if ips else "no addresses"
55
- except asyncio.TimeoutError:
55
+ except TimeoutError:
56
56
  addr = "unresolved (timed out)"
57
57
  except OSError as exc:
58
58
  detail = exc.strerror or str(exc) or "resolution failed"
@@ -65,8 +65,8 @@ def _parse_retry_after(value: str | None) -> float | None:
65
65
  if when is None:
66
66
  return None
67
67
  if when.tzinfo is None:
68
- when = when.replace(tzinfo=dt.timezone.utc)
69
- return max(0.0, (when - dt.datetime.now(dt.timezone.utc)).total_seconds())
68
+ when = when.replace(tzinfo=dt.UTC)
69
+ return max(0.0, (when - dt.datetime.now(dt.UTC)).total_seconds())
70
70
 
71
71
 
72
72
  def _next_page_url(resp: httpx.Response) -> str | None:
@@ -21,7 +21,10 @@ A deletion is *refused* while no current configuration scans its language.
21
21
  The stale results are then the only record of that language, and removing
22
22
  them would turn "scanned, but out of date" into "never scanned" without
23
23
  anything having improved. Add scanning for the language first; the refusal
24
- names it. Whether a deletion would close an open alert needs a network read,
24
+ names it. The refusal does not apply when default setup itself dropped the
25
+ language: its language list is the only inventory the report has, so nothing
26
+ would ever call for scanning it again, and Language Coverage shows no gap to
27
+ close. Whether a deletion would close an open alert needs a network read,
25
28
  so the remediator checks that separately before any write.
26
29
  """
27
30
 
@@ -148,8 +151,14 @@ def _plan_one(
148
151
  return CleanupItem(facts.repo, config, cause, CleanupAction.REENABLE_WORKFLOW)
149
152
  if not cause.orphaned:
150
153
  return None
154
+ # Default setup no longer lists a removed language, so no current
155
+ # configuration can scan it and none is expected to: the coverage
156
+ # refusal would ask for scanning that the setup has ruled out.
157
+ uncovered = cause is not StaleCause.LANGUAGE_REMOVED and (
158
+ config.language is None or config.language not in current
159
+ )
151
160
  refusal = None
152
- if config.language is None or config.language not in current:
161
+ if uncovered:
153
162
  refusal = (
154
163
  f"no current configuration scans {config.language or 'its language'}; "
155
164
  "add scanning for it first"
@@ -94,7 +94,8 @@ class StaleCause(str, Enum):
94
94
  """
95
95
 
96
96
  # The setup still runs, but its newest attempt errored: never deleted,
97
- # since fixing the run is what brings the scan back.
97
+ # since fixing the run is what brings the scan back. Yields to a cause
98
+ # whose setup is gone (see ``setup_gone``): there is no run to fix.
98
99
  ANALYSES_FAILING = "Analyses failing; check the latest run"
99
100
  DEFAULT_SETUP_DISABLED = "Default setup disabled; orphaned"
100
101
  LANGUAGE_REMOVED = "Language removed from default setup"
@@ -127,6 +128,21 @@ class StaleCause(str, Enum):
127
128
  """
128
129
  return self in _ORPHANED_CAUSES
129
130
 
131
+ @property
132
+ def setup_gone(self) -> bool:
133
+ """Whether the setup behind the configuration is gone or blocked.
134
+
135
+ Default setup off or no longer listing the language, the workflow
136
+ file removed, or advanced uploads rejected while default setup is on:
137
+ no attempt of any kind can follow, so an errored newest attempt is
138
+ the setup's last act, not a run anybody can fix, and this cause
139
+ outranks "analyses failing". The one orphaned cause left out is
140
+ superseded-within-workflow: that is inferred from a sibling's
141
+ activity, not the setup's own state, and a fresh errored attempt
142
+ under this category shows the workflow still uploads to it.
143
+ """
144
+ return self in _SETUP_GONE_CAUSES
145
+
130
146
  @property
131
147
  def reenable_workflow(self) -> bool:
132
148
  """Whether re-enabling the workflow is the fix, rather than deleting.
@@ -138,16 +154,16 @@ class StaleCause(str, Enum):
138
154
  return self is StaleCause.WORKFLOW_INACTIVE
139
155
 
140
156
 
141
- _ORPHANED_CAUSES = frozenset(
157
+ _SETUP_GONE_CAUSES = frozenset(
142
158
  {
143
159
  StaleCause.DEFAULT_SETUP_DISABLED,
144
160
  StaleCause.LANGUAGE_REMOVED,
145
161
  StaleCause.WORKFLOW_REMOVED,
146
- StaleCause.SUPERSEDED_IN_WORKFLOW,
147
162
  StaleCause.SUPERSEDED,
148
163
  StaleCause.SUPERSEDED_DISABLED,
149
164
  }
150
165
  )
166
+ _ORPHANED_CAUSES = _SETUP_GONE_CAUSES | {StaleCause.SUPERSEDED_IN_WORKFLOW}
151
167
 
152
168
 
153
169
  @dataclass(frozen=True)
@@ -206,7 +222,7 @@ class CodeQLConfiguration:
206
222
 
207
223
  # Aware sentinel so a configuration that never succeeded sorts ahead of every
208
224
  # dated one, without comparing a naive and an aware value.
209
- _NEVER = dt.datetime.min.replace(tzinfo=dt.timezone.utc)
225
+ _NEVER = dt.datetime.min.replace(tzinfo=dt.UTC)
210
226
 
211
227
 
212
228
  @dataclass(frozen=True)
@@ -135,16 +135,22 @@ def stale_cause(
135
135
  ) -> StaleCause:
136
136
  """Why a stale configuration stopped scanning, as far as the API can tell.
137
137
 
138
- An errored newest attempt comes first: the setup still exists and still
139
- runs, so whatever its state, the reader's next step is its failing logs.
140
- ``stale_days`` is the threshold the configuration was found stale by,
141
- which also decides whether another configuration is current.
138
+ An errored newest attempt is the cause while the setup still exists and
139
+ still runs: whatever its state, the reader's next step is its failing
140
+ logs. It is not the cause once the setup is gone or blocked. Default
141
+ setup drops a language whose last run found no code for it, and that
142
+ run uploads an error; reading the error as the cause would leave a
143
+ configuration that can never upload again waiting for a fix that cannot
144
+ come. ``stale_days`` is the threshold the configuration was found stale
145
+ by, which also decides whether another configuration is current.
142
146
  """
143
- if config.failing:
144
- return StaleCause.ANALYSES_FAILING
145
147
  if config.setup is SetupType.DEFAULT:
146
- return _default_setup_cause(config, facts)
147
- return _advanced_setup_cause(config, facts, stale_days)
148
+ cause = _default_setup_cause(config, facts)
149
+ else:
150
+ cause = _advanced_setup_cause(config, facts, stale_days)
151
+ if config.failing and not cause.setup_gone:
152
+ return StaleCause.ANALYSES_FAILING
153
+ return cause
148
154
 
149
155
 
150
156
  def build_stale_configurations_table(
@@ -20,6 +20,7 @@ import asyncio
20
20
  import logging
21
21
 
22
22
  from github_security_report import codeql, layout, malware, posture
23
+ from github_security_report.categories import CategoryKey
23
24
  from github_security_report.code_quality import CodeQualitySetup, applicable_languages
24
25
  from github_security_report.collect.codeql import collect_codeql_facts
25
26
  from github_security_report.collect.context import (
@@ -45,7 +46,7 @@ from github_security_report.pulls import (
45
46
  build_assigned_pull_requests_table,
46
47
  build_pull_requests_table,
47
48
  )
48
- from github_security_report.report import OrgReport
49
+ from github_security_report.report import OrgReport, TableSection
49
50
 
50
51
  log = logging.getLogger(__name__)
51
52
 
@@ -122,6 +123,31 @@ async def _posture_for_repo(repo: Repo, ctx: OrgCollectContext) -> RepoPosture:
122
123
  )
123
124
 
124
125
 
126
+ def _build_code_quality(
127
+ report: OrgReport, postures: list[RepoPosture], report_cfg: ReportConfig
128
+ ) -> tuple[TableSection, dict[str, tuple[str, ...]]]:
129
+ """The Code Quality table and its remediation plan, less the excluded.
130
+
131
+ A repository the category's own ``exclude`` patterns match is dropped
132
+ before the table is built, so it is neither a row nor a count nor a
133
+ remediation target, and is recorded as excluded for this category alone:
134
+ its footer then lists it alongside the organisation's own exclusions,
135
+ while every other category still reports the repository.
136
+ """
137
+ kept: list[RepoPosture] = []
138
+ dropped: list[Repo] = []
139
+ for p in postures:
140
+ if report_cfg.category_excludes(CategoryKey.CODE_QUALITY, p.repo.name):
141
+ dropped.append(p.repo)
142
+ else:
143
+ kept.append(p)
144
+ if dropped:
145
+ report.category_excluded[CategoryKey.CODE_QUALITY] = sorted(
146
+ (*report.excluded_repos, *dropped), key=lambda r: r.name
147
+ )
148
+ return posture.build_code_quality_table(kept), posture.code_quality_writes(kept)
149
+
150
+
125
151
  async def _attach_author_tables(
126
152
  report: OrgReport,
127
153
  in_scope: list[Repo],
@@ -251,8 +277,9 @@ async def attach_extra_tables(
251
277
  sweep_readable=ctx.sweep_status.get("dependabot") == 200,
252
278
  generated_at=report.generated_at,
253
279
  )
254
- report.code_quality = posture.build_code_quality_table(postures)
255
- report.code_quality_writes = posture.code_quality_writes(postures)
280
+ report.code_quality, report.code_quality_writes = _build_code_quality(
281
+ report, postures, report_cfg
282
+ )
256
283
  report.codeql_tables = codeql.build_codeql_tables(
257
284
  codeql_facts, stale_days=report_cfg.codeql_stale_days
258
285
  )
@@ -89,7 +89,7 @@ class RepoRunSync:
89
89
 
90
90
  def _date(when: dt.datetime) -> str:
91
91
  """The calendar date GitHub's ``created`` filter takes."""
92
- return when.astimezone(dt.timezone.utc).date().isoformat()
92
+ return when.astimezone(dt.UTC).date().isoformat()
93
93
 
94
94
 
95
95
  def _merge(