Skip to content

Commit b643f53

Browse files
committed
feat: compute SBOM dependencyPaths from topLevelAncestors
Replaces the stub that always returned [package.name] with real logic: direct deps emit just their name; transitive deps emit one "<ancestor> > <package>" chain per top-level ancestor, falling back to name-only when ancestors are absent or not in the lookup.
1 parent 0f7e8b0 commit b643f53

2 files changed

Lines changed: 64 additions & 2 deletions

File tree

socketsecurity/fossa_compat.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -415,8 +415,17 @@ def _build_dependency_entry(package: Package, dependency_paths: list[str]) -> di
415415

416416

417417
def _compute_dependency_paths(package: Package, package_lookup: dict[str, Package]) -> list[str]:
418-
"""Stub: filled in by Task 9. For now: package name only."""
419-
return [package.name]
418+
if bool(getattr(package, "direct", False)):
419+
return [package.name]
420+
ancestors = getattr(package, "topLevelAncestors", None) or []
421+
paths = []
422+
for ancestor_id in ancestors:
423+
ancestor = package_lookup.get(ancestor_id)
424+
if ancestor and getattr(ancestor, "name", None):
425+
paths.append(f"{ancestor.name} > {package.name}")
426+
if not paths:
427+
return [package.name]
428+
return paths
420429

421430

422431
def _partition_dependencies(packages: list[Package]) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:

tests/unit/test_fossa_compat.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,59 @@ def test_attribution_partitions_direct_vs_deep():
349349
assert deep_names == ["b"]
350350

351351

352+
def test_dependency_paths_direct_package_is_name_only():
353+
from socketsecurity.fossa_compat import _compute_dependency_paths
354+
pkg = Package(
355+
type="pypi", name="requests", version="2.31.0",
356+
id="pip+requests$2.31.0", score={}, alerts=[], direct=True,
357+
)
358+
paths = _compute_dependency_paths(pkg, {"pip+requests$2.31.0": pkg})
359+
assert paths == ["requests"]
360+
361+
362+
def test_dependency_paths_transitive_chains_through_ancestor_name():
363+
from socketsecurity.fossa_compat import _compute_dependency_paths
364+
parent = Package(
365+
type="pypi", name="requests", version="2.31.0",
366+
id="parent-id", score={}, alerts=[], direct=True,
367+
)
368+
child = Package(
369+
type="pypi", name="certifi", version="2024.7.4",
370+
id="child-id", score={}, alerts=[], direct=False,
371+
topLevelAncestors=["parent-id"],
372+
)
373+
lookup = {"parent-id": parent, "child-id": child}
374+
assert _compute_dependency_paths(child, lookup) == ["requests > certifi"]
375+
376+
377+
def test_dependency_paths_multi_ancestor_emits_one_per_root():
378+
from socketsecurity.fossa_compat import _compute_dependency_paths
379+
p1 = Package(type="pypi", name="boto3", version="1.0", id="p1",
380+
score={}, alerts=[], direct=True)
381+
p2 = Package(type="pypi", name="botocore", version="1.0", id="p2",
382+
score={}, alerts=[], direct=True)
383+
child = Package(
384+
type="pypi", name="jmespath", version="1.0", id="c",
385+
score={}, alerts=[], direct=False,
386+
topLevelAncestors=["p1", "p2"],
387+
)
388+
lookup = {"p1": p1, "p2": p2, "c": child}
389+
assert sorted(_compute_dependency_paths(child, lookup)) == [
390+
"boto3 > jmespath",
391+
"botocore > jmespath",
392+
]
393+
394+
395+
def test_dependency_paths_missing_ancestor_falls_back_to_name():
396+
from socketsecurity.fossa_compat import _compute_dependency_paths
397+
pkg = Package(
398+
type="pypi", name="orphan", version="1.0", id="o",
399+
score={}, alerts=[], direct=False,
400+
topLevelAncestors=["missing-id"],
401+
)
402+
assert _compute_dependency_paths(pkg, {"o": pkg}) == ["orphan"]
403+
404+
352405
def test_vulnerability_version_ranges_sourced_from_socket_fields():
353406
"""affectedVersionRanges/patchedVersionRanges come from Socket's singular fields, wrapped."""
354407
from socketsecurity.fossa_compat import _build_vulnerability_entry

0 commit comments

Comments
 (0)