diff --git a/CHANGELOG.md b/CHANGELOG.md index 074bb8b6..a5ba355a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,20 @@ # Changelog +## 2.6.13 + +### Fixed: mid-severity findings were dropped from the Slack summary + +- The Slack reachability formatter keyed every severity lookup on `medium`, + but the API sends `middle`. A mid-severity finding therefore missed all of + them at once: it was not counted, so the summary always read `Medium: 0`; it + was excluded from `total_findings`, which can drive the "and N more" count + negative; and it sorted at the default order of 4, below `low`, so it was the + first thing truncated when the Slack block limit was reached. +- Severity is now normalized to one spelling when an alert is read, matching + how the GitLab and PR-comment paths already handle both forms. The findings + themselves were always listed; only the counts, ordering and truncation were + wrong. + ## 2.6.12 ### Fixed: unreadable reachability facts no longer report a blocking package diff --git a/pyproject.toml b/pyproject.toml index b85e8b80..8f513c72 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" [project] name = "socketsecurity" -version = "2.6.12" +version = "2.6.13" requires-python = ">= 3.11" license = {"file" = "LICENSE"} dependencies = [ diff --git a/socketsecurity/__init__.py b/socketsecurity/__init__.py index c1ba543c..ce2d5859 100644 --- a/socketsecurity/__init__.py +++ b/socketsecurity/__init__.py @@ -1,3 +1,3 @@ __author__ = 'socket.dev' -__version__ = '2.6.12' +__version__ = '2.6.13' USER_AGENT = f'SocketPythonCLI/{__version__}' diff --git a/socketsecurity/plugins/formatters/slack.py b/socketsecurity/plugins/formatters/slack.py index adce7eec..904f5f3d 100644 --- a/socketsecurity/plugins/formatters/slack.py +++ b/socketsecurity/plugins/formatters/slack.py @@ -108,6 +108,12 @@ def _extract_alert_info(component: Dict[str, Any], alert: Dict[str, Any]) -> Dic """ props = alert.get('props', {}) or {} severity = str(alert.get('severity') or props.get('severity') or '').lower() + # The API's mid-level severity is "middle"; every lookup in this module is + # keyed on "medium". Normalizing here rather than adding a parallel key to + # each dict keeps one canonical spelling downstream, matching what + # Messages.map_socket_severity_to_gitlab already does. + if severity == 'middle': + severity = 'medium' return { 'cve_id': str(props.get('ghsaId') or props.get('cveId') or alert.get('title') or 'Unknown'), diff --git a/tests/unit/test_slack_severity_normalization.py b/tests/unit/test_slack_severity_normalization.py new file mode 100644 index 00000000..861fe34c --- /dev/null +++ b/tests/unit/test_slack_severity_normalization.py @@ -0,0 +1,76 @@ +"""The Slack formatter keys on "medium"; the API sends "middle". + +Every severity lookup in ``socketsecurity/plugins/formatters/slack.py`` is keyed +on ``medium``, but ``middle`` is what the API actually emits -- it is the value +in the OpenAPI spec's ``SocketIssueSeverity`` and in the SDK enum. Unnormalized, +a mid-severity finding fell through every one of them at once: + +* it was not counted, so the summary always read ``Medium: 0`` +* it was excluded from ``total_findings``, which can drive ``omitted_count`` + negative when mid-severity findings are the ones being displayed +* it sorted at the default order of 4, below ``low``, so it was truncated out of + the message first when the block limit was reached + +Two other call sites already handle both spellings (``Messages.map_socket_ +severity_to_gitlab`` and the GitLab severity map); this formatter did not. +""" + +import unittest + +from socketsecurity.plugins.formatters.slack import ( + SEVERITY_EMOJI, + SEVERITY_ORDER, + _extract_alert_info, + format_socket_facts_for_slack, +) + + +def _component(severity: str) -> dict: + return { + "name": "example-package", + "version": "1.0.0", + "alerts": [{"title": "Example alert", "severity": severity, "props": {}}], + } + + +class TestSeverityNormalization(unittest.TestCase): + def test_middle_normalizes_to_medium(self): + info = _extract_alert_info(_component("middle"), {"severity": "middle"}) + self.assertEqual(info["severity"], "medium") + + def test_middle_gets_the_medium_order_not_the_default(self): + info = _extract_alert_info(_component("middle"), {"severity": "middle"}) + self.assertEqual(info["severity_order"], SEVERITY_ORDER["medium"]) + # Regression: the default of 4 sorted mid-severity below "low". + self.assertLess(info["severity_order"], SEVERITY_ORDER["low"]) + + def test_middle_gets_the_medium_emoji_not_the_fallback(self): + info = _extract_alert_info(_component("middle"), {"severity": "middle"}) + self.assertEqual(info["severity_emoji"], SEVERITY_EMOJI["medium"]) + self.assertNotEqual(info["severity_emoji"], SEVERITY_EMOJI["low"]) + + def test_medium_still_works(self): + info = _extract_alert_info(_component("medium"), {"severity": "medium"}) + self.assertEqual(info["severity"], "medium") + self.assertEqual(info["severity_order"], SEVERITY_ORDER["medium"]) + + def test_middle_findings_are_counted_in_the_summary(self): + result = format_socket_facts_for_slack([_component("middle")]) + self.assertEqual(len(result), 1) + self.assertIn("🟡 Medium: 1", result[0]["summary"]) + + def test_middle_findings_reach_total_findings(self): + # Regression: excluded from the total, omitted_count could go negative. + result = format_socket_facts_for_slack([_component("middle")]) + self.assertEqual(result[0]["total_findings"], 1) + + def test_unrecognized_severity_still_falls_back(self): + info = _extract_alert_info( + _component("brand-new-level"), {"severity": "brand-new-level"} + ) + self.assertEqual(info["severity_order"], 4) + self.assertEqual(info["severity_emoji"], "⚪") + + +if __name__ == "__main__": + unittest.main() diff --git a/uv.lock b/uv.lock index 8e880d32..fd652795 100644 --- a/uv.lock +++ b/uv.lock @@ -1282,7 +1282,7 @@ wheels = [ [[package]] name = "socketsecurity" -version = "2.6.12" +version = "2.6.13" source = { editable = "." } dependencies = [ { name = "beautifulsoup4" },