Skip to content

Commit cd2f471

Browse files
committed
fix(purl): preserve error stream records
1 parent 931303c commit cd2f471

2 files changed

Lines changed: 40 additions & 5 deletions

File tree

socketdev/purl/__init__.py

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -96,17 +96,25 @@ def post(
9696
path += params
9797
response = self.api.do_request(path=path, payload=purls, method="POST")
9898
if response.status_code == 200:
99-
purl = []
99+
artifact_rows = []
100+
stream_records = []
100101
result = response.text
101102
result = result.strip('"').strip()
102103
for line in result.split("\n"):
103104
if line and line != '"':
104105
try:
105106
item = json.loads(line)
106-
purl.append(item)
107+
if isinstance(item, dict) and item.get("_type") in {
108+
"purlError",
109+
"summary",
110+
}:
111+
stream_records.append(item)
112+
else:
113+
artifact_rows.append(item)
107114
except json.JSONDecodeError:
108115
continue
109-
purl_deduped = Dedupe.dedupe(purl, batched=True)
116+
purl_deduped = Dedupe.dedupe(artifact_rows, batched=True)
117+
purl_deduped.extend(stream_records)
110118
if strict:
111119
self._raise_on_missing(components, purl_deduped)
112120
return purl_deduped
@@ -120,8 +128,8 @@ def _raise_on_missing(components: list, results: list) -> None:
120128
"""Raise APIPartialResponse if any requested component purl is absent from results.
121129
122130
Only components exposing a ``purl`` string are checked; the batch API echoes the
123-
request identifier back as ``inputPurl`` (falling back to ``purl``), so we compare
124-
against both.
131+
request identifier back as ``inputPurl`` (falling back to ``purl``), including
132+
under ``value`` for typed ``purlError`` stream records.
125133
"""
126134
requested = [
127135
c["purl"]
@@ -138,6 +146,11 @@ def _raise_on_missing(components: list, results: list) -> None:
138146
value = row.get(field)
139147
if isinstance(value, str):
140148
returned.add(value)
149+
record_value = row.get("value")
150+
if isinstance(record_value, dict):
151+
input_purl = record_value.get("inputPurl")
152+
if isinstance(input_purl, str):
153+
returned.add(input_purl)
141154
missing = [purl for purl in requested if purl not in returned]
142155
if missing:
143156
raise APIPartialResponse(

tests/unit/test_all_endpoints_unit.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -471,6 +471,28 @@ def test_purl_post_synthetic_pending_scan_row(self):
471471
self.assertIsNone(alert["severity"])
472472
self.assertIsNone(alert["action"])
473473

474+
def test_purl_post_preserves_purl_error_record(self):
475+
"""purlError stream records bypass artifact deduplication."""
476+
error_row = {
477+
"_type": "purlError",
478+
"value": {
479+
"error": "package_not_found",
480+
"inputPurl": "pkg:npm/missing@1.0.0",
481+
},
482+
}
483+
self._mock_purl_ndjson(json.dumps(error_row))
484+
485+
result = self.sdk.purl.post(
486+
components=[{"purl": "pkg:npm/missing@1.0.0"}],
487+
org_slug="test-org",
488+
purl_errors=True,
489+
strict=True,
490+
)
491+
492+
self.assertEqual(result, [error_row])
493+
url = self.mock_requests.request.call_args[0][1]
494+
self.assertIn("purlErrors=true", url)
495+
474496
def test_purl_post_strict_raises_on_missing(self):
475497
"""strict=True raises APIPartialResponse listing purls dropped from the response."""
476498
from socketdev.exceptions import APIPartialResponse

0 commit comments

Comments
 (0)