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/socketsecurity/core/__init__.py b/socketsecurity/core/__init__.py index 0a6b827..1f488b2 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.""" @@ -1402,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, diff --git a/tests/core/test_package_and_alerts.py b/tests/core/test_package_and_alerts.py index f616479..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 @@ -166,6 +166,62 @@ 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_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_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): @@ -266,3 +322,22 @@ 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): + assert _humanize_alert_type("gptDidYouMean") == "Gpt Did You Mean" + + def test_humanizes_single_word(self): + assert _humanize_alert_type("malware") == "Malware" + + def test_humanizes_pascal_case(self): + assert _humanize_alert_type("UnsafeShellAccess") == "Unsafe Shell Access" + + def test_empty_input_returns_empty_string(self): + assert _humanize_alert_type("") == "" + + def test_handles_acronyms_conservatively(self): + """Adjacent capitals are kept together: SQLInjection -> 'SQL Injection'.""" + assert _humanize_alert_type("SQLInjection") == "SQL Injection" + 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" },