Skip to content
Open
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
7 changes: 6 additions & 1 deletion socketsecurity/core/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2755,7 +2755,12 @@ def get_source_data(package: Package, packages: dict) -> list:
top_package = packages.get(top_id)
if top_package:
manifests = ""
top_purl = f"{top_package.type}/{top_package.name}@{top_package.version}"
ancestor_name = (
f"{top_package.namespace}/{top_package.name}"
if top_package.namespace
else top_package.name
)
top_purl = f"{top_package.type}/{ancestor_name}@{top_package.version}"
if hasattr(top_package, "manifestFiles") and top_package.manifestFiles:
for manifest_data in top_package.manifestFiles:
manifest_file = manifest_data.get("file")
Expand Down
116 changes: 114 additions & 2 deletions tests/core/test_json_package_changes_transport.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from socketsecurity.config import CliConfig
from socketsecurity.core import Core
from socketsecurity.core.classes import Package
from socketsecurity.core.messages import Messages
from socketsecurity.core.report_output import build_socket_json_report
from socketsecurity.core.socket_config import SocketConfig
from tests.support.socket_api_fixture import (
Expand Down Expand Up @@ -62,11 +63,13 @@ def _artifact(
ecosystem: str | None = None,
score: dict[str, int | float | None] | None = None,
parent_id: str | None = None,
ancestor_ids: list[str] | None = None,
alerts: list[dict[str, object]] | None = None,
) -> dict[str, object]:
reference: dict[str, object] = {
"diffType": change_type,
"direct": direct,
"topLevelAncestors": [parent_id] if parent_id else [],
"topLevelAncestors": ancestor_ids or ([parent_id] if parent_id else []),
"dependencies": [],
"manifestFiles": [{"file": "requirements.txt"}] if direct else [],
"artifact": {"id": artifact_id},
Expand All @@ -77,7 +80,7 @@ def _artifact(
"type": ecosystem or ("npm" if namespace else "pypi"),
"name": name,
"version": version,
"alerts": [],
"alerts": alerts or [],
"licenseDetails": [],
"author": ["Fixture Author"],
"head" if change_type in ("added", "updated", "unchanged") else "base": [
Expand Down Expand Up @@ -401,6 +404,115 @@ def test_package_change_identity_keeps_namespace_and_bucket_boundaries(
]


@pytest.mark.parametrize("streaming_fallback", [False, True])
def test_transitive_alert_and_package_json_keep_ancestor_namespaces(
socket_api: SocketApiFixture,
streaming_fallback: bool,
) -> None:
core = _core(socket_api)
alert = {
"key": "fixture-env-alert",
"type": "envVars",
"severity": "low",
"category": "supplyChainRisk",
"props": {},
"action": "warn",
}
artifacts = {
"added": [
_artifact("root-one", "added", "widget", "1.0.0", namespace="@one"),
_artifact("root-two", "added", "widget", "1.0.0", namespace="@two"),
_artifact(
"child",
"added",
"leaf",
"2.0.0",
direct=False,
ancestor_ids=["root-one", "root-two"],
alerts=[alert],
),
],
"updated": [],
"removed": [],
"replaced": [],
"unchanged": [],
}
if streaming_fallback:
socket_api.route(
"POST",
"orgs/fixture-org/diff-scans/from-ids",
SocketResponse(403, {"error": {"message": "forbidden"}}),
)
socket_api.route(
"GET",
"orgs/fixture-org/full-scans/diff",
SocketResponse(200, _stream_response(artifacts)),
)
else:
socket_api.route(
"POST",
"orgs/fixture-org/diff-scans/from-ids",
SocketResponse(201, {"diff_scan": {"id": "diff-provenance-fixture"}}),
)
socket_api.route(
"GET",
"orgs/fixture-org/diff-scans/diff-provenance-fixture",
SocketResponse(
200,
{
"diff_scan": {
"artifacts": artifacts,
"before_full_scan": {"id": "baseline-id"},
"after_full_scan": {"id": "new-scan-id"},
}
},
),
)

comparison = core._get_package_comparison(
"baseline-id", "new-scan-id", include_license_details=False
)
request_count = len(socket_api.requests)
expected_sources = [
["npm/@one/widget@1.0.0", "requirements.txt"],
["npm/@two/widget@1.0.0", "requirements.txt"],
]
alert_diff = core.create_diff_report(
comparison.added_packages, comparison.removed_packages, {}
)
assert [package.id for package in alert_diff.new_packages] == [
"root-one",
"root-two",
]
assert alert_diff.new_alerts[0].introduced_by == [
tuple(row) for row in expected_sources
]
_, source_text = Messages.create_sources(alert_diff.new_alerts[0], "raw")
assert source_text == "npm/@one/widget@1.0.0;npm/@two/widget@1.0.0"
alert_report = build_socket_json_report(
alert_diff, CliConfig(api_token="fictional-local-only", repo="fixture-repo")
)
assert alert_report["new_alerts"][0]["introduced_by"] == expected_sources
assert alert_report["scan_failed"] is False

full_diff = core.create_diff_report(
comparison.added_packages,
comparison.removed_packages,
{},
direct_only=False,
)
child_purl = next(
package for package in full_diff.new_packages if package.id == "child"
)
assert child_purl.introduced_by == [tuple(row) for row in expected_sources]
full_report = build_socket_json_report(
full_diff, CliConfig(api_token="fictional-local-only", repo="fixture-repo")
)
child_row = next(row for row in full_report["new_packages"] if row["id"] == "child")
assert child_row["introduced_by"] == [tuple(row) for row in expected_sources]
assert len(socket_api.requests) == request_count


def test_package_change_identity_treats_missing_and_empty_namespace_equally(
socket_api: SocketApiFixture,
) -> None:
Expand Down
179 changes: 179 additions & 0 deletions tests/unit/test_package_provenance.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
import copy
import json

from socketsecurity.config import CliConfig
from socketsecurity.core import Core
from socketsecurity.core.classes import Diff, Issue, Package
from socketsecurity.core.messages import Messages
from socketsecurity.core.report_output import build_socket_json_report


def _package(
package_id: str,
*,
name: str = "widget",
version: str = "1.0.0",
ecosystem: str = "npm",
namespace: str | None = None,
direct: bool = False,
ancestors: list[str] | None = None,
manifests: list[dict[str, str]] | None = None,
purl: str = "",
) -> Package:
package = Package(
type=ecosystem,
namespace=namespace,
name=name,
version=version,
id=package_id,
score={},
alerts=[],
direct=direct,
topLevelAncestors=ancestors,
manifestFiles=manifests,
purl=purl,
)
return package


def _issue(introduced_by: list[tuple[str, str]]) -> Issue:
return Issue(
pkg_type="npm",
pkg_name="leaf",
pkg_version="1.0.0",
category="supplyChainRisk",
type="envVars",
severity="low",
pkg_id="npm/leaf@1.0.0",
props={},
key="fixture-alert",
error=False,
warn=True,
ignore=False,
monitor=False,
description="Fixture alert",
title="Fixture alert",
emoji="!",
next_step_title="Review package",
suggestion="",
introduced_by=introduced_by,
purl="pkg:npm/leaf@1.0.0",
url="https://socket.dev/npm/package/leaf/alerts/1.0.0",
)


def test_source_data_preserves_scopes_and_maven_groups_and_consumers():
first = _package("first", namespace="@one", manifests=[{"file": "package.json"}])
second = _package("second", namespace="@two", manifests=[{"file": "package.json"}])
maven = _package(
"maven",
ecosystem="maven",
namespace="com.one",
manifests=[{"file": "pom.xml"}],
)
child = _package("child", ancestors=["first", "second", "maven"])
packages = {package.id: package for package in (first, second, maven, child)}
before = copy.deepcopy(packages)

sources = Core.get_source_data(child, packages)

assert sources == [
("npm/@one/widget@1.0.0", "package.json"),
("npm/@two/widget@1.0.0", "package.json"),
("maven/com.one/widget@1.0.0", "pom.xml"),
]
assert packages == before
issue = _issue(sources[:2])
_, raw = Messages.create_sources(issue, "raw")
_, console = Messages.create_sources(issue, "console")
_, markdown = Messages.create_sources(issue, "md")
assert raw == "npm/@one/widget@1.0.0;npm/@two/widget@1.0.0"
assert console == raw
assert "npm/@one/widget@1.0.0" in markdown
assert "npm/@two/widget@1.0.0" in markdown

report = build_socket_json_report(
Diff(new_alerts=[issue]), CliConfig(api_token="", repo="fixture")
)
serialized = json.loads(json.dumps(report))
assert serialized["new_alerts"][0]["introduced_by"] == [
["npm/@one/widget@1.0.0", "package.json"],
["npm/@two/widget@1.0.0", "package.json"],
]
assert len(serialized) == 18


def test_source_data_preserves_unscoped_direct_missing_and_order_behavior():
unscoped = _package(
"root",
ecosystem="pypi",
namespace=None,
manifests=[{"file": "requirements.in"}, {"file": "requirements.txt"}],
)
empty = _package(
"empty",
ecosystem="pypi",
namespace="",
manifests=[{"file": "pyproject.toml"}],
)
direct = _package(
"direct",
direct=True,
manifests=[{"file": "one.json"}, {"file": "two.json"}],
)
no_manifest = _package("no-manifest", manifests=[])
child = _package(
"child", ancestors=["root", "empty", "missing", "root", "no-manifest"]
)
packages = {item.id: item for item in (unscoped, empty, direct, no_manifest, child)}

assert Core.get_source_data(child, packages) == [
("pypi/widget@1.0.0", "requirements.in;requirements.txt"),
("pypi/widget@1.0.0", "pyproject.toml"),
("pypi/widget@1.0.0", "requirements.in;requirements.txt"),
]
assert Core.get_source_data(direct, packages) == [("direct", "one.json;two.json")]
assert Core.get_source_data(_package("orphan", ancestors=[]), packages) == []
assert (
Core.get_source_data(_package("missing", ancestors=["absent"]), packages) == []
)


def test_source_data_uses_structured_identity_without_mutating_purls_or_traversing():
artifact = {
"id": "socket",
"type": "npm",
"namespace": "@one",
"name": "widget",
"version": "1.0.0",
"score": {},
"alerts": [],
"topLevelAncestors": [],
"manifestFiles": [{"file": "package.json"}],
}
socket_package = Package.from_socket_artifact(artifact)
comparison_package = _package(
"comparison",
namespace="@one",
purl="@one/widget@1.0.0",
manifests=[{"file": "package.json"}],
)
Core.update_package_values(comparison_package)
child = _package("child", ancestors=["socket", "comparison", "socket", "child"])
socket_package.topLevelAncestors = ["comparison"]
comparison_package.topLevelAncestors = ["socket"]
packages = {item.id: item for item in (socket_package, comparison_package, child)}
original_purls = (socket_package.purl, comparison_package.purl)
original = copy.deepcopy(packages)

assert Core.get_source_data(child, packages) == [
("npm/@one/widget@1.0.0", "package.json"),
("npm/@one/widget@1.0.0", "package.json"),
("npm/@one/widget@1.0.0", "package.json"),
]
assert Core.get_source_data(socket_package, packages) == [
("npm/@one/widget@1.0.0", "package.json")
]
assert socket_package.purl == original_purls[0]
assert comparison_package.purl == original_purls[1]
assert packages == original
Loading