Skip to content

Commit 860b8ea

Browse files
leliaclaude
andauthored
Normalize Slack issue severity and bump SDK to v3.6.0 (#331)
* fix(slack): normalize the API's "middle" severity to "medium" Every severity lookup in the Slack reachability formatter is keyed on "medium", but "middle" is what the API sends. A mid-severity finding missed all of them at once: uncounted in the summary, excluded from total_findings so the "and N more" count can go negative, and sorted at the default order of 4 -- below "low" -- so it was truncated out of the message first. Normalized at the point the alert is read rather than by adding a parallel key to each dict, so one canonical spelling flows downstream. The GitLab severity map and the PR comment path already accept both forms; this formatter did not. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * chore(deps): bump socketdev to 3.6.0 Unblocks the pin now that 3.6.0 is on PyPI. SocketPURL_Type gained ten members -- alpm, chrome, clawhub, edge-extension, firefox-extension, qpkg, socket, swid, vscode and vscode-extension -- and removed none, so artifacts of those types stop falling back to "unknown". No other CLI change is needed: none of the SDK's enum types are imported here, and every severity and type lookup already has a default, so the new members cannot reach an unguarded branch. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * ci: only floor the version check at the latest published release The check required the PR version to exceed both main and PyPI. Comparing against main forbids the legitimate case where several PRs ship under one unreleased version: the first bumps main, and the rest ride it without bumping again so they stay under a single changelog header. Every such PR failed, and the only way to green it was a throwaway bump that would strand a changelog header on a version that never ships. PyPI is now the floor, since the real invariant is that a release cannot reuse a published version. Main is still a floor in the one direction that matters: a PR may leave the version alone or move it forwards, never back. Every genuine failure the old check caught -- forgetting to bump, reusing a published version, branching from a stale base -- still fails. Also added this workflow to its own paths filter so a change to the check is exercised by the PR that makes it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * ci: require pyproject.toml and __init__.py versions to agree The version lives as two hand-maintained literals with nothing deriving one from the other: pyproject.toml is what gets published, and __init__.py is what the CLI reports as its User-Agent. Every comparison in this job read only __init__.py, so bumping that alone passed the check and then published under the old number -- surfacing late, as twine rejecting an existing file, after the merge. Both are now required to match before any other comparison runs. uv.lock carries a third copy, but uv derives it and `uv lock --locked` in python-tests already fails when it drifts, so it needs no check here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent f65b6af commit 860b8ea

6 files changed

Lines changed: 158 additions & 14 deletions

File tree

.github/workflows/version-check.yml

Lines changed: 50 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ on:
66
- 'socketsecurity/**'
77
- 'pyproject.toml'
88
- 'uv.lock'
9+
# Included so a change to the check itself is exercised by its own PR.
10+
- '.github/workflows/version-check.yml'
911

1012
permissions:
1113
contents: read
@@ -42,16 +44,37 @@ jobs:
4244
export PR_VERSION
4345
export MAIN_VERSION
4446
45-
# Compare against both main and latest published PyPI release.
47+
# Compare against the latest published PyPI release.
4648
python3 <<'PY'
4749
import json
4850
import os
51+
import tomllib
4952
import urllib.request
5053
from packaging import version
5154
5255
pr_ver = version.parse(os.environ["PR_VERSION"])
5356
main_ver = version.parse(os.environ["MAIN_VERSION"])
5457
58+
with open("pyproject.toml", "rb") as fh:
59+
pyproject_ver = version.parse(tomllib.load(fh)["project"]["version"])
60+
61+
# The version is two hand-maintained literals with nothing deriving one
62+
# from the other: pyproject.toml is what actually gets published, and
63+
# socketsecurity/__init__.py is what the CLI reports as its User-Agent.
64+
# Every comparison below reads only __init__.py, so bumping that alone
65+
# would pass this job and then publish under the old number -- caught
66+
# late, by twine rejecting an existing file, after the merge. Require
67+
# the two to agree before comparing anything. (uv.lock carries a third
68+
# copy, but uv derives it and `uv lock --locked` in python-tests
69+
# already fails when it drifts.)
70+
if pr_ver != pyproject_ver:
71+
print(
72+
f"❌ Version mismatch inside the PR: pyproject.toml is "
73+
f"{pyproject_ver}, socketsecurity/__init__.py is {pr_ver}. "
74+
f"Bump both."
75+
)
76+
raise SystemExit(1)
77+
5578
with urllib.request.urlopen("https://pypi.org/pypi/socketsecurity/json") as response:
5679
pypi_data = json.load(response)
5780
@@ -62,19 +85,37 @@ jobs:
6285
published_versions.append(parsed)
6386
6487
pypi_ver = max(published_versions) if published_versions else version.parse("0.0.0")
65-
required_floor = max(main_ver, pypi_ver)
6688
67-
if pr_ver <= required_floor:
89+
# The only hard requirement is that the version is ahead of what is
90+
# actually released. Treating main's version as a second floor breaks
91+
# the legitimate case where several PRs share one unreleased release:
92+
# the first bumps main to the new version and the rest ride it without
93+
# bumping again, which is what keeps them under a single changelog
94+
# header. Main is therefore only a floor when this PR moves the
95+
# version -- a change to it must go forwards, never backwards.
96+
if pr_ver <= pypi_ver:
6897
print(
69-
f"❌ Version must be greater than main and PyPI! "
70-
f"Main: {main_ver}, PyPI: {pypi_ver}, PR: {pr_ver}"
98+
f"❌ Version {pr_ver} is already published on PyPI "
99+
f"(latest release: {pypi_ver}). Bump it."
100+
)
101+
raise SystemExit(1)
102+
103+
if pr_ver < main_ver:
104+
print(
105+
f"❌ Version moves backwards: main is {main_ver}, PR is {pr_ver}."
71106
)
72107
raise SystemExit(1)
73108
74-
print(
75-
f"✅ Version properly incremented. "
76-
f"Main: {main_ver}, PyPI: {pypi_ver}, PR: {pr_ver}"
77-
)
109+
if pr_ver == main_ver:
110+
print(
111+
f"✅ Riding main's unreleased {pr_ver} "
112+
f"(latest PyPI release: {pypi_ver})."
113+
)
114+
else:
115+
print(
116+
f"✅ Version properly incremented. "
117+
f"Main: {main_ver}, PyPI: {pypi_ver}, PR: {pr_ver}"
118+
)
78119
PY
79120
80121
- name: Require uv.lock update when pyproject changes

CHANGELOG.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,32 @@
99
- Clarified monorepo scan scoping, workspace flags, CI path filters, and timeout
1010
behavior, with a changed-workspace GitHub Actions example.
1111

12+
### Changed: bump socketdev to 3.6.0
13+
14+
- Bumped the pinned SDK (`socketdev`) from `3.5.0` to `3.6.0`. Its package-type
15+
enum gained ten members — `alpm`, `chrome`, `clawhub`, `edge-extension`,
16+
`firefox-extension`, `qpkg`, `socket`, `swid`, `vscode` and
17+
`vscode-extension` — so artifacts of those types are now reported under their
18+
own type instead of falling back to `unknown`.
19+
1220
### Fixed: apply configured exit codes to API failures
1321

1422
- Full-scan and streamed-diff API failures now use the configured infrastructure
1523
error exit code instead of the security-finding exit code.
1624

25+
### Fixed: mid-severity findings were dropped from the Slack summary
26+
27+
- The Slack reachability formatter keyed every severity lookup on `medium`,
28+
but the API sends `middle`. A mid-severity finding therefore missed all of
29+
them at once: it was not counted, so the summary always read `Medium: 0`; it
30+
was excluded from `total_findings`, which can drive the "and N more" count
31+
negative; and it sorted at the default order of 4, below `low`, so it was the
32+
first thing truncated when the Slack block limit was reached.
33+
- Severity is now normalized to one spelling when an alert is read, matching
34+
how the GitLab and PR-comment paths already handle both forms. The findings
35+
themselves were always listed; only the counts, ordering and truncation were
36+
wrong.
37+
1738
## 2.7.2
1839

1940
### Changed: bump pinned @coana-tech/cli to 15.10.39

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ dependencies = [
1616
"GitPython==3.1.59",
1717
"packaging==26.3",
1818
"python-dotenv==1.2.3",
19-
"socketdev==3.5.0",
19+
"socketdev==3.6.0",
2020
"beautifulsoup4==4.15.0",
2121
"markdown==3.10.3",
2222
"brotli==1.2.0; platform_python_implementation == 'CPython'",

socketsecurity/plugins/formatters/slack.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,12 @@ def _extract_alert_info(component: Dict[str, Any], alert: Dict[str, Any]) -> Dic
108108
"""
109109
props = alert.get('props', {}) or {}
110110
severity = str(alert.get('severity') or props.get('severity') or '').lower()
111+
# The API's mid-level severity is "middle"; every lookup in this module is
112+
# keyed on "medium". Normalizing here rather than adding a parallel key to
113+
# each dict keeps one canonical spelling downstream, matching what
114+
# Messages.map_socket_severity_to_gitlab already does.
115+
if severity == 'middle':
116+
severity = 'medium'
111117

112118
return {
113119
'cve_id': str(props.get('ghsaId') or props.get('cveId') or alert.get('title') or 'Unknown'),
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
"""The Slack formatter keys on "medium"; the API sends "middle".
2+
3+
Every severity lookup in ``socketsecurity/plugins/formatters/slack.py`` is keyed
4+
on ``medium``, but ``middle`` is what the API actually emits -- it is the value
5+
in the OpenAPI spec's ``SocketIssueSeverity`` and in the SDK enum. Unnormalized,
6+
a mid-severity finding fell through every one of them at once:
7+
8+
* it was not counted, so the summary always read ``Medium: 0``
9+
* it was excluded from ``total_findings``, which can drive ``omitted_count``
10+
negative when mid-severity findings are the ones being displayed
11+
* it sorted at the default order of 4, below ``low``, so it was truncated out of
12+
the message first when the block limit was reached
13+
14+
Two other call sites already handle both spellings (``Messages.map_socket_
15+
severity_to_gitlab`` and the GitLab severity map); this formatter did not.
16+
"""
17+
18+
import unittest
19+
20+
from socketsecurity.plugins.formatters.slack import (
21+
SEVERITY_EMOJI,
22+
SEVERITY_ORDER,
23+
_extract_alert_info,
24+
format_socket_facts_for_slack,
25+
)
26+
27+
28+
def _component(severity: str) -> dict:
29+
return {
30+
"name": "example-package",
31+
"version": "1.0.0",
32+
"alerts": [{"title": "Example alert", "severity": severity, "props": {}}],
33+
}
34+
35+
36+
class TestSeverityNormalization(unittest.TestCase):
37+
def test_middle_normalizes_to_medium(self):
38+
info = _extract_alert_info(_component("middle"), {"severity": "middle"})
39+
self.assertEqual(info["severity"], "medium")
40+
41+
def test_middle_gets_the_medium_order_not_the_default(self):
42+
info = _extract_alert_info(_component("middle"), {"severity": "middle"})
43+
self.assertEqual(info["severity_order"], SEVERITY_ORDER["medium"])
44+
# Regression: the default of 4 sorted mid-severity below "low".
45+
self.assertLess(info["severity_order"], SEVERITY_ORDER["low"])
46+
47+
def test_middle_gets_the_medium_emoji_not_the_fallback(self):
48+
info = _extract_alert_info(_component("middle"), {"severity": "middle"})
49+
self.assertEqual(info["severity_emoji"], SEVERITY_EMOJI["medium"])
50+
self.assertNotEqual(info["severity_emoji"], SEVERITY_EMOJI["low"])
51+
52+
def test_medium_still_works(self):
53+
info = _extract_alert_info(_component("medium"), {"severity": "medium"})
54+
self.assertEqual(info["severity"], "medium")
55+
self.assertEqual(info["severity_order"], SEVERITY_ORDER["medium"])
56+
57+
def test_middle_findings_are_counted_in_the_summary(self):
58+
result = format_socket_facts_for_slack([_component("middle")])
59+
self.assertEqual(len(result), 1)
60+
self.assertIn("🟡 Medium: 1", result[0]["summary"])
61+
62+
def test_middle_findings_reach_total_findings(self):
63+
# Regression: excluded from the total, omitted_count could go negative.
64+
result = format_socket_facts_for_slack([_component("middle")])
65+
self.assertEqual(result[0]["total_findings"], 1)
66+
67+
def test_unrecognized_severity_still_falls_back(self):
68+
info = _extract_alert_info(
69+
_component("brand-new-level"), {"severity": "brand-new-level"}
70+
)
71+
self.assertEqual(info["severity_order"], 4)
72+
self.assertEqual(info["severity_emoji"], "⚪")
73+
74+
75+
if __name__ == "__main__":
76+
unittest.main()

uv.lock

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)