Skip to content

Commit 313bcc1

Browse files
committed
Keep namespaces in dependency source labels
Identify scoped dependency ancestors correctly in package and alert reports. Preserve manifest paths and existing traversal behavior. Assisted-by: Codex:GPT-6
1 parent 60183b1 commit 313bcc1

3 files changed

Lines changed: 299 additions & 3 deletions

File tree

‎socketsecurity/core/__init__.py‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2755,7 +2755,12 @@ def get_source_data(package: Package, packages: dict) -> list:
27552755
top_package = packages.get(top_id)
27562756
if top_package:
27572757
manifests = ""
2758-
top_purl = f"{top_package.type}/{top_package.name}@{top_package.version}"
2758+
ancestor_name = (
2759+
f"{top_package.namespace}/{top_package.name}"
2760+
if top_package.namespace
2761+
else top_package.name
2762+
)
2763+
top_purl = f"{top_package.type}/{ancestor_name}@{top_package.version}"
27592764
if hasattr(top_package, "manifestFiles") and top_package.manifestFiles:
27602765
for manifest_data in top_package.manifestFiles:
27612766
manifest_file = manifest_data.get("file")

‎tests/core/test_json_package_changes_transport.py‎

Lines changed: 114 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from socketsecurity.config import CliConfig
1313
from socketsecurity.core import Core
1414
from socketsecurity.core.classes import Package
15+
from socketsecurity.core.messages import Messages
1516
from socketsecurity.core.report_output import build_socket_json_report
1617
from socketsecurity.core.socket_config import SocketConfig
1718
from tests.support.socket_api_fixture import (
@@ -62,11 +63,13 @@ def _artifact(
6263
ecosystem: str | None = None,
6364
score: dict[str, int | float | None] | None = None,
6465
parent_id: str | None = None,
66+
ancestor_ids: list[str] | None = None,
67+
alerts: list[dict[str, object]] | None = None,
6568
) -> dict[str, object]:
6669
reference: dict[str, object] = {
6770
"diffType": change_type,
6871
"direct": direct,
69-
"topLevelAncestors": [parent_id] if parent_id else [],
72+
"topLevelAncestors": ancestor_ids or ([parent_id] if parent_id else []),
7073
"dependencies": [],
7174
"manifestFiles": [{"file": "requirements.txt"}] if direct else [],
7275
"artifact": {"id": artifact_id},
@@ -77,7 +80,7 @@ def _artifact(
7780
"type": ecosystem or ("npm" if namespace else "pypi"),
7881
"name": name,
7982
"version": version,
80-
"alerts": [],
83+
"alerts": alerts or [],
8184
"licenseDetails": [],
8285
"author": ["Fixture Author"],
8386
"head" if change_type in ("added", "updated", "unchanged") else "base": [
@@ -401,6 +404,115 @@ def test_package_change_identity_keeps_namespace_and_bucket_boundaries(
401404
]
402405

403406

407+
@pytest.mark.parametrize("streaming_fallback", [False, True])
408+
def test_transitive_alert_and_package_json_keep_ancestor_namespaces(
409+
socket_api: SocketApiFixture,
410+
streaming_fallback: bool,
411+
) -> None:
412+
core = _core(socket_api)
413+
alert = {
414+
"key": "fixture-env-alert",
415+
"type": "envVars",
416+
"severity": "low",
417+
"category": "supplyChainRisk",
418+
"props": {},
419+
"action": "warn",
420+
}
421+
artifacts = {
422+
"added": [
423+
_artifact("root-one", "added", "widget", "1.0.0", namespace="@one"),
424+
_artifact("root-two", "added", "widget", "1.0.0", namespace="@two"),
425+
_artifact(
426+
"child",
427+
"added",
428+
"leaf",
429+
"2.0.0",
430+
direct=False,
431+
ancestor_ids=["root-one", "root-two"],
432+
alerts=[alert],
433+
),
434+
],
435+
"updated": [],
436+
"removed": [],
437+
"replaced": [],
438+
"unchanged": [],
439+
}
440+
if streaming_fallback:
441+
socket_api.route(
442+
"POST",
443+
"orgs/fixture-org/diff-scans/from-ids",
444+
SocketResponse(403, {"error": {"message": "forbidden"}}),
445+
)
446+
socket_api.route(
447+
"GET",
448+
"orgs/fixture-org/full-scans/diff",
449+
SocketResponse(200, _stream_response(artifacts)),
450+
)
451+
else:
452+
socket_api.route(
453+
"POST",
454+
"orgs/fixture-org/diff-scans/from-ids",
455+
SocketResponse(201, {"diff_scan": {"id": "diff-provenance-fixture"}}),
456+
)
457+
socket_api.route(
458+
"GET",
459+
"orgs/fixture-org/diff-scans/diff-provenance-fixture",
460+
SocketResponse(
461+
200,
462+
{
463+
"diff_scan": {
464+
"artifacts": artifacts,
465+
"before_full_scan": {"id": "baseline-id"},
466+
"after_full_scan": {"id": "new-scan-id"},
467+
}
468+
},
469+
),
470+
)
471+
472+
comparison = core._get_package_comparison(
473+
"baseline-id", "new-scan-id", include_license_details=False
474+
)
475+
request_count = len(socket_api.requests)
476+
expected_sources = [
477+
["npm/@one/widget@1.0.0", "requirements.txt"],
478+
["npm/@two/widget@1.0.0", "requirements.txt"],
479+
]
480+
alert_diff = core.create_diff_report(
481+
comparison.added_packages, comparison.removed_packages, {}
482+
)
483+
assert [package.id for package in alert_diff.new_packages] == [
484+
"root-one",
485+
"root-two",
486+
]
487+
assert alert_diff.new_alerts[0].introduced_by == [
488+
tuple(row) for row in expected_sources
489+
]
490+
_, source_text = Messages.create_sources(alert_diff.new_alerts[0], "raw")
491+
assert source_text == "npm/@one/widget@1.0.0;npm/@two/widget@1.0.0"
492+
alert_report = build_socket_json_report(
493+
alert_diff, CliConfig(api_token="fictional-local-only", repo="fixture-repo")
494+
)
495+
assert alert_report["new_alerts"][0]["introduced_by"] == expected_sources
496+
assert alert_report["scan_failed"] is False
497+
498+
full_diff = core.create_diff_report(
499+
comparison.added_packages,
500+
comparison.removed_packages,
501+
{},
502+
direct_only=False,
503+
)
504+
child_purl = next(
505+
package for package in full_diff.new_packages if package.id == "child"
506+
)
507+
assert child_purl.introduced_by == [tuple(row) for row in expected_sources]
508+
full_report = build_socket_json_report(
509+
full_diff, CliConfig(api_token="fictional-local-only", repo="fixture-repo")
510+
)
511+
child_row = next(row for row in full_report["new_packages"] if row["id"] == "child")
512+
assert child_row["introduced_by"] == [tuple(row) for row in expected_sources]
513+
assert len(socket_api.requests) == request_count
514+
515+
404516
def test_package_change_identity_treats_missing_and_empty_namespace_equally(
405517
socket_api: SocketApiFixture,
406518
) -> None:
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
import copy
2+
import json
3+
4+
from socketsecurity.config import CliConfig
5+
from socketsecurity.core import Core
6+
from socketsecurity.core.classes import Diff, Issue, Package
7+
from socketsecurity.core.messages import Messages
8+
from socketsecurity.core.report_output import build_socket_json_report
9+
10+
11+
def _package(
12+
package_id: str,
13+
*,
14+
name: str = "widget",
15+
version: str = "1.0.0",
16+
ecosystem: str = "npm",
17+
namespace: str | None = None,
18+
direct: bool = False,
19+
ancestors: list[str] | None = None,
20+
manifests: list[dict[str, str]] | None = None,
21+
purl: str = "",
22+
) -> Package:
23+
package = Package(
24+
type=ecosystem,
25+
namespace=namespace,
26+
name=name,
27+
version=version,
28+
id=package_id,
29+
score={},
30+
alerts=[],
31+
direct=direct,
32+
topLevelAncestors=ancestors,
33+
manifestFiles=manifests,
34+
purl=purl,
35+
)
36+
return package
37+
38+
39+
def _issue(introduced_by: list[tuple[str, str]]) -> Issue:
40+
return Issue(
41+
pkg_type="npm",
42+
pkg_name="leaf",
43+
pkg_version="1.0.0",
44+
category="supplyChainRisk",
45+
type="envVars",
46+
severity="low",
47+
pkg_id="npm/leaf@1.0.0",
48+
props={},
49+
key="fixture-alert",
50+
error=False,
51+
warn=True,
52+
ignore=False,
53+
monitor=False,
54+
description="Fixture alert",
55+
title="Fixture alert",
56+
emoji="!",
57+
next_step_title="Review package",
58+
suggestion="",
59+
introduced_by=introduced_by,
60+
purl="pkg:npm/leaf@1.0.0",
61+
url="https://socket.dev/npm/package/leaf/alerts/1.0.0",
62+
)
63+
64+
65+
def test_source_data_preserves_scopes_and_maven_groups_and_consumers():
66+
first = _package("first", namespace="@one", manifests=[{"file": "package.json"}])
67+
second = _package("second", namespace="@two", manifests=[{"file": "package.json"}])
68+
maven = _package(
69+
"maven",
70+
ecosystem="maven",
71+
namespace="com.one",
72+
manifests=[{"file": "pom.xml"}],
73+
)
74+
child = _package("child", ancestors=["first", "second", "maven"])
75+
packages = {package.id: package for package in (first, second, maven, child)}
76+
before = copy.deepcopy(packages)
77+
78+
sources = Core.get_source_data(child, packages)
79+
80+
assert sources == [
81+
("npm/@one/widget@1.0.0", "package.json"),
82+
("npm/@two/widget@1.0.0", "package.json"),
83+
("maven/com.one/widget@1.0.0", "pom.xml"),
84+
]
85+
assert packages == before
86+
issue = _issue(sources[:2])
87+
_, raw = Messages.create_sources(issue, "raw")
88+
_, console = Messages.create_sources(issue, "console")
89+
_, markdown = Messages.create_sources(issue, "md")
90+
assert raw == "npm/@one/widget@1.0.0;npm/@two/widget@1.0.0"
91+
assert console == raw
92+
assert "npm/@one/widget@1.0.0" in markdown
93+
assert "npm/@two/widget@1.0.0" in markdown
94+
95+
report = build_socket_json_report(
96+
Diff(new_alerts=[issue]), CliConfig(api_token="", repo="fixture")
97+
)
98+
serialized = json.loads(json.dumps(report))
99+
assert serialized["new_alerts"][0]["introduced_by"] == [
100+
["npm/@one/widget@1.0.0", "package.json"],
101+
["npm/@two/widget@1.0.0", "package.json"],
102+
]
103+
assert len(serialized) == 18
104+
105+
106+
def test_source_data_preserves_unscoped_direct_missing_and_order_behavior():
107+
unscoped = _package(
108+
"root",
109+
ecosystem="pypi",
110+
namespace=None,
111+
manifests=[{"file": "requirements.in"}, {"file": "requirements.txt"}],
112+
)
113+
empty = _package(
114+
"empty",
115+
ecosystem="pypi",
116+
namespace="",
117+
manifests=[{"file": "pyproject.toml"}],
118+
)
119+
direct = _package(
120+
"direct",
121+
direct=True,
122+
manifests=[{"file": "one.json"}, {"file": "two.json"}],
123+
)
124+
no_manifest = _package("no-manifest", manifests=[])
125+
child = _package(
126+
"child", ancestors=["root", "empty", "missing", "root", "no-manifest"]
127+
)
128+
packages = {item.id: item for item in (unscoped, empty, direct, no_manifest, child)}
129+
130+
assert Core.get_source_data(child, packages) == [
131+
("pypi/widget@1.0.0", "requirements.in;requirements.txt"),
132+
("pypi/widget@1.0.0", "pyproject.toml"),
133+
("pypi/widget@1.0.0", "requirements.in;requirements.txt"),
134+
]
135+
assert Core.get_source_data(direct, packages) == [("direct", "one.json;two.json")]
136+
assert Core.get_source_data(_package("orphan", ancestors=[]), packages) == []
137+
assert (
138+
Core.get_source_data(_package("missing", ancestors=["absent"]), packages) == []
139+
)
140+
141+
142+
def test_source_data_uses_structured_identity_without_mutating_purls_or_traversing():
143+
artifact = {
144+
"id": "socket",
145+
"type": "npm",
146+
"namespace": "@one",
147+
"name": "widget",
148+
"version": "1.0.0",
149+
"score": {},
150+
"alerts": [],
151+
"topLevelAncestors": [],
152+
"manifestFiles": [{"file": "package.json"}],
153+
}
154+
socket_package = Package.from_socket_artifact(artifact)
155+
comparison_package = _package(
156+
"comparison",
157+
namespace="@one",
158+
purl="@one/widget@1.0.0",
159+
manifests=[{"file": "package.json"}],
160+
)
161+
Core.update_package_values(comparison_package)
162+
child = _package("child", ancestors=["socket", "comparison", "socket", "child"])
163+
socket_package.topLevelAncestors = ["comparison"]
164+
comparison_package.topLevelAncestors = ["socket"]
165+
packages = {item.id: item for item in (socket_package, comparison_package, child)}
166+
original_purls = (socket_package.purl, comparison_package.purl)
167+
original = copy.deepcopy(packages)
168+
169+
assert Core.get_source_data(child, packages) == [
170+
("npm/@one/widget@1.0.0", "package.json"),
171+
("npm/@one/widget@1.0.0", "package.json"),
172+
("npm/@one/widget@1.0.0", "package.json"),
173+
]
174+
assert Core.get_source_data(socket_package, packages) == [
175+
("npm/@one/widget@1.0.0", "package.json")
176+
]
177+
assert socket_package.purl == original_purls[0]
178+
assert comparison_package.purl == original_purls[1]
179+
assert packages == original

0 commit comments

Comments
 (0)