From 8193af466bfd6dcd9568c81a532b5b870125751e Mon Sep 17 00:00:00 2001 From: jdalton Date: Tue, 13 Oct 2026 13:11:09 -0400 Subject: [PATCH] Expose scan references in JSON reports Include comparison and full-scan identities and canonical report links for JSON consumers. Preserve existing fields and null values. Assisted-by: Codex:GPT-6 --- docs/cli-reference.md | 13 ++++ socketsecurity/core/report_output.py | 15 ++++ tests/core/test_diff_identity_transport.py | 39 ++++++++++ tests/unit/test_report_output.py | 84 +++++++++++++++++++++- 4 files changed, 150 insertions(+), 1 deletion(-) diff --git a/docs/cli-reference.md b/docs/cli-reference.md index 861ea64..1249b0d 100644 --- a/docs/cli-reference.md +++ b/docs/cli-reference.md @@ -328,6 +328,19 @@ If you don't want to provide the Socket API Token every time then you can use th | `--exclude-license-details` | False | False | Exclude license details from the diff report (boosts performance for large repos) | | `--version` | False | False | Show program's version number and exit | +#### Standard Socket JSON scan references + +Standard Socket JSON output adds `diff_scan_id`, `old_scan_id`, `new_scan_id`, +`old_scan_url`, and `new_scan_url`. These fields are JSON null when the +corresponding reference is unavailable, including fallback comparisons, +full scans, and no-run results. `full_scan_id` retains its legacy meaning: the +new full-scan ID (or the existing no-run sentinel), not the comparison ID. + +`new_scan_url` is the canonical server URL for the new full scan. It can differ +from `report_url`, which is the display link and may include output options such +as license-detail settings. These additions apply to standard Socket JSON, not +FOSSA compatibility output. + #### Security Configuration | Parameter | Required | Default | Description | |:-------------------------|:---------|:--------|:------------------------------| diff --git a/socketsecurity/core/report_output.py b/socketsecurity/core/report_output.py index 5095865..90fdd1d 100644 --- a/socketsecurity/core/report_output.py +++ b/socketsecurity/core/report_output.py @@ -16,6 +16,11 @@ class ReportReferences: report_url: Optional[str] diff_url: Optional[str] is_full_scan: bool + diff_scan_id: Optional[str] = None + old_scan_id: Optional[str] = None + new_scan_id: Optional[str] = None + old_scan_url: Optional[str] = None + new_scan_url: Optional[str] = None def console_link(self) -> Optional[tuple[str, str]]: if self.is_full_scan: @@ -37,6 +42,11 @@ def build_report_references(diff_report: Diff) -> ReportReferences: report_url=getattr(diff_report, "report_url", None), diff_url=getattr(diff_report, "diff_url", ""), is_full_scan=getattr(diff_report, "is_full_scan", False), + diff_scan_id=getattr(diff_report, "diff_scan_id", None), + old_scan_id=getattr(diff_report, "old_scan_id", None), + new_scan_id=getattr(diff_report, "new_scan_id", None), + old_scan_url=getattr(diff_report, "old_scan_url", None), + new_scan_url=getattr(diff_report, "new_scan_url", None), ) @@ -131,6 +141,11 @@ def build_socket_json_report( report["full_scan_id"] = references.full_scan_id report["diff_url"] = references.diff_url report["report_url"] = references.report_url + report["diff_scan_id"] = references.diff_scan_id + report["old_scan_id"] = references.old_scan_id + report["new_scan_id"] = references.new_scan_id + report["old_scan_url"] = references.old_scan_url + report["new_scan_url"] = references.new_scan_url report["repo"] = repo if isinstance(repo, str) or repo is None else None report["branch"] = branch if isinstance(branch, str) or branch is None else None report["commit_sha"] = ( diff --git a/tests/core/test_diff_identity_transport.py b/tests/core/test_diff_identity_transport.py index 27a34ea..12c8b8d 100644 --- a/tests/core/test_diff_identity_transport.py +++ b/tests/core/test_diff_identity_transport.py @@ -11,6 +11,7 @@ from socketsecurity.config import CliConfig from socketsecurity.core import Core +from socketsecurity.core.report_output import build_socket_json_report from socketsecurity.core.socket_config import SocketConfig from tests.support.socket_api_fixture import ( SocketApiFixture, @@ -426,6 +427,24 @@ def create_full_scan(_request: SocketRequest) -> SocketResponse: ) assert diff.new_scan_url == expected_new_scan_url assert diff.report_url == expected_report_url + report = build_socket_json_report( + diff, + CliConfig( + api_token="fictional-local-only", + repo="fixture-repo", + legal=False, + ), + ) + assert report["full_scan_id"] == "new-full-scan-id" + assert report["diff_scan_id"] == ( + "new-comparison-id" if comparison_succeeds else None + ) + assert report["old_scan_id"] == "temporary-baseline-id" + assert report["new_scan_id"] == "new-full-scan-id" + assert report["diff_url"] == diff.diff_url + assert report["old_scan_url"] == diff.old_scan_url + assert report["new_scan_url"] == expected_new_scan_url + assert report["report_url"] == expected_report_url if comparison_succeeds and not null_urls: assert diff.diff_url == "https://reports.example.invalid/diff?canonical=1#comparison" assert diff.old_scan_url == "https://reports.example.invalid/before?source=get" @@ -539,6 +558,17 @@ def test_full_scan_identity_is_preserved_with_findings_disabled_or_enabled( assert result.new_scan_url == "https://socket.dev/scan/full-scan-id" assert result.old_scan_url is None assert result.report_url == result.diff_url == result.new_scan_url + report = build_socket_json_report( + result, + cli_config + or CliConfig(api_token="fictional-local-only", repo="fixture-repo"), + ) + assert report["full_scan_id"] == "full-scan-id" + assert report["diff_scan_id"] is None + assert report["old_scan_id"] is None + assert report["new_scan_id"] == "full-scan-id" + assert report["old_scan_url"] is None + assert report["new_scan_url"] == "https://socket.dev/scan/full-scan-id" if enable_json: assert result.packages["artifact-fixture-id"].purl == "npm/sample-package@1.0.0" findings_requests = [ @@ -567,4 +597,13 @@ def test_no_change_paths_keep_sentinels_without_transport_calls( assert result.diff_scan_id is None assert result.old_scan_url is None assert result.new_scan_url is None + report = build_socket_json_report( + result, CliConfig(api_token="fictional-local-only", repo="fixture-repo") + ) + assert report["diff_scan_id"] is None + assert report["old_scan_id"] is None + assert report["new_scan_id"] is None + assert report["old_scan_url"] is None + assert report["new_scan_url"] is None + assert report["full_scan_id"] == result.id assert len(socket_api.requests) == request_count_after_setup diff --git a/tests/unit/test_report_output.py b/tests/unit/test_report_output.py index 6e3db22..d50277a 100644 --- a/tests/unit/test_report_output.py +++ b/tests/unit/test_report_output.py @@ -58,14 +58,23 @@ def _diff( diff_url: str | None = "https://socket.dev/orgs/fictional/diffs/diff-456", is_full_scan: bool = False, alerts_fetched: bool = True, + diff_scan_id: str | None = None, + old_scan_id: str | None = None, + new_scan_id: str | None = "full-123", + old_scan_url: str | None = None, + new_scan_url: str | None = None, ) -> Diff: return Diff( id="full-123", - new_scan_id="full-123", report_url=report_url, diff_url=diff_url, is_full_scan=is_full_scan, alerts_fetched=alerts_fetched, + diff_scan_id=diff_scan_id, + old_scan_id=old_scan_id, + new_scan_id=new_scan_id, + old_scan_url=old_scan_url, + new_scan_url=new_scan_url, new_alerts=list(new_alerts or []), unchanged_alerts=list(unchanged_alerts or []), ) @@ -245,6 +254,11 @@ def test_default_socket_json_key_set_and_metadata_remain_unchanged(): "branch", "commit_sha", "legal_mode", + "diff_scan_id", + "old_scan_id", + "new_scan_id", + "old_scan_url", + "new_scan_url", } assert report == { "scan_failed": False, @@ -256,9 +270,77 @@ def test_default_socket_json_key_set_and_metadata_remain_unchanged(): "branch": "feature/example", "commit_sha": "abc123", "legal_mode": True, + "diff_scan_id": None, + "old_scan_id": None, + "new_scan_id": "full-123", + "old_scan_url": None, + "new_scan_url": None, } +def test_scan_references_are_distinct_nullable_and_preserve_input(): + diff = _diff( + diff_scan_id="comparison-789", + old_scan_id="baseline-456", + new_scan_id="full-123", + old_scan_url="https://reports.example.test/before?x=1#base", + new_scan_url="https://reports.example.test/after?x=2#new", + ) + original = copy.deepcopy(diff.__dict__) + + report = build_socket_json_report(diff, _config()) + + assert report == { + "scan_failed": False, + "new_alerts": [], + "full_scan_id": "full-123", + "diff_url": "https://socket.dev/orgs/fictional/diffs/diff-456", + "report_url": "https://socket.dev/orgs/fictional/reports/full-123", + "repo": "fictional-repo", + "branch": "", + "commit_sha": "", + "legal_mode": False, + "diff_scan_id": "comparison-789", + "old_scan_id": "baseline-456", + "new_scan_id": "full-123", + "old_scan_url": "https://reports.example.test/before?x=1#base", + "new_scan_url": "https://reports.example.test/after?x=2#new", + } + assert json.loads(json.dumps(report))["old_scan_id"] == "baseline-456" + assert diff.__dict__ == original + + +def test_missing_references_remain_null_without_url_inference(): + diff = _diff( + report_url="https://reports.example.test/display", + diff_url=None, + diff_scan_id=None, + old_scan_id=None, + new_scan_id=None, + old_scan_url=None, + new_scan_url=None, + ) + + report = build_socket_json_report(diff, _config()) + + assert report["diff_scan_id"] is None + assert report["new_scan_id"] is None + assert report["new_scan_url"] is None + assert report["diff_url"] is None + assert report["report_url"] == "https://reports.example.test/display" + + +def test_canonical_scan_url_is_distinct_from_display_report_url(): + canonical_url = "https://reports.example.test/scan?id=full-123#result" + display_url = "https://reports.example.test/scan?id=full-123&include_license_details=false#result" + diff = _diff(report_url=display_url, new_scan_url=canonical_url) + + report = build_socket_json_report(diff, _config()) + + assert report["new_scan_url"] == canonical_url + assert report["report_url"] == display_url + + def test_missing_reference_fields_keep_existing_json_defaults(): diff = Diff(new_alerts=[])