Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down
2 changes: 1 addition & 1 deletion socketsecurity/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
__author__ = 'socket.dev'
__version__ = '2.2.91'
__version__ = '2.2.92'
USER_AGENT = f'SocketPythonCLI/{__version__}'
35 changes: 32 additions & 3 deletions socketsecurity/core/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import logging
import os
import re
import sys
import tarfile
import tempfile
Expand Down Expand Up @@ -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."""

Expand Down Expand Up @@ -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,
Expand Down
77 changes: 76 additions & 1 deletion tests/core/test_package_and_alerts.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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"

2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading