Skip to content

Commit 8f9e402

Browse files
leliaclaude
andcommitted
fix(scans): keep the package list on full scans
create_full_scan_with_report_url only fetched SBOM data when an alert-bearing output format was enabled, so --generate-license and --legal-format fossa saw an empty diff.packages and wrote an attribution file with zero packages. That is the list they enumerate, as _requires_unchanged_artifacts already documents for the comparison path. Fetch the SBOM for them too, and enrich it through the PURL endpoint the way the comparison path does. The full scan's package map is keyed by artifact id while get_license_text_via_purl keys off ecosystem/name@version, so pass a purl-keyed view over the same Package objects. Alert consolidation stays behind its own gate, so an alert-only run does not pay for the license lookup and a license-only run does not build an alert list. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 95483f3 commit 8f9e402

2 files changed

Lines changed: 133 additions & 16 deletions

File tree

‎socketsecurity/core/__init__.py‎

Lines changed: 46 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1288,30 +1288,42 @@ def create_full_scan_with_report_url(
12881288
or self.cli_config.enable_sarif
12891289
)
12901290
)
1291+
# --generate-license (and --legal-format fossa, which it gates) enumerates
1292+
# diff.packages rather than the alert list, so a full scan has to carry the
1293+
# package map even when no alert-bearing output format is enabled. Without
1294+
# this, an SCM branch pipeline writes an attribution file with zero packages.
1295+
# Keep in sync with _requires_unchanged_artifacts, which lists the same
1296+
# consumers for the comparison path.
1297+
needs_license_artifacts = (
1298+
self.cli_config is not None and self.cli_config.generate_license
1299+
)
12911300

1292-
if needs_alerts:
1293-
log.info("Output format requires alerts, fetching SBOM data for full scan")
1301+
if needs_alerts or needs_license_artifacts:
1302+
log.info("Output format requires SBOM data, fetching it for the full scan")
12941303
sbom_start = time.time()
12951304
sbom_artifacts_dict = self.get_sbom_data(new_full_scan.id)
12961305
sbom_artifacts = self.get_sbom_data_list(sbom_artifacts_dict)
12971306
packages = self._create_packages_dict_without_license_text(sbom_artifacts)
1307+
if needs_license_artifacts:
1308+
packages = self._add_license_details(packages)
12981309
diff.packages = packages
12991310

1300-
all_alerts_collection: Dict[str, List[Issue]] = {}
1301-
for package_id, package in packages.items():
1302-
self.add_package_alerts_to_collection(
1303-
package=package,
1304-
alerts_collection=all_alerts_collection,
1305-
packages=packages
1306-
)
1311+
if needs_alerts:
1312+
all_alerts_collection: Dict[str, List[Issue]] = {}
1313+
for package_id, package in packages.items():
1314+
self.add_package_alerts_to_collection(
1315+
package=package,
1316+
alerts_collection=all_alerts_collection,
1317+
packages=packages
1318+
)
13071319

1308-
consolidated: Set[str] = set()
1309-
for alert_key, alerts in all_alerts_collection.items():
1310-
for alert in alerts:
1311-
alert_str = f"{alert.purl},{alert.type}"
1312-
if (alert.error or alert.warn) and alert_str not in consolidated:
1313-
diff.new_alerts.append(alert)
1314-
consolidated.add(alert_str)
1320+
consolidated: Set[str] = set()
1321+
for alert_key, alerts in all_alerts_collection.items():
1322+
for alert in alerts:
1323+
alert_str = f"{alert.purl},{alert.type}"
1324+
if (alert.error or alert.warn) and alert_str not in consolidated:
1325+
diff.new_alerts.append(alert)
1326+
consolidated.add(alert_str)
13151327

13161328
sbom_end = time.time()
13171329
log.info(
@@ -1323,6 +1335,24 @@ def create_full_scan_with_report_url(
13231335

13241336
return diff
13251337

1338+
def _add_license_details(self, packages: dict[str, Package]) -> dict[str, Package]:
1339+
"""Populate licenseAttrib/licenseDetails on a full scan's package map.
1340+
1341+
get_license_text_via_purl keys off ``ecosystem/name@version`` because that is
1342+
what the PURL endpoint echoes back, while a full scan's package map is keyed
1343+
by artifact id. Build a purl-keyed view over the same Package objects so the
1344+
enrichment lands on the map the caller keeps.
1345+
"""
1346+
batch_size = self.cli_config.max_purl_batch_size if self.cli_config else 5000
1347+
self.get_license_text_via_purl(
1348+
{
1349+
f"{package.type}/{package.name}@{package.version}": package
1350+
for package in packages.values()
1351+
},
1352+
batch_size=batch_size,
1353+
)
1354+
return packages
1355+
13261356
def get_full_scan(self, full_scan_id: str) -> FullScan:
13271357
"""
13281358
Get a FullScan object for an existing full scan including sbom_artifacts and packages.
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
"""What a full scan has to carry for each enabled output.
2+
3+
create_full_scan_with_report_url runs on every path with no baseline to compare
4+
against: API mode, and (since 2.8.0) SCM branch pipelines. Fetching the SBOM is
5+
the expensive part, so it is gated on the enabled outputs -- these pin which
6+
outputs need it.
7+
"""
8+
import pytest
9+
from socketdev.fullscans import FullScanParams
10+
11+
from socketsecurity.config import CliConfig
12+
from socketsecurity.core import Core
13+
from socketsecurity.core.socket_config import SocketConfig
14+
15+
16+
def _core(sdk, **cli_overrides):
17+
config = CliConfig.from_args(["--api-token", "test"])
18+
for key, value in cli_overrides.items():
19+
setattr(config, key, value)
20+
return Core(config=SocketConfig(api_key="test_key"), sdk=sdk, cli_config=config)
21+
22+
23+
@pytest.fixture
24+
def params():
25+
return FullScanParams(org_slug="test-org", repo="test", branch="main")
26+
27+
28+
@pytest.fixture
29+
def sdk(mock_sdk_with_responses):
30+
# get_license_text_via_purl iterates the response; the shared fixture leaves
31+
# purl.post as a bare MagicMock.
32+
mock_sdk_with_responses.purl.post.return_value = []
33+
return mock_sdk_with_responses
34+
35+
36+
def test_license_generation_gets_the_package_list(sdk, params):
37+
"""--generate-license enumerates diff.packages, not diff.new_alerts.
38+
39+
Without this the attribution file for an SCM branch pipeline comes out empty.
40+
"""
41+
core = _core(sdk, generate_license=True)
42+
43+
diff = core.create_full_scan_with_report_url(
44+
["."], params, explicit_files=["package.json"]
45+
)
46+
47+
assert diff.packages
48+
# No alert-bearing output format is enabled, so alerts stay unfetched.
49+
assert diff.new_alerts == []
50+
51+
52+
def test_license_details_are_requested_for_the_scanned_packages(sdk, params):
53+
core = _core(sdk, generate_license=True)
54+
55+
core.create_full_scan_with_report_url(
56+
["."], params, explicit_files=["package.json"]
57+
)
58+
59+
components = sdk.purl.post.call_args.kwargs["components"]
60+
# Keyed the way the PURL endpoint echoes results back, not by artifact id.
61+
assert all(component["purl"].startswith("pkg:/") for component in components)
62+
assert any("@" in component["purl"] for component in components)
63+
64+
65+
def test_alert_formats_still_fetch_the_sbom(sdk, params):
66+
core = _core(sdk, enable_json=True)
67+
68+
diff = core.create_full_scan_with_report_url(
69+
["."], params, explicit_files=["package.json"]
70+
)
71+
72+
assert diff.packages
73+
# Alert-only outputs do not pay for the license lookup. (The scan fixture's
74+
# alerts carry no action, so none of them consolidate into new_alerts.)
75+
sdk.purl.post.assert_not_called()
76+
77+
78+
def test_console_only_run_skips_the_sbom_fetch(sdk, params):
79+
core = _core(sdk)
80+
81+
diff = core.create_full_scan_with_report_url(
82+
["."], params, explicit_files=["package.json"]
83+
)
84+
85+
assert diff.packages == {}
86+
assert diff.new_alerts == []
87+
sdk.fullscans.stream.assert_not_called()

0 commit comments

Comments
 (0)