github-security-report 0.9.0__tar.gz → 0.10.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 (33) hide show
  1. {github_security_report-0.9.0 → github_security_report-0.10.0}/PKG-INFO +1 -1
  2. {github_security_report-0.9.0 → github_security_report-0.10.0}/pyproject.toml +4 -0
  3. {github_security_report-0.9.0 → github_security_report-0.10.0}/src/github_security_report/_version.py +2 -2
  4. {github_security_report-0.9.0 → github_security_report-0.10.0}/src/github_security_report/categories.py +2 -1
  5. {github_security_report-0.9.0 → github_security_report-0.10.0}/src/github_security_report/models.py +65 -14
  6. {github_security_report-0.9.0 → github_security_report-0.10.0}/src/github_security_report/severity.py +12 -0
  7. {github_security_report-0.9.0 → github_security_report-0.10.0}/.gitignore +0 -0
  8. {github_security_report-0.9.0 → github_security_report-0.10.0}/LICENSE +0 -0
  9. {github_security_report-0.9.0 → github_security_report-0.10.0}/LICENSES/Apache-2.0.txt +0 -0
  10. {github_security_report-0.9.0 → github_security_report-0.10.0}/README.md +0 -0
  11. {github_security_report-0.9.0 → github_security_report-0.10.0}/scripts/README.md +0 -0
  12. {github_security_report-0.9.0 → github_security_report-0.10.0}/src/github_security_report/__init__.py +0 -0
  13. {github_security_report-0.9.0 → github_security_report-0.10.0}/src/github_security_report/classify.py +0 -0
  14. {github_security_report-0.9.0 → github_security_report-0.10.0}/src/github_security_report/cli.py +0 -0
  15. {github_security_report-0.9.0 → github_security_report-0.10.0}/src/github_security_report/client.py +0 -0
  16. {github_security_report-0.9.0 → github_security_report-0.10.0}/src/github_security_report/collect.py +0 -0
  17. {github_security_report-0.9.0 → github_security_report-0.10.0}/src/github_security_report/config.py +0 -0
  18. {github_security_report-0.9.0 → github_security_report-0.10.0}/src/github_security_report/gating.py +0 -0
  19. {github_security_report-0.9.0 → github_security_report-0.10.0}/src/github_security_report/gitctx.py +0 -0
  20. {github_security_report-0.9.0 → github_security_report-0.10.0}/src/github_security_report/posture.py +0 -0
  21. {github_security_report-0.9.0 → github_security_report-0.10.0}/src/github_security_report/py.typed +0 -0
  22. {github_security_report-0.9.0 → github_security_report-0.10.0}/src/github_security_report/remediate.py +0 -0
  23. {github_security_report-0.9.0 → github_security_report-0.10.0}/src/github_security_report/render/__init__.py +0 -0
  24. {github_security_report-0.9.0 → github_security_report-0.10.0}/src/github_security_report/render/html.py +0 -0
  25. {github_security_report-0.9.0 → github_security_report-0.10.0}/src/github_security_report/render/markdown.py +0 -0
  26. {github_security_report-0.9.0 → github_security_report-0.10.0}/src/github_security_report/render/slack.py +0 -0
  27. {github_security_report-0.9.0 → github_security_report-0.10.0}/src/github_security_report/render/terminal.py +0 -0
  28. {github_security_report-0.9.0 → github_security_report-0.10.0}/src/github_security_report/report.py +0 -0
  29. {github_security_report-0.9.0 → github_security_report-0.10.0}/src/github_security_report/rulesets.py +0 -0
  30. {github_security_report-0.9.0 → github_security_report-0.10.0}/src/github_security_report/runner.py +0 -0
  31. {github_security_report-0.9.0 → github_security_report-0.10.0}/src/github_security_report/scope.py +0 -0
  32. {github_security_report-0.9.0 → github_security_report-0.10.0}/src/github_security_report/templates/index.html.j2 +0 -0
  33. {github_security_report-0.9.0 → github_security_report-0.10.0}/src/github_security_report/templates/report.html.j2 +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: github-security-report
3
- Version: 0.9.0
3
+ Version: 0.10.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
@@ -136,6 +136,10 @@ skip_empty = true
136
136
 
137
137
  [tool.mypy]
138
138
  python_version = "3.10"
139
+ # The whole first-party tree, so a bare 'mypy' checks what the pre-commit hook
140
+ # checks. Tests are deliberately included: they are held to the same standard
141
+ # as the package.
142
+ files = ["src", "tests", "scripts"]
139
143
  warn_return_any = true
140
144
  warn_unused_configs = true
141
145
  disallow_untyped_defs = false
@@ -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.9.0'
22
- __version_tuple__ = version_tuple = (0, 9, 0)
21
+ __version__ = version = '0.10.0'
22
+ __version_tuple__ = version_tuple = (0, 10, 0)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -93,7 +93,8 @@ _CATEGORIES: dict[CategoryKey, CategoryMeta] = {
93
93
  url="https://github.com/ossf/scorecard",
94
94
  description=(
95
95
  "OpenSSF Scorecard supply-chain health scores (a lower score is "
96
- "weaker), ranked weakest-first."
96
+ "weaker). Ranked by the worst severity rung present in the table "
97
+ "(most findings at that rung first), then weakest score first."
97
98
  ),
98
99
  ),
99
100
  CategoryKey.ZIZMOR: CategoryMeta(
@@ -5,8 +5,8 @@
5
5
  Encodes the Phase 0 design (see ``docs/BRIEF.md`` and
6
6
  ``docs/phase0-findings.md``): the six ranked signals, the four-state per-report
7
7
  classification, severity counts with hierarchical worst-first ordering, and the
8
- ranking rules (alert tables sort by severity descending; Scorecard by score
9
- ascending).
8
+ ranking rules (alert tables sort by severity descending; Scorecard by its worst
9
+ populated severity rung descending, then by score ascending).
10
10
  """
11
11
 
12
12
  from __future__ import annotations
@@ -16,7 +16,7 @@ from dataclasses import dataclass, field
16
16
  from enum import Enum
17
17
 
18
18
  from github_security_report.categories import CategoryKey, CategoryMeta, category_meta
19
- from github_security_report.severity import Severity
19
+ from github_security_report.severity import RUNGS_WORST_FIRST, Severity
20
20
 
21
21
 
22
22
  class SignalType(str, Enum):
@@ -66,7 +66,13 @@ class SignalType(str, Enum):
66
66
 
67
67
  @property
68
68
  def sort_ascending(self) -> bool:
69
- """Scorecard ranks by score ascending (lower == worse); others descend."""
69
+ """Whether lower is worse for this signal's primary metric.
70
+
71
+ True only for Scorecard, whose aggregate score runs the opposite way to
72
+ a finding count (lower == weaker). The score is Scorecard's *secondary*
73
+ key: ``rank_offenders`` leads on the worst severity rung present in the
74
+ table and uses the score to order repositories within that rung.
75
+ """
70
76
  return self is SignalType.SCORECARD
71
77
 
72
78
 
@@ -177,6 +183,22 @@ class SeverityCounts:
177
183
  def total(self) -> int:
178
184
  return self.critical + self.high + self.medium + self.low + self.informational
179
185
 
186
+ def at(self, rung: Severity) -> int:
187
+ """The count at exactly one severity rung.
188
+
189
+ Attribute access rather than a ``by_rung`` lookup, so the ranking hot
190
+ path does not build a dict per comparison.
191
+ """
192
+ if rung is Severity.CRITICAL:
193
+ return self.critical
194
+ if rung is Severity.HIGH:
195
+ return self.high
196
+ if rung is Severity.MEDIUM:
197
+ return self.medium
198
+ if rung is Severity.LOW:
199
+ return self.low
200
+ return self.informational
201
+
180
202
  def at_or_above(self, cutoff: Severity) -> int:
181
203
  """Count of findings whose severity is at least ``cutoff``.
182
204
 
@@ -185,14 +207,12 @@ class SeverityCounts:
185
207
  category's ``fail_severity`` cutoff. Findings below the cutoff (e.g.
186
208
  informational-only) do not count towards a failure.
187
209
  """
188
- by_rung = {
189
- Severity.CRITICAL: self.critical,
190
- Severity.HIGH: self.high,
191
- Severity.MEDIUM: self.medium,
192
- Severity.LOW: self.low,
193
- Severity.INFORMATIONAL: self.informational,
194
- }
195
- return sum(count for rung, count in by_rung.items() if rung >= cutoff)
210
+ return sum(count for rung, count in self.by_rung.items() if rung >= cutoff)
211
+
212
+ @property
213
+ def by_rung(self) -> dict[Severity, int]:
214
+ """Per-severity counts keyed by rung, in worst-first iteration order."""
215
+ return {rung: self.at(rung) for rung in RUNGS_WORST_FIRST}
196
216
 
197
217
  @property
198
218
  def weighted(self) -> int:
@@ -236,12 +256,41 @@ class RepoSignal:
236
256
  return self.state is RepoState.OFFENDER
237
257
 
238
258
 
259
+ # The rungs eligible to lead the Scorecard ordering, worst-first.
260
+ # ``INFORMATIONAL`` is deliberately absent: it is the non-actionable rung, so it
261
+ # never displaces the score as the primary key.
262
+ LEAD_RUNGS: tuple[Severity, ...] = tuple(
263
+ rung for rung in RUNGS_WORST_FIRST if rung is not Severity.INFORMATIONAL
264
+ )
265
+
266
+
267
+ def lead_rung(offenders: list[RepoSignal]) -> Severity | None:
268
+ """The worst severity rung any offender actually carries.
269
+
270
+ Returns ``None`` when no offender carries a finding at Low or above, in
271
+ which case there is no severity tier worth leading on.
272
+ """
273
+ return next(
274
+ (rung for rung in LEAD_RUNGS if any(s.counts.at(rung) for s in offenders)),
275
+ None,
276
+ )
277
+
278
+
239
279
  def rank_offenders(signals: list[RepoSignal]) -> list[RepoSignal]:
240
280
  """Sort offenders worst-first for a single signal.
241
281
 
242
282
  Alert-based signals sort by the hierarchical severity key descending, with
243
- total as a tiebreaker. Scorecard sorts by aggregate score ascending (lower
244
- == worse). Repo name breaks remaining ties, ascending.
283
+ total as a tiebreaker.
284
+
285
+ Scorecard sorts on two tiers: the count at the worst severity rung present
286
+ anywhere in the table (descending), then the aggregate score (ascending,
287
+ lower == worse). The leading rung cascades -- Critical, else High, else
288
+ Medium, else Low -- so the rung that actually discriminates between
289
+ repositories leads, and a lone Critical can never be buried mid-table by a
290
+ weaker repository with a lower score. When no offender carries a finding at
291
+ Low or above, the score alone orders the table.
292
+
293
+ Repo name breaks remaining ties, ascending.
245
294
 
246
295
  Numeric components are negated so the whole sort runs ascending (no
247
296
  ``reverse=True``); that keeps the name tiebreaker correctly ascending even
@@ -252,9 +301,11 @@ def rank_offenders(signals: list[RepoSignal]) -> list[RepoSignal]:
252
301
  return []
253
302
  signal = offenders[0].signal
254
303
  if signal.sort_ascending:
304
+ rung = lead_rung(offenders)
255
305
  return sorted(
256
306
  offenders,
257
307
  key=lambda s: (
308
+ -s.counts.at(rung) if rung is not None else 0,
258
309
  s.score if s.score is not None else float("inf"),
259
310
  s.repo.name,
260
311
  ),
@@ -52,6 +52,18 @@ class Severity(IntEnum):
52
52
  return self.name.lower()
53
53
 
54
54
 
55
+ # The canonical worst-first rung order, for the severity columns and for every
56
+ # worst-first ranking. Declared once so display order and ranking order cannot
57
+ # drift apart.
58
+ RUNGS_WORST_FIRST: tuple[Severity, ...] = (
59
+ Severity.CRITICAL,
60
+ Severity.HIGH,
61
+ Severity.MEDIUM,
62
+ Severity.LOW,
63
+ Severity.INFORMATIONAL,
64
+ )
65
+
66
+
55
67
  # Direct names on the security-severity scale (CodeQL, Scorecard, Dependabot).
56
68
  _SECURITY_NAMES: dict[str, Severity] = {
57
69
  "critical": Severity.CRITICAL,