Skip to content

Commit cc24682

Browse files
authored
fix(core): fall back to humanized title for unmapped alert types (#208)
* test: failing repro for empty title on gptDidYouMean alerts * test: failing repro for empty title on unknown alert types * test: lock in licenseSpdxDisj title fallback * feat(core): add alert-type humanizer and override-map plumbing * 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. * test: hoist _humanize_alert_type import to module scope * 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.
1 parent d502ab3 commit cc24682

5 files changed

Lines changed: 111 additions & 7 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ build-backend = "hatchling.build"
66

77
[project]
88
name = "socketsecurity"
9-
version = "2.2.91"
9+
version = "2.2.92"
1010
requires-python = ">= 3.11"
1111
license = {"file" = "LICENSE"}
1212
dependencies = [

socketsecurity/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
__author__ = 'socket.dev'
2-
__version__ = '2.2.91'
2+
__version__ = '2.2.92'
33
USER_AGENT = f'SocketPythonCLI/{__version__}'

socketsecurity/core/__init__.py

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import logging
22
import os
3+
import re
34
import sys
45
import tarfile
56
import tempfile
@@ -44,6 +45,26 @@
4445
version = __version__
4546
log = logging.getLogger("socketdev")
4647

48+
_ALERT_TYPE_TITLE_OVERRIDES = {
49+
"gptDidYouMean": "Possible typosquat attack (GPT)",
50+
}
51+
52+
_HUMANIZE_BOUNDARY = re.compile(r"(?<=[a-z0-9])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])")
53+
54+
55+
def _humanize_alert_type(alert_type: str) -> str:
56+
"""Convert a camelCase/PascalCase alert type into a Title-Cased label.
57+
58+
Used as a last-resort fallback when the SDK does not have metadata for an
59+
alert type and there is no explicit override. Adjacent capitals are kept
60+
together so acronyms like 'SQL' survive ('SQLInjection' -> 'SQL Injection').
61+
"""
62+
if not alert_type:
63+
return ""
64+
parts = _HUMANIZE_BOUNDARY.split(alert_type)
65+
return " ".join(part[:1].upper() + part[1:] for part in parts if part)
66+
67+
4768
class Core:
4869
"""Main class for interacting with Socket Security API and processing scan results."""
4970

@@ -1402,11 +1423,19 @@ def add_package_alerts_to_collection(self, package: Package, alerts_collection:
14021423
alert = Alert(**alert_item)
14031424
props = getattr(self.config.all_issues, alert.type, default_props)
14041425
introduced_by = self.get_source_data(package, packages)
1405-
1406-
# Handle special case for license policy violations
1426+
1427+
# Title resolution order:
1428+
# 1. SDK-provided title (props.title) if non-empty
1429+
# 2. Explicit override for known-but-unmapped alert types (e.g. gptDidYouMean)
1430+
# 3. Hard-coded special cases (e.g. licenseSpdxDisj)
1431+
# 4. Humanized alert.type as last-resort fallback
14071432
title = props.title
1408-
if alert.type == "licenseSpdxDisj" and not title:
1433+
if not title:
1434+
title = _ALERT_TYPE_TITLE_OVERRIDES.get(alert.type, "")
1435+
if not title and alert.type == "licenseSpdxDisj":
14091436
title = "License Policy Violation"
1437+
if not title:
1438+
title = _humanize_alert_type(alert.type)
14101439

14111440
issue_alert = Issue(
14121441
pkg_type=package.type,

tests/core/test_package_and_alerts.py

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import pytest
55
from socketdev import socketdev
66

7-
from socketsecurity.core import Core
7+
from socketsecurity.core import Core, _humanize_alert_type
88
from socketsecurity.core.classes import Issue, Package
99
from socketsecurity.core.socket_config import SocketConfig
1010

@@ -166,6 +166,62 @@ def test_add_package_alerts_basic(self, core):
166166
assert alert.type == "networkAccess"
167167
assert alert.severity == "high"
168168

169+
def test_gpt_did_you_mean_gets_typosquat_title(self, core):
170+
"""gptDidYouMean alerts must render a non-empty title (CUS2-2)."""
171+
package = self.make_package(
172+
alerts=[{
173+
"type": "gptDidYouMean",
174+
"key": "gpt-did-you-mean-alert",
175+
"severity": "middle",
176+
}],
177+
topLevelAncestors=[],
178+
)
179+
180+
result = core.add_package_alerts_to_collection(
181+
package, alerts_collection={}, packages={package.id: package}
182+
)
183+
184+
alert = result["gpt-did-you-mean-alert"][0]
185+
assert alert.type == "gptDidYouMean"
186+
assert alert.title, "title should not be empty for gptDidYouMean"
187+
assert "typosquat" in alert.title.lower()
188+
189+
def test_unknown_alert_type_falls_back_to_humanized_title(self, core):
190+
"""Any alert type not present in the SDK should still render a non-empty title."""
191+
package = self.make_package(
192+
alerts=[{
193+
"type": "someBrandNewAlertType",
194+
"key": "future-alert",
195+
"severity": "low",
196+
}],
197+
topLevelAncestors=[],
198+
)
199+
200+
result = core.add_package_alerts_to_collection(
201+
package, alerts_collection={}, packages={package.id: package}
202+
)
203+
204+
alert = result["future-alert"][0]
205+
assert alert.title == "Some Brand New Alert Type"
206+
207+
def test_license_spdx_disj_keeps_explicit_title(self, core):
208+
"""licenseSpdxDisj must keep its hard-coded fallback (regression guard for CUS2-2 fix)."""
209+
package = self.make_package(
210+
alerts=[{
211+
"type": "licenseSpdxDisj",
212+
"key": "license-alert",
213+
"severity": "high",
214+
}],
215+
topLevelAncestors=[],
216+
)
217+
218+
result = core.add_package_alerts_to_collection(
219+
package, alerts_collection={}, packages={package.id: package}
220+
)
221+
222+
alert = result["license-alert"][0]
223+
assert alert.title == "License Policy Violation"
224+
169225

170226

171227
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
266322
)
267323
assert result["npm/lodash@4.18.1"].licenseAttrib == [{"name": "MIT"}]
268324
assert result["npm/lodash@4.18.1"].licenseDetails == [{"license": "MIT"}]
325+
326+
327+
class TestHumanizeAlertType:
328+
def test_humanizes_camel_case(self):
329+
assert _humanize_alert_type("gptDidYouMean") == "Gpt Did You Mean"
330+
331+
def test_humanizes_single_word(self):
332+
assert _humanize_alert_type("malware") == "Malware"
333+
334+
def test_humanizes_pascal_case(self):
335+
assert _humanize_alert_type("UnsafeShellAccess") == "Unsafe Shell Access"
336+
337+
def test_empty_input_returns_empty_string(self):
338+
assert _humanize_alert_type("") == ""
339+
340+
def test_handles_acronyms_conservatively(self):
341+
"""Adjacent capitals are kept together: SQLInjection -> 'SQL Injection'."""
342+
assert _humanize_alert_type("SQLInjection") == "SQL Injection"
343+

uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)