github-security-report 0.22.0__tar.gz → 0.23.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 (120) hide show
  1. {github_security_report-0.22.0 → github_security_report-0.23.0}/PKG-INFO +42 -6
  2. {github_security_report-0.22.0 → github_security_report-0.23.0}/README.md +41 -5
  3. {github_security_report-0.22.0 → github_security_report-0.23.0}/src/github_security_report/_version.py +2 -2
  4. {github_security_report-0.22.0 → github_security_report-0.23.0}/src/github_security_report/categories/__init__.py +10 -0
  5. {github_security_report-0.22.0 → github_security_report-0.23.0}/src/github_security_report/cli/serialise.py +9 -0
  6. {github_security_report-0.22.0 → github_security_report-0.23.0}/src/github_security_report/collect/extras.py +30 -3
  7. {github_security_report-0.22.0 → github_security_report-0.23.0}/src/github_security_report/config/blocks.py +27 -0
  8. {github_security_report-0.22.0 → github_security_report-0.23.0}/src/github_security_report/config/models.py +17 -1
  9. {github_security_report-0.22.0 → github_security_report-0.23.0}/src/github_security_report/config/schema.py +13 -2
  10. {github_security_report-0.22.0 → github_security_report-0.23.0}/src/github_security_report/report/aggregate.py +5 -5
  11. {github_security_report-0.22.0 → github_security_report-0.23.0}/.gitignore +0 -0
  12. {github_security_report-0.22.0 → github_security_report-0.23.0}/LICENSE +0 -0
  13. {github_security_report-0.22.0 → github_security_report-0.23.0}/LICENSES/Apache-2.0.txt +0 -0
  14. {github_security_report-0.22.0 → github_security_report-0.23.0}/pyproject.toml +0 -0
  15. {github_security_report-0.22.0 → github_security_report-0.23.0}/scripts/README.md +0 -0
  16. {github_security_report-0.22.0 → github_security_report-0.23.0}/src/github_security_report/__init__.py +0 -0
  17. {github_security_report-0.22.0 → github_security_report-0.23.0}/src/github_security_report/authors.py +0 -0
  18. {github_security_report-0.22.0 → github_security_report-0.23.0}/src/github_security_report/categories/keys.py +0 -0
  19. {github_security_report-0.22.0 → github_security_report-0.23.0}/src/github_security_report/categories/signals.py +0 -0
  20. {github_security_report-0.22.0 → github_security_report-0.23.0}/src/github_security_report/categories/tables.py +0 -0
  21. {github_security_report-0.22.0 → github_security_report-0.23.0}/src/github_security_report/classify.py +0 -0
  22. {github_security_report-0.22.0 → github_security_report-0.23.0}/src/github_security_report/cli/__init__.py +0 -0
  23. {github_security_report-0.22.0 → github_security_report-0.23.0}/src/github_security_report/cli/__main__.py +0 -0
  24. {github_security_report-0.22.0 → github_security_report-0.23.0}/src/github_security_report/cli/app.py +0 -0
  25. {github_security_report-0.22.0 → github_security_report-0.23.0}/src/github_security_report/cli/boundary.py +0 -0
  26. {github_security_report-0.22.0 → github_security_report-0.23.0}/src/github_security_report/cli/modes.py +0 -0
  27. {github_security_report-0.22.0 → github_security_report-0.23.0}/src/github_security_report/cli/options.py +0 -0
  28. {github_security_report-0.22.0 → github_security_report-0.23.0}/src/github_security_report/cli/outputs.py +0 -0
  29. {github_security_report-0.22.0 → github_security_report-0.23.0}/src/github_security_report/cli/progress.py +0 -0
  30. {github_security_report-0.22.0 → github_security_report-0.23.0}/src/github_security_report/cli/publish.py +0 -0
  31. {github_security_report-0.22.0 → github_security_report-0.23.0}/src/github_security_report/cli/remediation.py +0 -0
  32. {github_security_report-0.22.0 → github_security_report-0.23.0}/src/github_security_report/cli/summary.py +0 -0
  33. {github_security_report-0.22.0 → github_security_report-0.23.0}/src/github_security_report/client/__init__.py +0 -0
  34. {github_security_report-0.22.0 → github_security_report-0.23.0}/src/github_security_report/client/accounting.py +0 -0
  35. {github_security_report-0.22.0 → github_security_report-0.23.0}/src/github_security_report/client/alerts.py +0 -0
  36. {github_security_report-0.22.0 → github_security_report-0.23.0}/src/github_security_report/client/analyses.py +0 -0
  37. {github_security_report-0.22.0 → github_security_report-0.23.0}/src/github_security_report/client/batch_errors.py +0 -0
  38. {github_security_report-0.22.0 → github_security_report-0.23.0}/src/github_security_report/client/codeql_parsers.py +0 -0
  39. {github_security_report-0.22.0 → github_security_report-0.23.0}/src/github_security_report/client/copilot.py +0 -0
  40. {github_security_report-0.22.0 → github_security_report-0.23.0}/src/github_security_report/client/endpoints.py +0 -0
  41. {github_security_report-0.22.0 → github_security_report-0.23.0}/src/github_security_report/client/errors.py +0 -0
  42. {github_security_report-0.22.0 → github_security_report-0.23.0}/src/github_security_report/client/events.py +0 -0
  43. {github_security_report-0.22.0 → github_security_report-0.23.0}/src/github_security_report/client/failures.py +0 -0
  44. {github_security_report-0.22.0 → github_security_report-0.23.0}/src/github_security_report/client/org_reads.py +0 -0
  45. {github_security_report-0.22.0 → github_security_report-0.23.0}/src/github_security_report/client/paging.py +0 -0
  46. {github_security_report-0.22.0 → github_security_report-0.23.0}/src/github_security_report/client/parsers.py +0 -0
  47. {github_security_report-0.22.0 → github_security_report-0.23.0}/src/github_security_report/client/queries.py +0 -0
  48. {github_security_report-0.22.0 → github_security_report-0.23.0}/src/github_security_report/client/reads.py +0 -0
  49. {github_security_report-0.22.0 → github_security_report-0.23.0}/src/github_security_report/client/repo_parser.py +0 -0
  50. {github_security_report-0.22.0 → github_security_report-0.23.0}/src/github_security_report/client/reviews.py +0 -0
  51. {github_security_report-0.22.0 → github_security_report-0.23.0}/src/github_security_report/client/transport.py +0 -0
  52. {github_security_report-0.22.0 → github_security_report-0.23.0}/src/github_security_report/client/workflow_runs.py +0 -0
  53. {github_security_report-0.22.0 → github_security_report-0.23.0}/src/github_security_report/client/writes.py +0 -0
  54. {github_security_report-0.22.0 → github_security_report-0.23.0}/src/github_security_report/code_quality.py +0 -0
  55. {github_security_report-0.22.0 → github_security_report-0.23.0}/src/github_security_report/codeql/__init__.py +0 -0
  56. {github_security_report-0.22.0 → github_security_report-0.23.0}/src/github_security_report/codeql/cleanup.py +0 -0
  57. {github_security_report-0.22.0 → github_security_report-0.23.0}/src/github_security_report/codeql/facts.py +0 -0
  58. {github_security_report-0.22.0 → github_security_report-0.23.0}/src/github_security_report/codeql/tables.py +0 -0
  59. {github_security_report-0.22.0 → github_security_report-0.23.0}/src/github_security_report/collect/__init__.py +0 -0
  60. {github_security_report-0.22.0 → github_security_report-0.23.0}/src/github_security_report/collect/codeql.py +0 -0
  61. {github_security_report-0.22.0 → github_security_report-0.23.0}/src/github_security_report/collect/context.py +0 -0
  62. {github_security_report-0.22.0 → github_security_report-0.23.0}/src/github_security_report/collect/graph.py +0 -0
  63. {github_security_report-0.22.0 → github_security_report-0.23.0}/src/github_security_report/collect/org.py +0 -0
  64. {github_security_report-0.22.0 → github_security_report-0.23.0}/src/github_security_report/collect/protocols.py +0 -0
  65. {github_security_report-0.22.0 → github_security_report-0.23.0}/src/github_security_report/collect/repo.py +0 -0
  66. {github_security_report-0.22.0 → github_security_report-0.23.0}/src/github_security_report/collect/run_sync.py +0 -0
  67. {github_security_report-0.22.0 → github_security_report-0.23.0}/src/github_security_report/collect/scoping.py +0 -0
  68. {github_security_report-0.22.0 → github_security_report-0.23.0}/src/github_security_report/collect/workflow_runs.py +0 -0
  69. {github_security_report-0.22.0 → github_security_report-0.23.0}/src/github_security_report/config/__init__.py +0 -0
  70. {github_security_report-0.22.0 → github_security_report-0.23.0}/src/github_security_report/config/loader.py +0 -0
  71. {github_security_report-0.22.0 → github_security_report-0.23.0}/src/github_security_report/config/order.py +0 -0
  72. {github_security_report-0.22.0 → github_security_report-0.23.0}/src/github_security_report/gating.py +0 -0
  73. {github_security_report-0.22.0 → github_security_report-0.23.0}/src/github_security_report/gitctx.py +0 -0
  74. {github_security_report-0.22.0 → github_security_report-0.23.0}/src/github_security_report/issue_presentation.py +0 -0
  75. {github_security_report-0.22.0 → github_security_report-0.23.0}/src/github_security_report/issues.py +0 -0
  76. {github_security_report-0.22.0 → github_security_report-0.23.0}/src/github_security_report/layout.py +0 -0
  77. {github_security_report-0.22.0 → github_security_report-0.23.0}/src/github_security_report/malware.py +0 -0
  78. {github_security_report-0.22.0 → github_security_report-0.23.0}/src/github_security_report/models.py +0 -0
  79. {github_security_report-0.22.0 → github_security_report-0.23.0}/src/github_security_report/ordering.py +0 -0
  80. {github_security_report-0.22.0 → github_security_report-0.23.0}/src/github_security_report/posture/__init__.py +0 -0
  81. {github_security_report-0.22.0 → github_security_report-0.23.0}/src/github_security_report/posture/enablement.py +0 -0
  82. {github_security_report-0.22.0 → github_security_report-0.23.0}/src/github_security_report/posture/facts.py +0 -0
  83. {github_security_report-0.22.0 → github_security_report-0.23.0}/src/github_security_report/posture/quality.py +0 -0
  84. {github_security_report-0.22.0 → github_security_report-0.23.0}/src/github_security_report/posture/releases.py +0 -0
  85. {github_security_report-0.22.0 → github_security_report-0.23.0}/src/github_security_report/pulls/__init__.py +0 -0
  86. {github_security_report-0.22.0 → github_security_report-0.23.0}/src/github_security_report/pulls/columns.py +0 -0
  87. {github_security_report-0.22.0 → github_security_report-0.23.0}/src/github_security_report/pulls/counting.py +0 -0
  88. {github_security_report-0.22.0 → github_security_report-0.23.0}/src/github_security_report/pulls/presentation.py +0 -0
  89. {github_security_report-0.22.0 → github_security_report-0.23.0}/src/github_security_report/pulls/table.py +0 -0
  90. {github_security_report-0.22.0 → github_security_report-0.23.0}/src/github_security_report/py.typed +0 -0
  91. {github_security_report-0.22.0 → github_security_report-0.23.0}/src/github_security_report/ranking.py +0 -0
  92. {github_security_report-0.22.0 → github_security_report-0.23.0}/src/github_security_report/remediate/__init__.py +0 -0
  93. {github_security_report-0.22.0 → github_security_report-0.23.0}/src/github_security_report/remediate/codeql.py +0 -0
  94. {github_security_report-0.22.0 → github_security_report-0.23.0}/src/github_security_report/remediate/model.py +0 -0
  95. {github_security_report-0.22.0 → github_security_report-0.23.0}/src/github_security_report/render/__init__.py +0 -0
  96. {github_security_report-0.22.0 → github_security_report-0.23.0}/src/github_security_report/render/html.py +0 -0
  97. {github_security_report-0.22.0 → github_security_report-0.23.0}/src/github_security_report/render/markdown.py +0 -0
  98. {github_security_report-0.22.0 → github_security_report-0.23.0}/src/github_security_report/render/slack.py +0 -0
  99. {github_security_report-0.22.0 → github_security_report-0.23.0}/src/github_security_report/render/slack_limits.py +0 -0
  100. {github_security_report-0.22.0 → github_security_report-0.23.0}/src/github_security_report/render/slack_text.py +0 -0
  101. {github_security_report-0.22.0 → github_security_report-0.23.0}/src/github_security_report/render/terminal.py +0 -0
  102. {github_security_report-0.22.0 → github_security_report-0.23.0}/src/github_security_report/render/terminal_notices.py +0 -0
  103. {github_security_report-0.22.0 → github_security_report-0.23.0}/src/github_security_report/report/__init__.py +0 -0
  104. {github_security_report-0.22.0 → github_security_report-0.23.0}/src/github_security_report/report/display.py +0 -0
  105. {github_security_report-0.22.0 → github_security_report-0.23.0}/src/github_security_report/report/signals.py +0 -0
  106. {github_security_report-0.22.0 → github_security_report-0.23.0}/src/github_security_report/report/tables.py +0 -0
  107. {github_security_report-0.22.0 → github_security_report-0.23.0}/src/github_security_report/rulesets.py +0 -0
  108. {github_security_report-0.22.0 → github_security_report-0.23.0}/src/github_security_report/runner.py +0 -0
  109. {github_security_report-0.22.0 → github_security_report-0.23.0}/src/github_security_report/scope.py +0 -0
  110. {github_security_report-0.22.0 → github_security_report-0.23.0}/src/github_security_report/secret_patterns.py +0 -0
  111. {github_security_report-0.22.0 → github_security_report-0.23.0}/src/github_security_report/severity.py +0 -0
  112. {github_security_report-0.22.0 → github_security_report-0.23.0}/src/github_security_report/summary.py +0 -0
  113. {github_security_report-0.22.0 → github_security_report-0.23.0}/src/github_security_report/templates/index.html.j2 +0 -0
  114. {github_security_report-0.22.0 → github_security_report-0.23.0}/src/github_security_report/templates/report.html.j2 +0 -0
  115. {github_security_report-0.22.0 → github_security_report-0.23.0}/src/github_security_report/workflow_runs/__init__.py +0 -0
  116. {github_security_report-0.22.0 → github_security_report-0.23.0}/src/github_security_report/workflow_runs/aggregate.py +0 -0
  117. {github_security_report-0.22.0 → github_security_report-0.23.0}/src/github_security_report/workflow_runs/models.py +0 -0
  118. {github_security_report-0.22.0 → github_security_report-0.23.0}/src/github_security_report/workflow_runs/store.py +0 -0
  119. {github_security_report-0.22.0 → github_security_report-0.23.0}/src/github_security_report/workflow_runs/store_codec.py +0 -0
  120. {github_security_report-0.22.0 → github_security_report-0.23.0}/src/github_security_report/workflow_runs/tables.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: github-security-report
3
- Version: 0.22.0
3
+ Version: 0.23.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
@@ -200,7 +200,8 @@ tables (org mode):
200
200
  enabled count out of the applicable count per language, such as
201
201
  `python 30/32 · go 5/5 · javascript-typescript 10/12`. See
202
202
  [ADR-0006](docs/adr/0006-github-code-quality.md); hide it with the
203
- `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)).
204
205
  - **Workflow Failures** — repositories whose GitHub Actions workflows failed
205
206
  in the window, ranked by failed runs. The motivating case is the bulk
206
207
  Dependabot merge whose integration tests need credentials, so they run
@@ -532,10 +533,11 @@ governs that line on every surface:
532
533
 
533
534
  Hiding the line changes nothing else: every count is unaffected, and the report
534
535
  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.
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.
539
541
 
540
542
  Archived and test repositories are excluded from analysis by default. Opt them
541
543
  back in with `report.include_archived` / `report.include_test` in the config, or
@@ -686,6 +688,40 @@ table with qualitative columns has no enabled list to name. These categories
686
688
  render as a name list rather than a one-column table on every surface. The
687
689
  `report.json` artifact is unaffected.
688
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
+
689
725
  ### Per-category row ordering
690
726
 
691
727
  Each table ships a sensible default ordering — largest backlog first, stalest
@@ -156,7 +156,8 @@ tables (org mode):
156
156
  enabled count out of the applicable count per language, such as
157
157
  `python 30/32 · go 5/5 · javascript-typescript 10/12`. See
158
158
  [ADR-0006](docs/adr/0006-github-code-quality.md); hide it with the
159
- `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)).
160
161
  - **Workflow Failures** — repositories whose GitHub Actions workflows failed
161
162
  in the window, ranked by failed runs. The motivating case is the bulk
162
163
  Dependabot merge whose integration tests need credentials, so they run
@@ -488,10 +489,11 @@ governs that line on every surface:
488
489
 
489
490
  Hiding the line changes nothing else: every count is unaffected, and the report
490
491
  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.
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.
495
497
 
496
498
  Archived and test repositories are excluded from analysis by default. Opt them
497
499
  back in with `report.include_archived` / `report.include_test` in the config, or
@@ -642,6 +644,40 @@ table with qualitative columns has no enabled list to name. These categories
642
644
  render as a name list rather than a one-column table on every surface. The
643
645
  `report.json` artifact is unaffected.
644
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
+
645
681
  ### Per-category row ordering
646
682
 
647
683
  Each table ships a sensible default ordering — largest backlog first, stalest
@@ -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.0'
22
+ __version_tuple__ = version_tuple = (0, 23, 0)
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
 
@@ -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
@@ -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
  )
@@ -11,6 +11,7 @@ with a usable error. Those parsers live here; the top-level loader in
11
11
 
12
12
  from __future__ import annotations
13
13
 
14
+ import re
14
15
  from collections.abc import Mapping
15
16
  from dataclasses import replace
16
17
  from types import MappingProxyType
@@ -227,6 +228,27 @@ def _require_reachable_warning(
227
228
  )
228
229
 
229
230
 
231
+ def _exclude_patterns_from(key: str, raw: list[str]) -> tuple[re.Pattern[str], ...]:
232
+ """Compile a category's ``exclude`` list, naming the pattern that fails.
233
+
234
+ Compiled once here rather than on every repository, and rejected at load
235
+ time rather than mid-run: a pattern that does not compile would otherwise
236
+ surface only after the whole organisation had been read. An oversized
237
+ repeat count raises ``OverflowError`` and deep nesting ``RecursionError``
238
+ rather than ``re.error``, so those are caught too.
239
+ """
240
+ patterns: list[re.Pattern[str]] = []
241
+ for text in raw:
242
+ try:
243
+ patterns.append(re.compile(text, re.IGNORECASE))
244
+ except (re.error, OverflowError, RecursionError) as exc:
245
+ raise ConfigError(
246
+ f"report.categories.{key}.exclude pattern {text!r} is not a "
247
+ f"valid regular expression: {exc}"
248
+ ) from exc
249
+ return tuple(patterns)
250
+
251
+
230
252
  def _categories_from(
231
253
  data: dict, base: Mapping[str, CategoryToggle]
232
254
  ) -> Mapping[str, CategoryToggle]:
@@ -267,5 +289,10 @@ def _categories_from(
267
289
  repo_list=(
268
290
  RepoList(raw["repo_list"]) if "repo_list" in raw else current.repo_list
269
291
  ),
292
+ exclude=(
293
+ _exclude_patterns_from(key, raw["exclude"])
294
+ if "exclude" in raw
295
+ else current.exclude
296
+ ),
270
297
  )
271
298
  return MappingProxyType(merged)
@@ -10,6 +10,7 @@ defaults they fall back to. Construction from raw JSON lives in
10
10
  from __future__ import annotations
11
11
 
12
12
  import datetime as dt
13
+ import re
13
14
  from collections.abc import Mapping
14
15
  from dataclasses import dataclass, field
15
16
  from types import MappingProxyType
@@ -94,7 +95,10 @@ class CategoryToggle:
94
95
  ordering of a generic table with a list of column names; ``None`` keeps the
95
96
  ordering the table's builder chose. ``repo_list`` overrides which side of a
96
97
  boolean feature category is named (see :class:`RepoList`); ``None`` falls
97
- back to the report-wide ``repo_list``.
98
+ back to the report-wide ``repo_list``. ``exclude`` drops the repositories
99
+ whose name matches any of its regular expressions from this one category
100
+ (see :data:`~github_security_report.categories.EXCLUDABLE_CATEGORIES`);
101
+ they are reported on its Excluded footer line rather than dropped silently.
98
102
  """
99
103
 
100
104
  enabled: bool = True
@@ -103,6 +107,7 @@ class CategoryToggle:
103
107
  top_n: int | None = None
104
108
  sort: tuple[str, ...] | None = None
105
109
  repo_list: RepoList | None = None
110
+ exclude: tuple[re.Pattern[str], ...] = ()
106
111
 
107
112
  def shows_on(self, output: str) -> bool:
108
113
  """Whether this category renders on ``output`` (cli/slack/markdown/html)."""
@@ -404,6 +409,17 @@ class ReportConfig:
404
409
  return toggle.repo_list
405
410
  return self.repo_list
406
411
 
412
+ def category_excludes(self, key: CategoryKey, name: str) -> bool:
413
+ """Whether category ``key`` drops repository ``name`` by its own list.
414
+
415
+ Patterns are searched rather than fully matched, so ``^test-`` reads
416
+ as the prefix it looks like, and compiled case-insensitively, since
417
+ GitHub repository names are. False for an unconfigured category: only
418
+ the organisation's ``exclude`` list removes a repository everywhere.
419
+ """
420
+ toggle = self.categories.get(key.value)
421
+ return toggle is not None and any(p.search(name) for p in toggle.exclude)
422
+
407
423
 
408
424
  @dataclass(frozen=True)
409
425
  class OrgConfig:
@@ -9,6 +9,7 @@ weekdays), the error raised on invalid input, and ``CONFIG_SCHEMA`` itself.
9
9
  from __future__ import annotations
10
10
 
11
11
  from github_security_report.categories import (
12
+ EXCLUDABLE_CATEGORIES,
12
13
  REPO_LIST_CATEGORIES,
13
14
  CategoryMeta,
14
15
  all_categories,
@@ -81,8 +82,9 @@ def _category_schema(meta: CategoryMeta) -> dict:
81
82
  """The schema for one category's block under ``report.categories``.
82
83
 
83
84
  ``repo_list`` is offered only to the boolean feature categories, the only
84
- ones with an enabled list to name: anywhere else it would validate and then
85
- do nothing, so it is left out and the schema rejects it by name.
85
+ ones with an enabled list to name, and ``exclude`` only to the categories
86
+ whose builders apply it: anywhere else either would validate and then do
87
+ nothing, so it is left out and the schema rejects it by name.
86
88
  """
87
89
  properties: dict = {
88
90
  "enabled": {"type": "boolean"},
@@ -110,6 +112,15 @@ def _category_schema(meta: CategoryMeta) -> dict:
110
112
  }
111
113
  if meta.key in REPO_LIST_CATEGORIES:
112
114
  properties["repo_list"] = {"enum": REPO_LIST_VALUES}
115
+ if meta.key in EXCLUDABLE_CATEGORIES:
116
+ # Regular expressions searched against the repository name, case-
117
+ # insensitively; a match drops the repository from this category only
118
+ # and lists it on the category's Excluded footer line. The loader
119
+ # rejects a pattern that does not compile, which JSON Schema cannot.
120
+ properties["exclude"] = {
121
+ "type": "array",
122
+ "items": {"type": "string", "minLength": 1},
123
+ }
113
124
  return {"type": "object", "additionalProperties": False, "properties": properties}
114
125
 
115
126
 
@@ -59,11 +59,11 @@ class OrgReport:
59
59
  # organisation's baseline: every category reports it unless
60
60
  # ``category_excluded`` gives that category a list of its own.
61
61
  excluded_repos: list[Repo] = field(default_factory=list)
62
- # Exclusions for a category that differ from the organisation baseline.
63
- # Absent keys inherit ``excluded_repos``, which today is every category: no
64
- # builder yet excludes repositories per category. It is the seam a
65
- # category-specific exclusion attaches to, and what the conditional-hide
66
- # footer setting compares against the baseline.
62
+ # Exclusions for a category that differ from the organisation baseline:
63
+ # the baseline plus the repositories the category's own ``exclude``
64
+ # patterns dropped (see ``categories.EXCLUDABLE_CATEGORIES``). Absent keys
65
+ # inherit ``excluded_repos``. This is what the conditional-hide footer
66
+ # setting compares against the baseline.
67
67
  category_excluded: dict[CategoryKey, list[Repo]] = field(default_factory=dict)
68
68
  # Extra Dependabot posture tables rendered as sub-sections beneath the
69
69
  # Dependabot signal heading (alerts not enabled, security updates not