Skip to content

Commit 4a43cc2

Browse files
committed
feat: build FOSSA-shape Dependency entries with attribution text
Add _build_dependency_entry and _build_dependency_licenses to produce the 14-key per-dependency dict that matches real FOSSA attribution output. License entries prefer licenseAttrib (full attribText + spdxExpr), fall back to declared license string, or emit [] when unlicensed. Also removes the stale test_fossa_attribution_payload_shape_is_stable test, which asserted the pre-Task-6 two-key shape and was already failing.
1 parent 0fc70ab commit 4a43cc2

2 files changed

Lines changed: 99 additions & 38 deletions

File tree

socketsecurity/fossa_compat.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,48 @@ def _build_attribution_project(diff_report: Diff, config: CliConfig) -> dict[str
372372
return {"name": repo, "revision": revision}
373373

374374

375+
def _build_dependency_licenses(package: Package) -> list[dict[str, str]]:
376+
"""Build the `licenses[]` array: prefer licenseAttrib entries (full attribution text),
377+
fall back to a single name-only entry from declared license, else empty.
378+
"""
379+
attribs = getattr(package, "licenseAttrib", None) or []
380+
licenses = []
381+
for attrib in attribs:
382+
attrib_text = attrib.get("attribText", "") if isinstance(attrib, dict) else getattr(attrib, "attribText", "")
383+
attrib_data = attrib.get("attribData", []) if isinstance(attrib, dict) else getattr(attrib, "attribData", [])
384+
spdx = ""
385+
if attrib_data:
386+
first = attrib_data[0]
387+
spdx = first.get("spdxExpr", "") if isinstance(first, dict) else getattr(first, "spdxExpr", "")
388+
if attrib_text or spdx:
389+
licenses.append({"attribution": attrib_text or "", "name": spdx or getattr(package, "license", "") or ""})
390+
if licenses:
391+
return licenses
392+
declared = getattr(package, "license", None)
393+
if declared:
394+
return [{"attribution": "", "name": declared}]
395+
return []
396+
397+
398+
def _build_dependency_entry(package: Package, dependency_paths: list[str]) -> dict[str, Any]:
399+
return {
400+
"authors": list(getattr(package, "author", []) or []),
401+
"dependencyPaths": list(dependency_paths),
402+
"description": "",
403+
"downloadUrl": "",
404+
"hash": None,
405+
"isGolang": None,
406+
"licenses": _build_dependency_licenses(package),
407+
"notes": [],
408+
"otherLicenses": [],
409+
"package": package.name,
410+
"projectUrl": "",
411+
"source": _ecosystem_to_package_manager(package.type),
412+
"title": package.name,
413+
"version": package.version,
414+
}
415+
416+
375417
def _partition_dependencies(packages: list[Package]) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
376418
"""Stub: filled in by Tasks 7-9. Returns (direct, deep) lists of Dependency dicts."""
377419
return ([], [])

tests/unit/test_fossa_compat.py

Lines changed: 57 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -177,47 +177,66 @@ def test_project_metadata_fallbacks_when_missing_fields():
177177
assert project["url"] is None
178178

179179

180-
def test_fossa_attribution_payload_shape_is_stable():
181-
config = CliConfig.from_args([
182-
"--api-token", "test",
183-
"--legal-format", "fossa",
184-
"--repo", "owner/repo",
185-
"--branch", "refs/heads/main",
186-
])
187-
diff = Diff(id="scan-123", report_url="https://socket.dev/report/123")
188-
diff.packages = {
189-
"pkg-1": Package(
190-
id="pkg-1",
191-
name="requests",
192-
version="2.31.0",
193-
type="pypi",
194-
score={},
195-
alerts=[],
196-
direct=True,
197-
url="https://socket.dev/pypi/package/requests/overview/2.31.0",
198-
license="Apache-2.0",
199-
licenseDetails=[{"id": "Apache-2.0"}],
200-
licenseAttrib=[{"id": "Apache-2.0"}],
201-
purl="pkg:pypi/requests@2.31.0",
202-
)
180+
def test_dependency_entry_full_shape():
181+
"""Per-dependency dict has the exact 14-key FOSSA attribution shape."""
182+
from socketsecurity.fossa_compat import _build_dependency_entry
183+
package = Package(
184+
type="pypi",
185+
name="requests",
186+
version="2.31.0",
187+
id="pip+requests$2.31.0",
188+
score={},
189+
alerts=[],
190+
direct=True,
191+
author=["Kenneth Reitz <me@kennethreitz.com>"],
192+
license="Apache-2.0",
193+
licenseAttrib=[{"attribText": "Apache License 2.0\n\nCopyright 2023 Kenneth Reitz",
194+
"attribData": [{"spdxExpr": "Apache-2.0"}]}],
195+
)
196+
entry = _build_dependency_entry(package, dependency_paths=["requests"])
197+
assert set(entry.keys()) == {
198+
"authors", "dependencyPaths", "description", "downloadUrl", "hash",
199+
"isGolang", "licenses", "notes", "otherLicenses", "package",
200+
"projectUrl", "source", "title", "version",
203201
}
202+
assert entry["authors"] == ["Kenneth Reitz <me@kennethreitz.com>"]
203+
assert entry["dependencyPaths"] == ["requests"]
204+
assert entry["description"] == ""
205+
assert entry["downloadUrl"] == ""
206+
assert entry["hash"] is None
207+
assert entry["isGolang"] is None
208+
assert entry["licenses"] == [{
209+
"attribution": "Apache License 2.0\n\nCopyright 2023 Kenneth Reitz",
210+
"name": "Apache-2.0",
211+
}]
212+
assert entry["notes"] == []
213+
assert entry["otherLicenses"] == []
214+
assert entry["package"] == "requests"
215+
assert entry["projectUrl"] == ""
216+
assert entry["source"] == "pip"
217+
assert entry["title"] == "requests"
218+
assert entry["version"] == "2.31.0"
219+
220+
221+
def test_dependency_entry_falls_back_to_declared_license_when_no_attrib():
222+
"""When licenseAttrib is empty, `licenses[]` falls back to a single name-only entry from Package.license."""
223+
from socketsecurity.fossa_compat import _build_dependency_entry
224+
package = Package(
225+
type="pypi", name="x", version="1.0", id="pip+x$1.0",
226+
score={}, alerts=[], license="MIT",
227+
)
228+
entry = _build_dependency_entry(package, dependency_paths=["x"])
229+
assert entry["licenses"] == [{"attribution": "", "name": "MIT"}]
204230

205-
payload = build_fossa_attribution_payload(diff, config)
206231

207-
assert sorted(payload.keys()) == ["dependencies", "project"]
208-
assert sorted(payload["project"].keys()) == sorted(EXPECTED_PROJECT_KEYS)
209-
assert payload["dependencies"] == [{
210-
"id": "pkg-1",
211-
"name": "requests",
212-
"version": "2.31.0",
213-
"ecosystem": "pip",
214-
"direct": True,
215-
"url": "https://socket.dev/pypi/package/requests/overview/2.31.0",
216-
"purl": "pkg:pypi/requests@2.31.0",
217-
"declaredLicense": "Apache-2.0",
218-
"licenseDetails": [{"id": "Apache-2.0"}],
219-
"licenseAttrib": [{"id": "Apache-2.0"}],
220-
}]
232+
def test_dependency_entry_unlicensed_package_emits_empty_licenses():
233+
from socketsecurity.fossa_compat import _build_dependency_entry
234+
package = Package(
235+
type="pypi", name="x", version="1.0", id="pip+x$1.0",
236+
score={}, alerts=[], license=None,
237+
)
238+
entry = _build_dependency_entry(package, dependency_paths=["x"])
239+
assert entry["licenses"] == []
221240

222241

223242
def test_analyze_payload_top_level_keys_exactly_four():

0 commit comments

Comments
 (0)