github-security-report 0.8.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.
- {github_security_report-0.8.0 → github_security_report-0.10.0}/PKG-INFO +3 -3
- {github_security_report-0.8.0 → github_security_report-0.10.0}/pyproject.toml +7 -3
- {github_security_report-0.8.0 → github_security_report-0.10.0}/src/github_security_report/_version.py +2 -2
- {github_security_report-0.8.0 → github_security_report-0.10.0}/src/github_security_report/categories.py +17 -8
- {github_security_report-0.8.0 → github_security_report-0.10.0}/src/github_security_report/models.py +65 -14
- {github_security_report-0.8.0 → github_security_report-0.10.0}/src/github_security_report/severity.py +26 -9
- {github_security_report-0.8.0 → github_security_report-0.10.0}/.gitignore +0 -0
- {github_security_report-0.8.0 → github_security_report-0.10.0}/LICENSE +0 -0
- {github_security_report-0.8.0 → github_security_report-0.10.0}/LICENSES/Apache-2.0.txt +0 -0
- {github_security_report-0.8.0 → github_security_report-0.10.0}/README.md +0 -0
- {github_security_report-0.8.0 → github_security_report-0.10.0}/scripts/README.md +0 -0
- {github_security_report-0.8.0 → github_security_report-0.10.0}/src/github_security_report/__init__.py +0 -0
- {github_security_report-0.8.0 → github_security_report-0.10.0}/src/github_security_report/classify.py +0 -0
- {github_security_report-0.8.0 → github_security_report-0.10.0}/src/github_security_report/cli.py +0 -0
- {github_security_report-0.8.0 → github_security_report-0.10.0}/src/github_security_report/client.py +0 -0
- {github_security_report-0.8.0 → github_security_report-0.10.0}/src/github_security_report/collect.py +0 -0
- {github_security_report-0.8.0 → github_security_report-0.10.0}/src/github_security_report/config.py +0 -0
- {github_security_report-0.8.0 → github_security_report-0.10.0}/src/github_security_report/gating.py +0 -0
- {github_security_report-0.8.0 → github_security_report-0.10.0}/src/github_security_report/gitctx.py +0 -0
- {github_security_report-0.8.0 → github_security_report-0.10.0}/src/github_security_report/posture.py +0 -0
- {github_security_report-0.8.0 → github_security_report-0.10.0}/src/github_security_report/py.typed +0 -0
- {github_security_report-0.8.0 → github_security_report-0.10.0}/src/github_security_report/remediate.py +0 -0
- {github_security_report-0.8.0 → github_security_report-0.10.0}/src/github_security_report/render/__init__.py +0 -0
- {github_security_report-0.8.0 → github_security_report-0.10.0}/src/github_security_report/render/html.py +0 -0
- {github_security_report-0.8.0 → github_security_report-0.10.0}/src/github_security_report/render/markdown.py +0 -0
- {github_security_report-0.8.0 → github_security_report-0.10.0}/src/github_security_report/render/slack.py +0 -0
- {github_security_report-0.8.0 → github_security_report-0.10.0}/src/github_security_report/render/terminal.py +0 -0
- {github_security_report-0.8.0 → github_security_report-0.10.0}/src/github_security_report/report.py +0 -0
- {github_security_report-0.8.0 → github_security_report-0.10.0}/src/github_security_report/rulesets.py +0 -0
- {github_security_report-0.8.0 → github_security_report-0.10.0}/src/github_security_report/runner.py +0 -0
- {github_security_report-0.8.0 → github_security_report-0.10.0}/src/github_security_report/scope.py +0 -0
- {github_security_report-0.8.0 → github_security_report-0.10.0}/src/github_security_report/templates/index.html.j2 +0 -0
- {github_security_report-0.8.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.
|
|
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
|
|
@@ -29,14 +29,14 @@ Requires-Dist: jinja2==3.1.6
|
|
|
29
29
|
Requires-Dist: jsonschema==4.26.0
|
|
30
30
|
Requires-Dist: pyyaml==6.0.3
|
|
31
31
|
Requires-Dist: rich==15.0.0
|
|
32
|
-
Requires-Dist: typer==0.
|
|
32
|
+
Requires-Dist: typer==0.27.0
|
|
33
33
|
Provides-Extra: dev
|
|
34
34
|
Requires-Dist: mypy==2.3.0; extra == 'dev'
|
|
35
35
|
Requires-Dist: pytest-asyncio==1.4.0; extra == 'dev'
|
|
36
36
|
Requires-Dist: pytest-cov==7.1.0; extra == 'dev'
|
|
37
37
|
Requires-Dist: pytest==9.1.1; extra == 'dev'
|
|
38
38
|
Requires-Dist: respx==0.23.1; extra == 'dev'
|
|
39
|
-
Requires-Dist: ruff==0.15.
|
|
39
|
+
Requires-Dist: ruff==0.15.22; extra == 'dev'
|
|
40
40
|
Requires-Dist: syrupy==5.5.3; extra == 'dev'
|
|
41
41
|
Requires-Dist: types-jsonschema==4.26.0.20260518; extra == 'dev'
|
|
42
42
|
Requires-Dist: types-pyyaml==6.0.12.20250915; extra == 'dev'
|
|
@@ -45,7 +45,7 @@ keywords = [
|
|
|
45
45
|
]
|
|
46
46
|
dependencies = [
|
|
47
47
|
"httpx[http2]==0.28.1",
|
|
48
|
-
"typer==0.
|
|
48
|
+
"typer==0.27.0",
|
|
49
49
|
"rich==15.0.0",
|
|
50
50
|
"jinja2==3.1.6",
|
|
51
51
|
"jsonschema==4.26.0",
|
|
@@ -65,7 +65,7 @@ dev = [
|
|
|
65
65
|
"respx==0.23.1",
|
|
66
66
|
"syrupy==5.5.3",
|
|
67
67
|
"mypy==2.3.0",
|
|
68
|
-
"ruff==0.15.
|
|
68
|
+
"ruff==0.15.22",
|
|
69
69
|
"types-jsonschema==4.26.0.20260518",
|
|
70
70
|
"types-PyYAML==6.0.12.20250915",
|
|
71
71
|
]
|
|
@@ -97,7 +97,7 @@ dev = [
|
|
|
97
97
|
"respx==0.23.1",
|
|
98
98
|
"syrupy==5.5.3",
|
|
99
99
|
"mypy==2.3.0",
|
|
100
|
-
"ruff==0.15.
|
|
100
|
+
"ruff==0.15.22",
|
|
101
101
|
"types-jsonschema==4.26.0.20260518",
|
|
102
102
|
"types-PyYAML==6.0.12.20250915",
|
|
103
103
|
]
|
|
@@ -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.
|
|
22
|
-
__version_tuple__ = version_tuple = (0,
|
|
21
|
+
__version__ = version = '0.10.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 10, 0)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -67,9 +67,9 @@ class CategoryMeta:
|
|
|
67
67
|
# A repository fails (appears as an offender) only when it carries a finding
|
|
68
68
|
# at or above this rung; findings below it fold into the clean count. The
|
|
69
69
|
# global default is MEDIUM, so Low and Informational findings pass; a
|
|
70
|
-
# category may lower it (Zizmor uses
|
|
71
|
-
# Meaningful only for the severity-ranked signals; binary
|
|
72
|
-
# it. Overridable per category via the JSON config.
|
|
70
|
+
# category may lower it (Zizmor uses INFORMATIONAL, so every finding
|
|
71
|
+
# counts). Meaningful only for the severity-ranked signals; binary
|
|
72
|
+
# categories ignore it. Overridable per category via the JSON config.
|
|
73
73
|
fail_severity: Severity = Severity.MEDIUM
|
|
74
74
|
|
|
75
75
|
|
|
@@ -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)
|
|
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(
|
|
@@ -106,10 +107,18 @@ _CATEGORIES: dict[CategoryKey, CategoryMeta] = {
|
|
|
106
107
|
"Zizmor static analysis of GitHub Actions workflows, ranked "
|
|
107
108
|
"worst-first by severity."
|
|
108
109
|
),
|
|
109
|
-
#
|
|
110
|
-
#
|
|
111
|
-
#
|
|
112
|
-
|
|
110
|
+
# The organisation scan pipeline runs zizmor with an
|
|
111
|
+
# 'informational' floor, so every finding it can report reaches the
|
|
112
|
+
# SARIF. Match that here: any zizmor finding counts, at any
|
|
113
|
+
# severity. This mirrors the ruleset-enforced PR gate, which blocks
|
|
114
|
+
# on any finding regardless of level.
|
|
115
|
+
#
|
|
116
|
+
# zizmor emits both Low and Informational findings at SARIF level
|
|
117
|
+
# "note", and the code-scanning alerts API exposes only that level
|
|
118
|
+
# (not zizmor's own severity property), so the two are
|
|
119
|
+
# indistinguishable here. Cutting at INFORMATIONAL sidesteps the
|
|
120
|
+
# ambiguity: both surface either way.
|
|
121
|
+
fail_severity=Severity.INFORMATIONAL,
|
|
113
122
|
),
|
|
114
123
|
CategoryKey.AISLOP: CategoryMeta(
|
|
115
124
|
key=CategoryKey.AISLOP,
|
{github_security_report-0.8.0 → github_security_report-0.10.0}/src/github_security_report/models.py
RENAMED
|
@@ -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
|
|
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
|
-
"""
|
|
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
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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.
|
|
244
|
-
|
|
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
|
),
|
|
@@ -19,10 +19,13 @@ directly.
|
|
|
19
19
|
The ``note`` mapping mirrors zizmor's own SARIF encoder, which emits both its
|
|
20
20
|
Low and Informational findings at SARIF level ``note`` (Medium -> ``warning``,
|
|
21
21
|
High -> ``error``). The organisation scan pipeline runs zizmor with
|
|
22
|
-
``--min-severity
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
22
|
+
``--min-severity informational``, so both tiers reach the uploaded SARIF and a
|
|
23
|
+
``note`` alert may be either. The code-scanning alerts API exposes only the
|
|
24
|
+
SARIF level -- not the ``zizmor/severity`` property carried in the raw SARIF --
|
|
25
|
+
so the two cannot be separated here. ``note`` therefore stays at LOW rather
|
|
26
|
+
than INFORMATIONAL, which errs towards over- rather than under-stating the
|
|
27
|
+
estate's posture; the zizmor category cuts at INFORMATIONAL (see
|
|
28
|
+
``categories.py``) so both tiers are reported regardless of where this maps.
|
|
26
29
|
"""
|
|
27
30
|
|
|
28
31
|
from __future__ import annotations
|
|
@@ -49,6 +52,18 @@ class Severity(IntEnum):
|
|
|
49
52
|
return self.name.lower()
|
|
50
53
|
|
|
51
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
|
+
|
|
52
67
|
# Direct names on the security-severity scale (CodeQL, Scorecard, Dependabot).
|
|
53
68
|
_SECURITY_NAMES: dict[str, Severity] = {
|
|
54
69
|
"critical": Severity.CRITICAL,
|
|
@@ -59,11 +74,13 @@ _SECURITY_NAMES: dict[str, Severity] = {
|
|
|
59
74
|
}
|
|
60
75
|
|
|
61
76
|
# SARIF level -> security scale, used only as a fallback (zizmor, aislop).
|
|
62
|
-
# zizmor's SARIF encoder emits Low AND Informational findings as ``note``,
|
|
63
|
-
# the scan pipeline's --min-severity
|
|
64
|
-
#
|
|
65
|
-
#
|
|
66
|
-
#
|
|
77
|
+
# zizmor's SARIF encoder emits Low AND Informational findings as ``note``, and
|
|
78
|
+
# the scan pipeline's --min-severity informational floor lets both through, so
|
|
79
|
+
# a ``note`` alert may be either tier. The alerts API does not expose zizmor's
|
|
80
|
+
# own severity property, so they are indistinguishable here; ``note`` maps to
|
|
81
|
+
# LOW to avoid under-stating, and the zizmor category's INFORMATIONAL cutoff
|
|
82
|
+
# ensures both are reported. aislop uses the same three levels. The rare
|
|
83
|
+
# ``none`` level stays at INFORMATIONAL.
|
|
67
84
|
_SARIF_LEVEL_NAMES: dict[str, Severity] = {
|
|
68
85
|
"error": Severity.HIGH,
|
|
69
86
|
"warning": Severity.MEDIUM,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{github_security_report-0.8.0 → github_security_report-0.10.0}/src/github_security_report/cli.py
RENAMED
|
File without changes
|
{github_security_report-0.8.0 → github_security_report-0.10.0}/src/github_security_report/client.py
RENAMED
|
File without changes
|
{github_security_report-0.8.0 → github_security_report-0.10.0}/src/github_security_report/collect.py
RENAMED
|
File without changes
|
{github_security_report-0.8.0 → github_security_report-0.10.0}/src/github_security_report/config.py
RENAMED
|
File without changes
|
{github_security_report-0.8.0 → github_security_report-0.10.0}/src/github_security_report/gating.py
RENAMED
|
File without changes
|
{github_security_report-0.8.0 → github_security_report-0.10.0}/src/github_security_report/gitctx.py
RENAMED
|
File without changes
|
{github_security_report-0.8.0 → github_security_report-0.10.0}/src/github_security_report/posture.py
RENAMED
|
File without changes
|
{github_security_report-0.8.0 → github_security_report-0.10.0}/src/github_security_report/py.typed
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{github_security_report-0.8.0 → github_security_report-0.10.0}/src/github_security_report/report.py
RENAMED
|
File without changes
|
|
File without changes
|
{github_security_report-0.8.0 → github_security_report-0.10.0}/src/github_security_report/runner.py
RENAMED
|
File without changes
|
{github_security_report-0.8.0 → github_security_report-0.10.0}/src/github_security_report/scope.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|