github-security-report 0.21.1__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.21.1 → github_security_report-0.23.0}/PKG-INFO +53 -7
  2. {github_security_report-0.21.1 → github_security_report-0.23.0}/README.md +52 -6
  3. {github_security_report-0.21.1 → github_security_report-0.23.0}/src/github_security_report/_version.py +2 -2
  4. {github_security_report-0.21.1 → github_security_report-0.23.0}/src/github_security_report/categories/__init__.py +10 -0
  5. {github_security_report-0.21.1 → github_security_report-0.23.0}/src/github_security_report/cli/serialise.py +9 -0
  6. {github_security_report-0.21.1 → github_security_report-0.23.0}/src/github_security_report/collect/extras.py +30 -3
  7. {github_security_report-0.21.1 → github_security_report-0.23.0}/src/github_security_report/collect/workflow_runs.py +1 -0
  8. {github_security_report-0.21.1 → github_security_report-0.23.0}/src/github_security_report/config/blocks.py +28 -0
  9. {github_security_report-0.21.1 → github_security_report-0.23.0}/src/github_security_report/config/models.py +20 -1
  10. {github_security_report-0.21.1 → github_security_report-0.23.0}/src/github_security_report/config/schema.py +18 -2
  11. {github_security_report-0.21.1 → github_security_report-0.23.0}/src/github_security_report/malware.py +2 -2
  12. {github_security_report-0.21.1 → github_security_report-0.23.0}/src/github_security_report/report/aggregate.py +5 -5
  13. {github_security_report-0.21.1 → github_security_report-0.23.0}/src/github_security_report/workflow_runs/tables.py +11 -2
  14. {github_security_report-0.21.1 → github_security_report-0.23.0}/.gitignore +0 -0
  15. {github_security_report-0.21.1 → github_security_report-0.23.0}/LICENSE +0 -0
  16. {github_security_report-0.21.1 → github_security_report-0.23.0}/LICENSES/Apache-2.0.txt +0 -0
  17. {github_security_report-0.21.1 → github_security_report-0.23.0}/pyproject.toml +0 -0
  18. {github_security_report-0.21.1 → github_security_report-0.23.0}/scripts/README.md +0 -0
  19. {github_security_report-0.21.1 → github_security_report-0.23.0}/src/github_security_report/__init__.py +0 -0
  20. {github_security_report-0.21.1 → github_security_report-0.23.0}/src/github_security_report/authors.py +0 -0
  21. {github_security_report-0.21.1 → github_security_report-0.23.0}/src/github_security_report/categories/keys.py +0 -0
  22. {github_security_report-0.21.1 → github_security_report-0.23.0}/src/github_security_report/categories/signals.py +0 -0
  23. {github_security_report-0.21.1 → github_security_report-0.23.0}/src/github_security_report/categories/tables.py +0 -0
  24. {github_security_report-0.21.1 → github_security_report-0.23.0}/src/github_security_report/classify.py +0 -0
  25. {github_security_report-0.21.1 → github_security_report-0.23.0}/src/github_security_report/cli/__init__.py +0 -0
  26. {github_security_report-0.21.1 → github_security_report-0.23.0}/src/github_security_report/cli/__main__.py +0 -0
  27. {github_security_report-0.21.1 → github_security_report-0.23.0}/src/github_security_report/cli/app.py +0 -0
  28. {github_security_report-0.21.1 → github_security_report-0.23.0}/src/github_security_report/cli/boundary.py +0 -0
  29. {github_security_report-0.21.1 → github_security_report-0.23.0}/src/github_security_report/cli/modes.py +0 -0
  30. {github_security_report-0.21.1 → github_security_report-0.23.0}/src/github_security_report/cli/options.py +0 -0
  31. {github_security_report-0.21.1 → github_security_report-0.23.0}/src/github_security_report/cli/outputs.py +0 -0
  32. {github_security_report-0.21.1 → github_security_report-0.23.0}/src/github_security_report/cli/progress.py +0 -0
  33. {github_security_report-0.21.1 → github_security_report-0.23.0}/src/github_security_report/cli/publish.py +0 -0
  34. {github_security_report-0.21.1 → github_security_report-0.23.0}/src/github_security_report/cli/remediation.py +0 -0
  35. {github_security_report-0.21.1 → github_security_report-0.23.0}/src/github_security_report/cli/summary.py +0 -0
  36. {github_security_report-0.21.1 → github_security_report-0.23.0}/src/github_security_report/client/__init__.py +0 -0
  37. {github_security_report-0.21.1 → github_security_report-0.23.0}/src/github_security_report/client/accounting.py +0 -0
  38. {github_security_report-0.21.1 → github_security_report-0.23.0}/src/github_security_report/client/alerts.py +0 -0
  39. {github_security_report-0.21.1 → github_security_report-0.23.0}/src/github_security_report/client/analyses.py +0 -0
  40. {github_security_report-0.21.1 → github_security_report-0.23.0}/src/github_security_report/client/batch_errors.py +0 -0
  41. {github_security_report-0.21.1 → github_security_report-0.23.0}/src/github_security_report/client/codeql_parsers.py +0 -0
  42. {github_security_report-0.21.1 → github_security_report-0.23.0}/src/github_security_report/client/copilot.py +0 -0
  43. {github_security_report-0.21.1 → github_security_report-0.23.0}/src/github_security_report/client/endpoints.py +0 -0
  44. {github_security_report-0.21.1 → github_security_report-0.23.0}/src/github_security_report/client/errors.py +0 -0
  45. {github_security_report-0.21.1 → github_security_report-0.23.0}/src/github_security_report/client/events.py +0 -0
  46. {github_security_report-0.21.1 → github_security_report-0.23.0}/src/github_security_report/client/failures.py +0 -0
  47. {github_security_report-0.21.1 → github_security_report-0.23.0}/src/github_security_report/client/org_reads.py +0 -0
  48. {github_security_report-0.21.1 → github_security_report-0.23.0}/src/github_security_report/client/paging.py +0 -0
  49. {github_security_report-0.21.1 → github_security_report-0.23.0}/src/github_security_report/client/parsers.py +0 -0
  50. {github_security_report-0.21.1 → github_security_report-0.23.0}/src/github_security_report/client/queries.py +0 -0
  51. {github_security_report-0.21.1 → github_security_report-0.23.0}/src/github_security_report/client/reads.py +0 -0
  52. {github_security_report-0.21.1 → github_security_report-0.23.0}/src/github_security_report/client/repo_parser.py +0 -0
  53. {github_security_report-0.21.1 → github_security_report-0.23.0}/src/github_security_report/client/reviews.py +0 -0
  54. {github_security_report-0.21.1 → github_security_report-0.23.0}/src/github_security_report/client/transport.py +0 -0
  55. {github_security_report-0.21.1 → github_security_report-0.23.0}/src/github_security_report/client/workflow_runs.py +0 -0
  56. {github_security_report-0.21.1 → github_security_report-0.23.0}/src/github_security_report/client/writes.py +0 -0
  57. {github_security_report-0.21.1 → github_security_report-0.23.0}/src/github_security_report/code_quality.py +0 -0
  58. {github_security_report-0.21.1 → github_security_report-0.23.0}/src/github_security_report/codeql/__init__.py +0 -0
  59. {github_security_report-0.21.1 → github_security_report-0.23.0}/src/github_security_report/codeql/cleanup.py +0 -0
  60. {github_security_report-0.21.1 → github_security_report-0.23.0}/src/github_security_report/codeql/facts.py +0 -0
  61. {github_security_report-0.21.1 → github_security_report-0.23.0}/src/github_security_report/codeql/tables.py +0 -0
  62. {github_security_report-0.21.1 → github_security_report-0.23.0}/src/github_security_report/collect/__init__.py +0 -0
  63. {github_security_report-0.21.1 → github_security_report-0.23.0}/src/github_security_report/collect/codeql.py +0 -0
  64. {github_security_report-0.21.1 → github_security_report-0.23.0}/src/github_security_report/collect/context.py +0 -0
  65. {github_security_report-0.21.1 → github_security_report-0.23.0}/src/github_security_report/collect/graph.py +0 -0
  66. {github_security_report-0.21.1 → github_security_report-0.23.0}/src/github_security_report/collect/org.py +0 -0
  67. {github_security_report-0.21.1 → github_security_report-0.23.0}/src/github_security_report/collect/protocols.py +0 -0
  68. {github_security_report-0.21.1 → github_security_report-0.23.0}/src/github_security_report/collect/repo.py +0 -0
  69. {github_security_report-0.21.1 → github_security_report-0.23.0}/src/github_security_report/collect/run_sync.py +0 -0
  70. {github_security_report-0.21.1 → github_security_report-0.23.0}/src/github_security_report/collect/scoping.py +0 -0
  71. {github_security_report-0.21.1 → github_security_report-0.23.0}/src/github_security_report/config/__init__.py +0 -0
  72. {github_security_report-0.21.1 → github_security_report-0.23.0}/src/github_security_report/config/loader.py +0 -0
  73. {github_security_report-0.21.1 → github_security_report-0.23.0}/src/github_security_report/config/order.py +0 -0
  74. {github_security_report-0.21.1 → github_security_report-0.23.0}/src/github_security_report/gating.py +0 -0
  75. {github_security_report-0.21.1 → github_security_report-0.23.0}/src/github_security_report/gitctx.py +0 -0
  76. {github_security_report-0.21.1 → github_security_report-0.23.0}/src/github_security_report/issue_presentation.py +0 -0
  77. {github_security_report-0.21.1 → github_security_report-0.23.0}/src/github_security_report/issues.py +0 -0
  78. {github_security_report-0.21.1 → github_security_report-0.23.0}/src/github_security_report/layout.py +0 -0
  79. {github_security_report-0.21.1 → github_security_report-0.23.0}/src/github_security_report/models.py +0 -0
  80. {github_security_report-0.21.1 → github_security_report-0.23.0}/src/github_security_report/ordering.py +0 -0
  81. {github_security_report-0.21.1 → github_security_report-0.23.0}/src/github_security_report/posture/__init__.py +0 -0
  82. {github_security_report-0.21.1 → github_security_report-0.23.0}/src/github_security_report/posture/enablement.py +0 -0
  83. {github_security_report-0.21.1 → github_security_report-0.23.0}/src/github_security_report/posture/facts.py +0 -0
  84. {github_security_report-0.21.1 → github_security_report-0.23.0}/src/github_security_report/posture/quality.py +0 -0
  85. {github_security_report-0.21.1 → github_security_report-0.23.0}/src/github_security_report/posture/releases.py +0 -0
  86. {github_security_report-0.21.1 → github_security_report-0.23.0}/src/github_security_report/pulls/__init__.py +0 -0
  87. {github_security_report-0.21.1 → github_security_report-0.23.0}/src/github_security_report/pulls/columns.py +0 -0
  88. {github_security_report-0.21.1 → github_security_report-0.23.0}/src/github_security_report/pulls/counting.py +0 -0
  89. {github_security_report-0.21.1 → github_security_report-0.23.0}/src/github_security_report/pulls/presentation.py +0 -0
  90. {github_security_report-0.21.1 → github_security_report-0.23.0}/src/github_security_report/pulls/table.py +0 -0
  91. {github_security_report-0.21.1 → github_security_report-0.23.0}/src/github_security_report/py.typed +0 -0
  92. {github_security_report-0.21.1 → github_security_report-0.23.0}/src/github_security_report/ranking.py +0 -0
  93. {github_security_report-0.21.1 → github_security_report-0.23.0}/src/github_security_report/remediate/__init__.py +0 -0
  94. {github_security_report-0.21.1 → github_security_report-0.23.0}/src/github_security_report/remediate/codeql.py +0 -0
  95. {github_security_report-0.21.1 → github_security_report-0.23.0}/src/github_security_report/remediate/model.py +0 -0
  96. {github_security_report-0.21.1 → github_security_report-0.23.0}/src/github_security_report/render/__init__.py +0 -0
  97. {github_security_report-0.21.1 → github_security_report-0.23.0}/src/github_security_report/render/html.py +0 -0
  98. {github_security_report-0.21.1 → github_security_report-0.23.0}/src/github_security_report/render/markdown.py +0 -0
  99. {github_security_report-0.21.1 → github_security_report-0.23.0}/src/github_security_report/render/slack.py +0 -0
  100. {github_security_report-0.21.1 → github_security_report-0.23.0}/src/github_security_report/render/slack_limits.py +0 -0
  101. {github_security_report-0.21.1 → github_security_report-0.23.0}/src/github_security_report/render/slack_text.py +0 -0
  102. {github_security_report-0.21.1 → github_security_report-0.23.0}/src/github_security_report/render/terminal.py +0 -0
  103. {github_security_report-0.21.1 → github_security_report-0.23.0}/src/github_security_report/render/terminal_notices.py +0 -0
  104. {github_security_report-0.21.1 → github_security_report-0.23.0}/src/github_security_report/report/__init__.py +0 -0
  105. {github_security_report-0.21.1 → github_security_report-0.23.0}/src/github_security_report/report/display.py +0 -0
  106. {github_security_report-0.21.1 → github_security_report-0.23.0}/src/github_security_report/report/signals.py +0 -0
  107. {github_security_report-0.21.1 → github_security_report-0.23.0}/src/github_security_report/report/tables.py +0 -0
  108. {github_security_report-0.21.1 → github_security_report-0.23.0}/src/github_security_report/rulesets.py +0 -0
  109. {github_security_report-0.21.1 → github_security_report-0.23.0}/src/github_security_report/runner.py +0 -0
  110. {github_security_report-0.21.1 → github_security_report-0.23.0}/src/github_security_report/scope.py +0 -0
  111. {github_security_report-0.21.1 → github_security_report-0.23.0}/src/github_security_report/secret_patterns.py +0 -0
  112. {github_security_report-0.21.1 → github_security_report-0.23.0}/src/github_security_report/severity.py +0 -0
  113. {github_security_report-0.21.1 → github_security_report-0.23.0}/src/github_security_report/summary.py +0 -0
  114. {github_security_report-0.21.1 → github_security_report-0.23.0}/src/github_security_report/templates/index.html.j2 +0 -0
  115. {github_security_report-0.21.1 → github_security_report-0.23.0}/src/github_security_report/templates/report.html.j2 +0 -0
  116. {github_security_report-0.21.1 → github_security_report-0.23.0}/src/github_security_report/workflow_runs/__init__.py +0 -0
  117. {github_security_report-0.21.1 → github_security_report-0.23.0}/src/github_security_report/workflow_runs/aggregate.py +0 -0
  118. {github_security_report-0.21.1 → github_security_report-0.23.0}/src/github_security_report/workflow_runs/models.py +0 -0
  119. {github_security_report-0.21.1 → github_security_report-0.23.0}/src/github_security_report/workflow_runs/store.py +0 -0
  120. {github_security_report-0.21.1 → github_security_report-0.23.0}/src/github_security_report/workflow_runs/store_codec.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: github-security-report
3
- Version: 0.21.1
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
@@ -209,7 +210,9 @@ tables (org mode):
209
210
  pull request could have caught them. Each row gives **Failed** (runs that
210
211
  concluded `failure`, `startup_failure`, `timed_out` or `action_required`),
211
212
  **Runs** (every concluded run of every workflow in the repository),
212
- **Fail %** (failed over failed *plus passed*, so skips never dilute it),
213
+ **Fail %** (failed over failed *plus passed*, so skips never dilute it;
214
+ on the terminal, red above `workflow_runs.fail_rate_error_threshold`, 5%
215
+ by default, and yellow for any lower non-zero rate),
213
216
  **Skipped** (skipped, cancelled, neutral and stale runs — a cancelled run
214
217
  reached no verdict, and a bulk-merge round cancels hundreds of superseded
215
218
  ones), **Retried** (runs that passed on a later attempt, a direct sign of
@@ -530,10 +533,11 @@ governs that line on every surface:
530
533
 
531
534
  Hiding the line changes nothing else: every count is unaffected, and the report
532
535
  header's repository count already leaves excluded repositories out. So a clean
533
- category reads `All Clean` once no Excluded line qualifies it. Today every
534
- category reports the organisation's own list, so `conditional-hide` currently
535
- hides the same lines `always-hide` does. It differs once a category excludes
536
- 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.
537
541
 
538
542
  Archived and test repositories are excluded from analysis by default. Opt them
539
543
  back in with `report.include_archived` / `report.include_test` in the config, or
@@ -684,6 +688,40 @@ table with qualitative columns has no enabled list to name. These categories
684
688
  render as a name list rather than a one-column table on every surface. The
685
689
  `report.json` artifact is unaffected.
686
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
+
687
725
  ### Per-category row ordering
688
726
 
689
727
  Each table ships a sensible default ordering — largest backlog first, stalest
@@ -1618,6 +1656,7 @@ like every other `report` key. Every key with its default:
1618
1656
  "collapse_matrix": true,
1619
1657
  "persistent_threshold": 0.9,
1620
1658
  "min_runs": 3,
1659
+ "fail_rate_error_threshold": 0.05,
1621
1660
  "cache": true
1622
1661
  }
1623
1662
  },
@@ -1659,6 +1698,13 @@ fewer than `min_runs` runs that reached a verdict *insufficient* to say. Both
1659
1698
  judge the same pass-or-fail population that Fail % does, so skips count
1660
1699
  towards neither.
1661
1700
 
1701
+ `fail_rate_error_threshold` colours the per-repository table's **Fail %**
1702
+ on the terminal: a repository failing more than that fraction of its
1703
+ pass-or-fail runs shows red, and any lower non-zero rate shows yellow.
1704
+ Markdown, HTML and Slack carry no cell colour, so the setting leaves them
1705
+ unchanged. It is independent of `persistent_threshold`, which judges
1706
+ single workflows.
1707
+
1662
1708
  `cache` and `cache_dir` govern the **run store**, a SQLite file per
1663
1709
  organisation at
1664
1710
 
@@ -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
@@ -165,7 +166,9 @@ tables (org mode):
165
166
  pull request could have caught them. Each row gives **Failed** (runs that
166
167
  concluded `failure`, `startup_failure`, `timed_out` or `action_required`),
167
168
  **Runs** (every concluded run of every workflow in the repository),
168
- **Fail %** (failed over failed *plus passed*, so skips never dilute it),
169
+ **Fail %** (failed over failed *plus passed*, so skips never dilute it;
170
+ on the terminal, red above `workflow_runs.fail_rate_error_threshold`, 5%
171
+ by default, and yellow for any lower non-zero rate),
169
172
  **Skipped** (skipped, cancelled, neutral and stale runs — a cancelled run
170
173
  reached no verdict, and a bulk-merge round cancels hundreds of superseded
171
174
  ones), **Retried** (runs that passed on a later attempt, a direct sign of
@@ -486,10 +489,11 @@ governs that line on every surface:
486
489
 
487
490
  Hiding the line changes nothing else: every count is unaffected, and the report
488
491
  header's repository count already leaves excluded repositories out. So a clean
489
- category reads `All Clean` once no Excluded line qualifies it. Today every
490
- category reports the organisation's own list, so `conditional-hide` currently
491
- hides the same lines `always-hide` does. It differs once a category excludes
492
- 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.
493
497
 
494
498
  Archived and test repositories are excluded from analysis by default. Opt them
495
499
  back in with `report.include_archived` / `report.include_test` in the config, or
@@ -640,6 +644,40 @@ table with qualitative columns has no enabled list to name. These categories
640
644
  render as a name list rather than a one-column table on every surface. The
641
645
  `report.json` artifact is unaffected.
642
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
+
643
681
  ### Per-category row ordering
644
682
 
645
683
  Each table ships a sensible default ordering — largest backlog first, stalest
@@ -1574,6 +1612,7 @@ like every other `report` key. Every key with its default:
1574
1612
  "collapse_matrix": true,
1575
1613
  "persistent_threshold": 0.9,
1576
1614
  "min_runs": 3,
1615
+ "fail_rate_error_threshold": 0.05,
1577
1616
  "cache": true
1578
1617
  }
1579
1618
  },
@@ -1615,6 +1654,13 @@ fewer than `min_runs` runs that reached a verdict *insufficient* to say. Both
1615
1654
  judge the same pass-or-fail population that Fail % does, so skips count
1616
1655
  towards neither.
1617
1656
 
1657
+ `fail_rate_error_threshold` colours the per-repository table's **Fail %**
1658
+ on the terminal: a repository failing more than that fraction of its
1659
+ pass-or-fail runs shows red, and any lower non-zero rate shows yellow.
1660
+ Markdown, HTML and Slack carry no cell colour, so the setting leaves them
1661
+ unchanged. It is independent of `persistent_threshold`, which judges
1662
+ single workflows.
1663
+
1618
1664
  `cache` and `cache_dir` govern the **run store**, a SQLite file per
1619
1665
  organisation at
1620
1666
 
@@ -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.21.1'
22
- __version_tuple__ = version_tuple = (0, 21, 1)
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
  )
@@ -316,5 +316,6 @@ async def attach_workflow_failures(
316
316
  ),
317
317
  unknown=result.unknown,
318
318
  oldest=result.oldest,
319
+ fail_rate_error_threshold=cfg.fail_rate_error_threshold,
319
320
  )
320
321
  )
@@ -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
@@ -178,6 +179,7 @@ def _workflow_runs_from(data: dict, base: WorkflowRunsConfig) -> WorkflowRunsCon
178
179
  "collapse_matrix",
179
180
  "persistent_threshold",
180
181
  "min_runs",
182
+ "fail_rate_error_threshold",
181
183
  "cache",
182
184
  "cache_dir",
183
185
  }
@@ -226,6 +228,27 @@ def _require_reachable_warning(
226
228
  )
227
229
 
228
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
+
229
252
  def _categories_from(
230
253
  data: dict, base: Mapping[str, CategoryToggle]
231
254
  ) -> Mapping[str, CategoryToggle]:
@@ -266,5 +289,10 @@ def _categories_from(
266
289
  repo_list=(
267
290
  RepoList(raw["repo_list"]) if "repo_list" in raw else current.repo_list
268
291
  ),
292
+ exclude=(
293
+ _exclude_patterns_from(key, raw["exclude"])
294
+ if "exclude" in raw
295
+ else current.exclude
296
+ ),
269
297
  )
270
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)."""
@@ -188,6 +193,9 @@ class WorkflowRunsConfig:
188
193
  # intermittent. Fewer than ``min_runs`` concluded runs is too few to say.
189
194
  persistent_threshold: float = 0.9
190
195
  min_runs: int = 3
196
+ # A repository failing more than this share of its runs has its Fail %
197
+ # shown as an error; any lower non-zero rate is a warning.
198
+ fail_rate_error_threshold: float = 0.05
191
199
  cache: bool = True
192
200
  cache_dir: str | None = None
193
201
 
@@ -401,6 +409,17 @@ class ReportConfig:
401
409
  return toggle.repo_list
402
410
  return self.repo_list
403
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
+
404
423
 
405
424
  @dataclass(frozen=True)
406
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
 
@@ -274,6 +285,11 @@ CONFIG_SCHEMA: dict = {
274
285
  "maximum": 1,
275
286
  },
276
287
  "min_runs": {"type": "integer", "minimum": 1},
288
+ "fail_rate_error_threshold": {
289
+ "type": "number",
290
+ "minimum": 0,
291
+ "maximum": 1,
292
+ },
277
293
  "cache": {"type": "boolean"},
278
294
  "cache_dir": {"type": "string", "minLength": 1},
279
295
  },
@@ -29,8 +29,8 @@ _NUMERIC_COLUMNS = frozenset({4, 5})
29
29
  # A headline and its detail, split by a newline: each surface sets the detail
30
30
  # on a line of its own under the headline (see the summary renderers).
31
31
  COVERAGE_NOTE = (
32
- "Malware alert enablement/status NOT currently available through APIs\n"
33
- "Repositories without findings may have the feature off so coverage unknown"
32
+ "Enablement status of malware detection NOT currently available via APIs\n"
33
+ "Coverage unknown; repositories without findings may have the feature off"
34
34
  )
35
35
 
36
36
 
@@ -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
@@ -84,8 +84,13 @@ def _percent(rate: float) -> str:
84
84
  return f"{round(rate * 100)}%"
85
85
 
86
86
 
87
+ # The repository failure rate above which Fail % reads as an error.
88
+ DEFAULT_FAIL_RATE_ERROR_THRESHOLD = 0.05
89
+
90
+
87
91
  def _rate_level(rate: float, threshold: float) -> str | None:
88
- if rate >= threshold:
92
+ """Error strictly above ``threshold``, warning for any lower failure."""
93
+ if rate > threshold:
89
94
  return CELL_BAD
90
95
  return CELL_WARN if rate > 0 else None
91
96
 
@@ -256,6 +261,7 @@ def build_workflow_failure_tables(
256
261
  options: WindowOptions,
257
262
  unknown: frozenset[str] = frozenset(),
258
263
  oldest: Mapping[str, dt.datetime | None] | None = None,
264
+ fail_rate_error_threshold: float = DEFAULT_FAIL_RATE_ERROR_THRESHOLD,
259
265
  ) -> tuple[tuple[TableSection, ...], tuple[TableSection, ...]]:
260
266
  """Both tables for every window, shortest window first.
261
267
 
@@ -264,6 +270,9 @@ def build_workflow_failure_tables(
264
270
  as unreadable rather than as failure-free on every window; ``oldest``
265
271
  says how far back each repository's stored history reaches, so a window
266
272
  reaching further is noted as truncated in the footer.
273
+ ``fail_rate_error_threshold`` is the repository failure rate above which
274
+ the Fail % cell reads as an error rather than a warning; the persistent
275
+ threshold in ``options`` judges only the detail table's Pattern.
267
276
  """
268
277
  by_name = {repo.name: repo for repo in in_scope}
269
278
  held = list(records)
@@ -305,7 +314,7 @@ def build_workflow_failure_tables(
305
314
  category=category_meta(CategoryKey.WORKFLOW_FAILURES),
306
315
  columns=REPO_COLUMNS,
307
316
  rows=[
308
- _repo_row(by_name[s.repo], s, options.persistent_threshold)
317
+ _repo_row(by_name[s.repo], s, fail_rate_error_threshold)
309
318
  for s in failing
310
319
  ],
311
320
  sum_columns=frozenset({1, 2, 4, 5, 6}),