From c4c699d9c0d67ade2d2dde8c77eeae5672dc76fb Mon Sep 17 00:00:00 2001 From: Eric Hibbs Date: Tue, 19 May 2026 12:38:16 -0700 Subject: [PATCH 1/7] test: failing repro for empty title on gptDidYouMean alerts --- tests/core/test_package_and_alerts.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/core/test_package_and_alerts.py b/tests/core/test_package_and_alerts.py index f616479..eb9830d 100644 --- a/tests/core/test_package_and_alerts.py +++ b/tests/core/test_package_and_alerts.py @@ -166,6 +166,26 @@ def test_add_package_alerts_basic(self, core): assert alert.type == "networkAccess" assert alert.severity == "high" + def test_gpt_did_you_mean_gets_typosquat_title(self, core): + """gptDidYouMean alerts must render a non-empty title (CUS2-2).""" + package = self.make_package( + alerts=[{ + "type": "gptDidYouMean", + "key": "gpt-did-you-mean-alert", + "severity": "middle", + }], + topLevelAncestors=[], + ) + + result = core.add_package_alerts_to_collection( + package, alerts_collection={}, packages={package.id: package} + ) + + alert = result["gpt-did-you-mean-alert"][0] + assert alert.type == "gptDidYouMean" + assert alert.title, "title should not be empty for gptDidYouMean" + assert "typosquat" in alert.title.lower() + def test_get_capabilities_for_added_packages(self, core): From fa6e03f36164bd516f6a23773e27bb74df228753 Mon Sep 17 00:00:00 2001 From: Eric Hibbs Date: Tue, 19 May 2026 12:40:33 -0700 Subject: [PATCH 2/7] test: failing repro for empty title on unknown alert types --- tests/core/test_package_and_alerts.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/core/test_package_and_alerts.py b/tests/core/test_package_and_alerts.py index eb9830d..8cf99f1 100644 --- a/tests/core/test_package_and_alerts.py +++ b/tests/core/test_package_and_alerts.py @@ -186,6 +186,24 @@ def test_gpt_did_you_mean_gets_typosquat_title(self, core): assert alert.title, "title should not be empty for gptDidYouMean" assert "typosquat" in alert.title.lower() + def test_unknown_alert_type_falls_back_to_humanized_title(self, core): + """Any alert type not present in the SDK should still render a non-empty title.""" + package = self.make_package( + alerts=[{ + "type": "someBrandNewAlertType", + "key": "future-alert", + "severity": "low", + }], + topLevelAncestors=[], + ) + + result = core.add_package_alerts_to_collection( + package, alerts_collection={}, packages={package.id: package} + ) + + alert = result["future-alert"][0] + assert alert.title == "Some Brand New Alert Type" + def test_get_capabilities_for_added_packages(self, core): From ecb66f50c7c17674a4042075f7119c7e2455739f Mon Sep 17 00:00:00 2001 From: Eric Hibbs Date: Tue, 19 May 2026 12:40:53 -0700 Subject: [PATCH 3/7] test: lock in licenseSpdxDisj title fallback --- tests/core/test_package_and_alerts.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/core/test_package_and_alerts.py b/tests/core/test_package_and_alerts.py index 8cf99f1..1442d10 100644 --- a/tests/core/test_package_and_alerts.py +++ b/tests/core/test_package_and_alerts.py @@ -204,6 +204,24 @@ def test_unknown_alert_type_falls_back_to_humanized_title(self, core): alert = result["future-alert"][0] assert alert.title == "Some Brand New Alert Type" + def test_license_spdx_disj_keeps_explicit_title(self, core): + """licenseSpdxDisj must keep its hard-coded fallback (regression guard for CUS2-2 fix).""" + package = self.make_package( + alerts=[{ + "type": "licenseSpdxDisj", + "key": "license-alert", + "severity": "high", + }], + topLevelAncestors=[], + ) + + result = core.add_package_alerts_to_collection( + package, alerts_collection={}, packages={package.id: package} + ) + + alert = result["license-alert"][0] + assert alert.title == "License Policy Violation" + def test_get_capabilities_for_added_packages(self, core): From e31ab9bec0eccafd3d6d05b2175e14034661c679 Mon Sep 17 00:00:00 2001 From: Eric Hibbs Date: Tue, 19 May 2026 12:45:43 -0700 Subject: [PATCH 4/7] feat(core): add alert-type humanizer and override-map plumbing --- socketsecurity/core/__init__.py | 21 +++++++++++++++++++++ tests/core/test_package_and_alerts.py | 24 ++++++++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/socketsecurity/core/__init__.py b/socketsecurity/core/__init__.py index 0a6b827..d9741bc 100644 --- a/socketsecurity/core/__init__.py +++ b/socketsecurity/core/__init__.py @@ -1,5 +1,6 @@ import logging import os +import re import sys import tarfile import tempfile @@ -44,6 +45,26 @@ version = __version__ log = logging.getLogger("socketdev") +_ALERT_TYPE_TITLE_OVERRIDES = { + "gptDidYouMean": "Possible typosquat attack (GPT)", +} + +_HUMANIZE_BOUNDARY = re.compile(r"(?<=[a-z0-9])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])") + + +def _humanize_alert_type(alert_type: str) -> str: + """Convert a camelCase/PascalCase alert type into a Title-Cased label. + + Used as a last-resort fallback when the SDK does not have metadata for an + alert type and there is no explicit override. Adjacent capitals are kept + together so acronyms like 'SQL' survive ('SQLInjection' -> 'SQL Injection'). + """ + if not alert_type: + return "" + parts = _HUMANIZE_BOUNDARY.split(alert_type) + return " ".join(part[:1].upper() + part[1:] for part in parts if part) + + class Core: """Main class for interacting with Socket Security API and processing scan results.""" diff --git a/tests/core/test_package_and_alerts.py b/tests/core/test_package_and_alerts.py index 1442d10..5034d85 100644 --- a/tests/core/test_package_and_alerts.py +++ b/tests/core/test_package_and_alerts.py @@ -322,3 +322,27 @@ def test_get_license_text_via_purl_uses_org_scoped_endpoint(self, core, mock_sdk ) assert result["npm/lodash@4.18.1"].licenseAttrib == [{"name": "MIT"}] assert result["npm/lodash@4.18.1"].licenseDetails == [{"license": "MIT"}] + + +class TestHumanizeAlertType: + def test_humanizes_camel_case(self): + from socketsecurity.core import _humanize_alert_type + assert _humanize_alert_type("gptDidYouMean") == "Gpt Did You Mean" + + def test_humanizes_single_word(self): + from socketsecurity.core import _humanize_alert_type + assert _humanize_alert_type("malware") == "Malware" + + def test_humanizes_pascal_case(self): + from socketsecurity.core import _humanize_alert_type + assert _humanize_alert_type("UnsafeShellAccess") == "Unsafe Shell Access" + + def test_empty_input_returns_empty_string(self): + from socketsecurity.core import _humanize_alert_type + assert _humanize_alert_type("") == "" + + def test_handles_acronyms_conservatively(self): + """Adjacent capitals are kept together: SQLInjection -> 'SQL Injection'.""" + from socketsecurity.core import _humanize_alert_type + assert _humanize_alert_type("SQLInjection") == "SQL Injection" + From 756a8bea0f563035879b770076084ee4a3f30a42 Mon Sep 17 00:00:00 2001 From: Eric Hibbs Date: Tue, 19 May 2026 12:47:05 -0700 Subject: [PATCH 5/7] fix(core): fall back to humanized title for unmapped alert types Resolves CUS2-2: gptDidYouMean and any future alert type without SDK metadata previously rendered as a blank Alert column in the CLI output table, SARIF report, and PR/security comments. Title resolution now falls back through an explicit override map and a generic humanizer. --- socketsecurity/core/__init__.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/socketsecurity/core/__init__.py b/socketsecurity/core/__init__.py index d9741bc..1f488b2 100644 --- a/socketsecurity/core/__init__.py +++ b/socketsecurity/core/__init__.py @@ -1423,11 +1423,19 @@ def add_package_alerts_to_collection(self, package: Package, alerts_collection: alert = Alert(**alert_item) props = getattr(self.config.all_issues, alert.type, default_props) introduced_by = self.get_source_data(package, packages) - - # Handle special case for license policy violations + + # Title resolution order: + # 1. SDK-provided title (props.title) if non-empty + # 2. Explicit override for known-but-unmapped alert types (e.g. gptDidYouMean) + # 3. Hard-coded special cases (e.g. licenseSpdxDisj) + # 4. Humanized alert.type as last-resort fallback title = props.title - if alert.type == "licenseSpdxDisj" and not title: + if not title: + title = _ALERT_TYPE_TITLE_OVERRIDES.get(alert.type, "") + if not title and alert.type == "licenseSpdxDisj": title = "License Policy Violation" + if not title: + title = _humanize_alert_type(alert.type) issue_alert = Issue( pkg_type=package.type, From 37e256367803900e94bd2084031fa3330d41fda6 Mon Sep 17 00:00:00 2001 From: Eric Hibbs Date: Tue, 19 May 2026 13:04:24 -0700 Subject: [PATCH 6/7] test: hoist _humanize_alert_type import to module scope --- tests/core/test_package_and_alerts.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/tests/core/test_package_and_alerts.py b/tests/core/test_package_and_alerts.py index 5034d85..171eae7 100644 --- a/tests/core/test_package_and_alerts.py +++ b/tests/core/test_package_and_alerts.py @@ -4,7 +4,7 @@ import pytest from socketdev import socketdev -from socketsecurity.core import Core +from socketsecurity.core import Core, _humanize_alert_type from socketsecurity.core.classes import Issue, Package from socketsecurity.core.socket_config import SocketConfig @@ -326,23 +326,18 @@ def test_get_license_text_via_purl_uses_org_scoped_endpoint(self, core, mock_sdk class TestHumanizeAlertType: def test_humanizes_camel_case(self): - from socketsecurity.core import _humanize_alert_type assert _humanize_alert_type("gptDidYouMean") == "Gpt Did You Mean" def test_humanizes_single_word(self): - from socketsecurity.core import _humanize_alert_type assert _humanize_alert_type("malware") == "Malware" def test_humanizes_pascal_case(self): - from socketsecurity.core import _humanize_alert_type assert _humanize_alert_type("UnsafeShellAccess") == "Unsafe Shell Access" def test_empty_input_returns_empty_string(self): - from socketsecurity.core import _humanize_alert_type assert _humanize_alert_type("") == "" def test_handles_acronyms_conservatively(self): """Adjacent capitals are kept together: SQLInjection -> 'SQL Injection'.""" - from socketsecurity.core import _humanize_alert_type assert _humanize_alert_type("SQLInjection") == "SQL Injection" From 57138a72348cb66d0c2ddd448d876df8e182b673 Mon Sep 17 00:00:00 2001 From: Eric Hibbs Date: Fri, 29 May 2026 13:58:32 -0700 Subject: [PATCH 7/7] chore(release): bump to 2.2.92 to clear main collision #199 landed on main between the original 2.2.91 bump and this PR opening, so 2.2.91 ties main and fails check_version. Bump to 2.2.92. --- pyproject.toml | 2 +- socketsecurity/__init__.py | 2 +- uv.lock | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 137b98d..b661dc7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" [project] name = "socketsecurity" -version = "2.2.91" +version = "2.2.92" requires-python = ">= 3.11" license = {"file" = "LICENSE"} dependencies = [ diff --git a/socketsecurity/__init__.py b/socketsecurity/__init__.py index 7fdf737..9f58d90 100644 --- a/socketsecurity/__init__.py +++ b/socketsecurity/__init__.py @@ -1,3 +1,3 @@ __author__ = 'socket.dev' -__version__ = '2.2.91' +__version__ = '2.2.92' USER_AGENT = f'SocketPythonCLI/{__version__}' diff --git a/uv.lock b/uv.lock index e47a62c..88ccbd5 100644 --- a/uv.lock +++ b/uv.lock @@ -1168,7 +1168,7 @@ wheels = [ [[package]] name = "socketsecurity" -version = "2.2.91" +version = "2.2.92" source = { editable = "." } dependencies = [ { name = "bs4" },