github-security-report 0.3.0__tar.gz → 0.4.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 (31) hide show
  1. {github_security_report-0.3.0 → github_security_report-0.4.0}/PKG-INFO +2 -2
  2. {github_security_report-0.3.0 → github_security_report-0.4.0}/README.md +1 -1
  3. {github_security_report-0.3.0 → github_security_report-0.4.0}/src/github_security_report/_version.py +2 -2
  4. {github_security_report-0.3.0 → github_security_report-0.4.0}/src/github_security_report/cli.py +1 -0
  5. {github_security_report-0.3.0 → github_security_report-0.4.0}/src/github_security_report/render/html.py +13 -5
  6. {github_security_report-0.3.0 → github_security_report-0.4.0}/src/github_security_report/render/markdown.py +31 -18
  7. {github_security_report-0.3.0 → github_security_report-0.4.0}/src/github_security_report/render/slack.py +19 -12
  8. {github_security_report-0.3.0 → github_security_report-0.4.0}/src/github_security_report/render/terminal.py +20 -7
  9. {github_security_report-0.3.0 → github_security_report-0.4.0}/src/github_security_report/report.py +12 -0
  10. {github_security_report-0.3.0 → github_security_report-0.4.0}/.gitignore +0 -0
  11. {github_security_report-0.3.0 → github_security_report-0.4.0}/LICENSE +0 -0
  12. {github_security_report-0.3.0 → github_security_report-0.4.0}/LICENSES/Apache-2.0.txt +0 -0
  13. {github_security_report-0.3.0 → github_security_report-0.4.0}/pyproject.toml +0 -0
  14. {github_security_report-0.3.0 → github_security_report-0.4.0}/scripts/README.md +0 -0
  15. {github_security_report-0.3.0 → github_security_report-0.4.0}/src/github_security_report/__init__.py +0 -0
  16. {github_security_report-0.3.0 → github_security_report-0.4.0}/src/github_security_report/categories.py +0 -0
  17. {github_security_report-0.3.0 → github_security_report-0.4.0}/src/github_security_report/classify.py +0 -0
  18. {github_security_report-0.3.0 → github_security_report-0.4.0}/src/github_security_report/client.py +0 -0
  19. {github_security_report-0.3.0 → github_security_report-0.4.0}/src/github_security_report/collect.py +0 -0
  20. {github_security_report-0.3.0 → github_security_report-0.4.0}/src/github_security_report/config.py +0 -0
  21. {github_security_report-0.3.0 → github_security_report-0.4.0}/src/github_security_report/gitctx.py +0 -0
  22. {github_security_report-0.3.0 → github_security_report-0.4.0}/src/github_security_report/models.py +0 -0
  23. {github_security_report-0.3.0 → github_security_report-0.4.0}/src/github_security_report/posture.py +0 -0
  24. {github_security_report-0.3.0 → github_security_report-0.4.0}/src/github_security_report/py.typed +0 -0
  25. {github_security_report-0.3.0 → github_security_report-0.4.0}/src/github_security_report/render/__init__.py +0 -0
  26. {github_security_report-0.3.0 → github_security_report-0.4.0}/src/github_security_report/rulesets.py +0 -0
  27. {github_security_report-0.3.0 → github_security_report-0.4.0}/src/github_security_report/runner.py +0 -0
  28. {github_security_report-0.3.0 → github_security_report-0.4.0}/src/github_security_report/scope.py +0 -0
  29. {github_security_report-0.3.0 → github_security_report-0.4.0}/src/github_security_report/severity.py +0 -0
  30. {github_security_report-0.3.0 → github_security_report-0.4.0}/src/github_security_report/templates/index.html.j2 +0 -0
  31. {github_security_report-0.3.0 → github_security_report-0.4.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.0
3
+ Version: 0.4.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
@@ -390,7 +390,7 @@ and the Slack **bot token** is consumed by the workflow, not the CLI.
390
390
  | `top_n_slack` | No | — | Offenders per signal in the Slack digest (`0` = no limit; overrides `top_n`) |
391
391
  | `fail_threshold` | No | `none` | `none`/`low`/`medium`/`high`/`critical`/`any` (repo mode) |
392
392
  | `force_notify` | No | `false` | Post to Slack regardless of `report_day` |
393
- | `tool_version` | No | `0.1.0` | Published PyPI version (ignored on pull requests) |
393
+ | `tool_version` | No | `""` | Published PyPI version to install. Empty (the default) uses the Dependabot-managed pin in `.github/runtime-pin/requirements.txt`; set a specific version to override. Ignored on pull requests or when `use_local_source` is `true` (both run from source) |
394
394
  | `use_local_source` | No | `false` | Run from the checked-out source instead of PyPI (for testing unreleased code from any event) |
395
395
 
396
396
  <!-- markdownlint-enable MD013 -->
@@ -346,7 +346,7 @@ and the Slack **bot token** is consumed by the workflow, not the CLI.
346
346
  | `top_n_slack` | No | — | Offenders per signal in the Slack digest (`0` = no limit; overrides `top_n`) |
347
347
  | `fail_threshold` | No | `none` | `none`/`low`/`medium`/`high`/`critical`/`any` (repo mode) |
348
348
  | `force_notify` | No | `false` | Post to Slack regardless of `report_day` |
349
- | `tool_version` | No | `0.1.0` | Published PyPI version (ignored on pull requests) |
349
+ | `tool_version` | No | `""` | Published PyPI version to install. Empty (the default) uses the Dependabot-managed pin in `.github/runtime-pin/requirements.txt`; set a specific version to override. Ignored on pull requests or when `use_local_source` is `true` (both run from source) |
350
350
  | `use_local_source` | No | `false` | Run from the checked-out source instead of PyPI (for testing unreleased code from any event) |
351
351
 
352
352
  <!-- markdownlint-enable MD013 -->
@@ -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.3.0'
22
- __version_tuple__ = version_tuple = (0, 3, 0)
21
+ __version__ = version = '0.4.0'
22
+ __version_tuple__ = version_tuple = (0, 4, 0)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -114,6 +114,7 @@ def _org_to_dict(org: OrgReport) -> dict:
114
114
  "high": rs.counts.high,
115
115
  "medium": rs.counts.medium,
116
116
  "low": rs.counts.low,
117
+ "informational": rs.counts.informational,
117
118
  "total": rs.counts.total,
118
119
  },
119
120
  "score": rs.score,
@@ -26,6 +26,7 @@ from github_security_report.report import (
26
26
  SummaryLine,
27
27
  TableSection,
28
28
  build_summary,
29
+ section_shows_informational,
29
30
  truncate,
30
31
  )
31
32
 
@@ -64,9 +65,9 @@ def slugify(org: str) -> str:
64
65
  return slug or "org"
65
66
 
66
67
 
67
- def _row_cells(sig: RepoSignal) -> list[str]:
68
+ def _row_cells(sig: RepoSignal, *, informational: bool = False) -> list[str]:
68
69
  # Reuse the Markdown row shape (public API), dropping the leading repo cell.
69
- return markdown.row_cells(sig)[1:]
70
+ return markdown.row_cells(sig, informational=informational)[1:]
70
71
 
71
72
 
72
73
  def _summary_context(
@@ -146,10 +147,13 @@ def _section_context(
146
147
  top_n: int | None = None,
147
148
  ) -> dict:
148
149
  offenders, hidden = truncate(section.offenders, top_n)
150
+ informational = section_shows_informational(offenders)
149
151
  # A trailing totals row sums the additive severity columns across the shown
150
152
  # rows; secret scanning has no such columns, so it gets none.
151
153
  total_cells = (
152
- markdown.total_row_cells(section.signal, offenders)
154
+ markdown.total_row_cells(
155
+ section.signal, offenders, informational=informational
156
+ )
153
157
  if section.signal.uses_severity_columns and offenders
154
158
  else None
155
159
  )
@@ -162,9 +166,13 @@ def _section_context(
162
166
  # Severity sections have numeric count columns after the leading
163
167
  # repository column, so they are right-aligned with tabular figures.
164
168
  "numeric": True,
165
- "columns": markdown.columns(section.signal),
169
+ "columns": markdown.columns(section.signal, informational=informational),
166
170
  "rows": [
167
- {"name": s.repo.name, "url": s.repo.html_url, "cells": _row_cells(s)}
171
+ {
172
+ "name": s.repo.name,
173
+ "url": s.repo.html_url,
174
+ "cells": _row_cells(s, informational=informational),
175
+ }
168
176
  for s in offenders
169
177
  ],
170
178
  "hidden": hidden,
@@ -25,6 +25,7 @@ from github_security_report.report import (
25
25
  TableSection,
26
26
  build_summary,
27
27
  offender_column_totals,
28
+ section_shows_informational,
28
29
  truncate,
29
30
  )
30
31
 
@@ -36,35 +37,38 @@ def _link(repo: Repo) -> str:
36
37
  return f"[{repo.name}]({repo.html_url})"
37
38
 
38
39
 
39
- def _columns(signal: SignalType) -> list[str]:
40
+ def _columns(signal: SignalType, *, informational: bool = False) -> list[str]:
40
41
  if signal is SignalType.SECRET_SCANNING:
41
42
  return ["Repository", "Open"]
43
+ info = ["Info"] if informational else []
42
44
  if signal is SignalType.SCORECARD:
43
- return ["Repository", "Score", "Critical", "High", "Medium", "Low"]
44
- return ["Repository", "Critical", "High", "Medium", "Low", "Total"]
45
+ return ["Repository", "Score", "Critical", "High", "Medium", "Low", *info]
46
+ return ["Repository", "Critical", "High", "Medium", "Low", *info, "Total"]
45
47
 
46
48
 
47
- def _row(sig: RepoSignal) -> list[str]:
49
+ def _row(sig: RepoSignal, *, informational: bool = False) -> list[str]:
48
50
  c = sig.counts
49
51
  if sig.signal is SignalType.SECRET_SCANNING:
50
52
  return [_link(sig.repo), str(c.total)]
53
+ info = [str(c.informational)] if informational else []
51
54
  if sig.signal is SignalType.SCORECARD:
52
55
  score = f"{sig.score:.1f}" if sig.score is not None else "—"
53
- return [_link(sig.repo), score, str(c.critical), str(c.high), str(c.medium), str(c.low)]
54
- return [_link(sig.repo), str(c.critical), str(c.high), str(c.medium), str(c.low), str(c.total)]
56
+ return [_link(sig.repo), score, str(c.critical), str(c.high), str(c.medium), str(c.low), *info]
57
+ return [_link(sig.repo), str(c.critical), str(c.high), str(c.medium), str(c.low), *info, str(c.total)]
55
58
 
56
59
 
57
60
  def _table(section: SignalSection, top_n: int | None = None) -> list[str]:
58
- cols = _columns(section.signal)
61
+ offenders, hidden = truncate(section.offenders, top_n)
62
+ informational = section_shows_informational(offenders)
63
+ cols = _columns(section.signal, informational=informational)
59
64
  aligns = ["---"] + ["---:"] * (len(cols) - 1)
60
65
  lines = ["| " + " | ".join(cols) + " |", "| " + " | ".join(aligns) + " |"]
61
- offenders, hidden = truncate(section.offenders, top_n)
62
66
  for sig in offenders:
63
- lines.append("| " + " | ".join(_row(sig)) + " |")
67
+ lines.append("| " + " | ".join(_row(sig, informational=informational)) + " |")
64
68
  # Trailing column-totals row, for signals whose columns are additive
65
69
  # severity counts (every offender table except secret scanning).
66
70
  if section.signal.uses_severity_columns:
67
- cells = total_row_cells(section.signal, offenders)
71
+ cells = total_row_cells(section.signal, offenders, informational=informational)
68
72
  lines.append("| " + " | ".join(cells) + " |")
69
73
  if hidden:
70
74
  lines.append("")
@@ -74,18 +78,26 @@ def _table(section: SignalSection, top_n: int | None = None) -> list[str]:
74
78
 
75
79
  # Public, render-surface-agnostic accessors for the per-signal table shape, so
76
80
  # other renderers (e.g. HTML) do not reach into this module's private helpers.
77
- def columns(signal: SignalType) -> list[str]:
78
- """Column headings for a signal's offender table (repository first)."""
79
- return _columns(signal)
81
+ def columns(signal: SignalType, *, informational: bool = False) -> list[str]:
82
+ """Column headings for a signal's offender table (repository first).
80
83
 
84
+ ``informational`` adds the sub-low Informational column, used only for
85
+ tables that actually carry such findings (see
86
+ :func:`report.section_shows_informational`).
87
+ """
88
+ return _columns(signal, informational=informational)
81
89
 
82
- def row_cells(sig: RepoSignal) -> list[str]:
90
+
91
+ def row_cells(sig: RepoSignal, *, informational: bool = False) -> list[str]:
83
92
  """Cells for one offender row (the repository link is the first cell)."""
84
- return _row(sig)
93
+ return _row(sig, informational=informational)
85
94
 
86
95
 
87
96
  def total_row_cells(
88
- signal: SignalType, offenders: Sequence[RepoSignal]
97
+ signal: SignalType,
98
+ offenders: Sequence[RepoSignal],
99
+ *,
100
+ informational: bool = False,
89
101
  ) -> list[str]:
90
102
  """Cells for a trailing "Total" row summing the severity columns.
91
103
 
@@ -101,9 +113,10 @@ def total_row_cells(
101
113
  str(totals.medium),
102
114
  str(totals.low),
103
115
  ]
116
+ info = [str(totals.informational)] if informational else []
104
117
  if signal is SignalType.SCORECARD:
105
- return ["Total", "", *base]
106
- return ["Total", *base, str(totals.total)]
118
+ return ["Total", "", *base, *info]
119
+ return ["Total", *base, *info, str(totals.total)]
107
120
 
108
121
 
109
122
  def _summary_lines(
@@ -24,6 +24,7 @@ from github_security_report.report import (
24
24
  TableSection,
25
25
  build_summary,
26
26
  offender_column_totals,
27
+ section_shows_informational,
27
28
  truncate,
28
29
  )
29
30
 
@@ -35,26 +36,28 @@ _SLACK_MAX_BLOCKS = 50
35
36
  _NAME_LIST_LABEL = {"disabled": "Disabled", "excluded": "Excluded"}
36
37
 
37
38
 
38
- def _plain_columns(signal: SignalType) -> list[str]:
39
+ def _plain_columns(signal: SignalType, *, informational: bool = False) -> list[str]:
39
40
  if signal is SignalType.SECRET_SCANNING:
40
41
  return ["Repository", "Open"]
42
+ info = ["I"] if informational else []
41
43
  if signal is SignalType.SCORECARD:
42
- return ["Repository", "Score", "C", "H", "M", "L"]
43
- return ["Repository", "C", "H", "M", "L"]
44
+ return ["Repository", "Score", "C", "H", "M", "L", *info]
45
+ return ["Repository", "C", "H", "M", "L", *info]
44
46
 
45
47
 
46
- def _plain_row(sig: RepoSignal) -> list[str]:
48
+ def _plain_row(sig: RepoSignal, *, informational: bool = False) -> list[str]:
47
49
  c = sig.counts
48
50
  if sig.signal is SignalType.SECRET_SCANNING:
49
51
  return [sig.repo.name, str(c.total)]
52
+ info = [str(c.informational)] if informational else []
50
53
  if sig.signal is SignalType.SCORECARD:
51
54
  score = f"{sig.score:.1f}" if sig.score is not None else "-"
52
- return [sig.repo.name, score, str(c.critical), str(c.high), str(c.medium), str(c.low)]
53
- return [sig.repo.name, str(c.critical), str(c.high), str(c.medium), str(c.low)]
55
+ return [sig.repo.name, score, str(c.critical), str(c.high), str(c.medium), str(c.low), *info]
56
+ return [sig.repo.name, str(c.critical), str(c.high), str(c.medium), str(c.low), *info]
54
57
 
55
58
 
56
59
  def _plain_total_row(
57
- signal: SignalType, offenders: list[RepoSignal]
60
+ signal: SignalType, offenders: list[RepoSignal], *, informational: bool = False
58
61
  ) -> list[str]:
59
62
  """Trailing "Total" row summing the severity columns for Slack tables.
60
63
 
@@ -69,19 +72,23 @@ def _plain_total_row(
69
72
  str(totals.medium),
70
73
  str(totals.low),
71
74
  ]
75
+ info = [str(totals.informational)] if informational else []
72
76
  if signal is SignalType.SCORECARD:
73
- return ["Total", "", *base]
74
- return ["Total", *base]
77
+ return ["Total", "", *base, *info]
78
+ return ["Total", *base, *info]
75
79
 
76
80
 
77
81
  def _fixed_table(section: SignalSection, top_n: int) -> str:
78
- cols = _plain_columns(section.signal)
79
82
  shown, hidden = truncate(section.offenders, top_n)
80
- rows = [_plain_row(s) for s in shown]
83
+ informational = section_shows_informational(shown)
84
+ cols = _plain_columns(section.signal, informational=informational)
85
+ rows = [_plain_row(s, informational=informational) for s in shown]
81
86
  # A trailing totals row sums the additive severity columns; secret scanning
82
87
  # has no such columns, so skip it. Summed over the shown (truncated) rows.
83
88
  if section.signal.uses_severity_columns and shown:
84
- rows.append(_plain_total_row(section.signal, shown))
89
+ rows.append(
90
+ _plain_total_row(section.signal, shown, informational=informational)
91
+ )
85
92
  widths = [len(c) for c in cols]
86
93
  for row in rows:
87
94
  for i, cell in enumerate(row):
@@ -25,11 +25,16 @@ from github_security_report.report import (
25
25
  SummaryLine,
26
26
  TableSection,
27
27
  build_summary,
28
+ section_shows_informational,
28
29
  truncate,
29
30
  )
30
31
 
31
32
  _SEVERITY_STYLE = {"critical": "bold red", "high": "red", "medium": "yellow", "low": "dim"}
32
33
 
34
+ # The sub-low Informational column (shown only when a table carries note-level
35
+ # findings) is the least urgent, so it is dimmed like the Low column.
36
+ _INFORMATIONAL_STYLE = "dim"
37
+
33
38
  # Rich style per summary-footer kind, shared by signal and table sections.
34
39
  _SUMMARY_STYLE = {
35
40
  "fail": "red",
@@ -43,7 +48,9 @@ _SUMMARY_STYLE = {
43
48
  _NAME_LIST_LABEL = {"disabled": "Disabled", "excluded": "Excluded"}
44
49
 
45
50
 
46
- def _add_columns(table: Table, signal: SignalType) -> None:
51
+ def _add_columns(
52
+ table: Table, signal: SignalType, *, informational: bool = False
53
+ ) -> None:
47
54
  table.add_column("Repository", overflow="fold")
48
55
  if signal is SignalType.SECRET_SCANNING:
49
56
  table.add_column("Open", justify="right")
@@ -52,19 +59,22 @@ def _add_columns(table: Table, signal: SignalType) -> None:
52
59
  table.add_column("Score", justify="right")
53
60
  for name, style in _SEVERITY_STYLE.items():
54
61
  table.add_column(name.capitalize(), justify="right", style=style)
62
+ if informational:
63
+ table.add_column("Info", justify="right", style=_INFORMATIONAL_STYLE)
55
64
  if signal is not SignalType.SCORECARD:
56
65
  table.add_column("Total", justify="right")
57
66
 
58
67
 
59
- def _row(sig: RepoSignal) -> list[str]:
68
+ def _row(sig: RepoSignal, *, informational: bool = False) -> list[str]:
60
69
  c = sig.counts
61
70
  if sig.signal is SignalType.SECRET_SCANNING:
62
71
  return [sig.repo.name, str(c.total)]
63
72
  base = [str(c.critical), str(c.high), str(c.medium), str(c.low)]
73
+ info = [str(c.informational)] if informational else []
64
74
  if sig.signal is SignalType.SCORECARD:
65
75
  score = f"{sig.score:.1f}" if sig.score is not None else "—"
66
- return [sig.repo.name, score, *base]
67
- return [sig.repo.name, *base, str(c.total)]
76
+ return [sig.repo.name, score, *base, *info]
77
+ return [sig.repo.name, *base, *info, str(c.total)]
68
78
 
69
79
 
70
80
  def _truncated_names(names: Sequence[str], top_n: int | None) -> str:
@@ -108,16 +118,19 @@ def render_section(
108
118
  ) -> None:
109
119
  offenders, hidden_offenders = truncate(section.offenders, top_n)
110
120
  if offenders:
121
+ informational = section_shows_informational(offenders)
111
122
  table = Table(title=section.signal.heading, title_justify="left", title_style="bold")
112
- _add_columns(table, section.signal)
123
+ _add_columns(table, section.signal, informational=informational)
113
124
  for sig in offenders:
114
- table.add_row(*_row(sig))
125
+ table.add_row(*_row(sig, informational=informational))
115
126
  # A trailing totals row sums the additive severity columns across the
116
127
  # rows shown above. Secret scanning has no such columns, so skip it.
117
128
  if section.signal.uses_severity_columns:
118
129
  table.add_section()
119
130
  table.add_row(
120
- *markdown.total_row_cells(section.signal, offenders),
131
+ *markdown.total_row_cells(
132
+ section.signal, offenders, informational=informational
133
+ ),
121
134
  style="bold",
122
135
  )
123
136
  console.print(table)
@@ -301,6 +301,18 @@ def offender_column_totals(offenders: Sequence[RepoSignal]) -> SeverityCounts:
301
301
  return totals
302
302
 
303
303
 
304
+ def section_shows_informational(offenders: Sequence[RepoSignal]) -> bool:
305
+ """Whether any offender carries informational (sub-low) findings.
306
+
307
+ Drives the conditional Informational severity column: it is rendered only
308
+ for tables that actually have sub-low findings -- e.g. zizmor's note-level
309
+ results -- so severity tables without such data (CodeQL, Dependabot alerts)
310
+ are not padded with an all-zero column. Callers pass the displayed
311
+ (already-truncated) offenders, so the column matches the visible rows.
312
+ """
313
+ return any(sig.counts.informational for sig in offenders)
314
+
315
+
304
316
  def build_org_report(
305
317
  org: str,
306
318
  repo_signals: list[RepoSignal],