Skip to content

Commit b8b6d48

Browse files
committed
Use server-provided scan report links
1 parent ef00228 commit b8b6d48

13 files changed

Lines changed: 505 additions & 73 deletions

‎socketsecurity/core/__init__.py‎

Lines changed: 48 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,11 @@
3131
DiffScanResult,
3232
PackageComparisonResult,
3333
create_diff_scan_result,
34+
matching_full_scan_url,
35+
normalize_report_url,
3436
)
3537
from socketsecurity.core.exceptions import APIResourceNotFound
38+
from socketsecurity.core.report_urls import add_query_params
3639

3740
from .resource_utils import check_file_count_against_ulimit
3841
from .socket_config import SocketConfig
@@ -1315,8 +1318,9 @@ def create_full_scan_with_report_url(
13151318
raise
13161319

13171320
# Construct report URL
1318-
base_socket = "https://socket.dev/dashboard/org"
1319-
diff.report_url = f"{base_socket}/{self.config.org_slug}/sbom/{new_full_scan.id}"
1321+
diff.old_scan_url = None
1322+
diff.new_scan_url = normalize_report_url(new_full_scan.html_report_url)
1323+
diff.report_url = diff.new_scan_url
13201324
diff.diff_url = diff.report_url
13211325
diff.set_scan_ids(new_full_scan.id)
13221326
diff.is_full_scan = True
@@ -2050,6 +2054,9 @@ def _get_diff_scan_result(
20502054
# them, so omit_unchanged and the bounded cached-polling contract cannot be
20512055
# bypassed by an eager response.
20522056
artifacts_dict = None
2057+
completed_diff_url = None
2058+
completed_old_scan_url = None
2059+
completed_new_scan_url = None
20532060

20542061
# cached=true is the polling contract (202 while computing, 200 when
20552062
# ready). The API ignores omit_license_details when cached=true - cached
@@ -2098,6 +2105,13 @@ def _get_diff_scan_result(
20982105
f"Error fetching diff scan {diff_scan_id}: unexpected response: {str(response)[:500]}"
20992106
)
21002107
artifacts_dict = scan["artifacts"]
2108+
completed_diff_url = normalize_report_url(scan.get("html_url"))
2109+
completed_old_scan_url = matching_full_scan_url(
2110+
scan.get("before_full_scan"), head_full_scan_id
2111+
)
2112+
completed_new_scan_url = matching_full_scan_url(
2113+
scan.get("after_full_scan"), new_full_scan_id
2114+
)
21012115
log.info(
21022116
"Diff scan comparison ready in "
21032117
f"{time.monotonic() - poll_start:.2f}s: id={diff_scan_id}, "
@@ -2115,7 +2129,13 @@ def _get_diff_scan_result(
21152129
last_interval = interval
21162130
interval = min(interval * DIFF_SCAN_POLL_BACKOFF_MULTIPLIER, DIFF_SCAN_POLL_MAX_INTERVAL_SECONDS)
21172131

2118-
return create_diff_scan_result(artifacts_dict, diff_scan_id)
2132+
return create_diff_scan_result(
2133+
artifacts_dict,
2134+
diff_scan_id,
2135+
diff_url=completed_diff_url,
2136+
old_scan_url=completed_old_scan_url,
2137+
new_scan_url=completed_new_scan_url,
2138+
)
21192139

21202140
def get_diff_scan_artifacts(
21212141
self,
@@ -2231,18 +2251,20 @@ def _get_package_comparison(
22312251

22322252
if comparison is None:
22332253
try:
2234-
diff_artifacts = (
2235-
self.sdk.fullscans.stream_diff(
2254+
stream_response = self.sdk.fullscans.stream_diff(
22362255
self.config.org_slug,
22372256
head_full_scan_id,
22382257
new_full_scan_id,
22392258
use_types=True,
22402259
include_license_details=str(include_license_details).lower()
2241-
).data.artifacts
2242-
)
2260+
)
2261+
diff_artifacts = stream_response.data.artifacts
22432262
comparison = DiffScanResult(
22442263
artifacts=diff_artifacts,
22452264
diff_scan_id=None,
2265+
diff_url=normalize_report_url(
2266+
getattr(stream_response.data, "diff_report_url", None)
2267+
),
22462268
)
22472269
except APIFailure as e:
22482270
log.error(f"API Error: {e}")
@@ -2327,6 +2349,9 @@ def _get_package_comparison(
23272349
removed_packages=removed_packages,
23282350
packages=packages,
23292351
diff_scan_id=comparison.diff_scan_id,
2352+
diff_url=comparison.diff_url,
2353+
old_scan_url=comparison.old_scan_url,
2354+
new_scan_url=comparison.new_scan_url,
23302355
)
23312356

23322357
def get_added_and_removed_packages(
@@ -2409,6 +2434,7 @@ def create_new_diff(
24092434
head_full_scan_id = self.resolve_base_full_scan_id(params)
24102435

24112436
# If no head scan exists, create an empty baseline scan
2437+
baseline_report_url = None
24122438
if head_full_scan_id is None:
24132439
log.info("No previous scan found - creating empty baseline scan")
24142440
new_params = copy.deepcopy(params.__dict__)
@@ -2424,6 +2450,7 @@ def create_new_diff(
24242450
try:
24252451
head_full_scan = self.create_full_scan(empty_files, tmp_params, base_paths=base_paths)
24262452
head_full_scan_id = head_full_scan.id
2453+
baseline_report_url = normalize_report_url(head_full_scan.html_report_url)
24272454
log.info(
24282455
"Baseline selected: source=empty "
24292456
f"scan_id={json.dumps(head_full_scan_id)}"
@@ -2513,22 +2540,26 @@ def create_new_diff(
25132540
diff = self.create_diff_report(added_packages, removed_packages, unchanged_packages)
25142541
diff.packages = packages
25152542

2516-
base_socket = "https://socket.dev/dashboard/org"
25172543
diff.set_scan_ids(
25182544
new_full_scan.id,
25192545
old_scan_id=head_full_scan_id,
25202546
diff_scan_id=comparison.diff_scan_id,
25212547
)
25222548

2523-
report_url = f"{base_socket}/{self.config.org_slug}/sbom/{diff.id}"
2524-
if not params.include_license_details:
2525-
report_url += "?include_license_details=false"
2526-
diff.report_url = report_url
2527-
2528-
if head_full_scan_id is not None:
2529-
diff.diff_url = f"{base_socket}/{self.config.org_slug}/diff/{head_full_scan_id}/{diff.id}"
2530-
else:
2531-
diff.diff_url = diff.report_url
2549+
old_scan_url = comparison.old_scan_url or baseline_report_url
2550+
new_scan_url = comparison.new_scan_url or normalize_report_url(
2551+
new_full_scan.html_report_url
2552+
)
2553+
diff.old_scan_url = old_scan_url
2554+
diff.new_scan_url = new_scan_url
2555+
diff.report_url = (
2556+
new_scan_url
2557+
if params.include_license_details
2558+
else add_query_params(
2559+
new_scan_url, {"include_license_details": "false"}
2560+
)
2561+
)
2562+
diff.diff_url = comparison.diff_url
25322563

25332564
# PR/MR runs previously logged only the scan IDs, so a CI log had no link to the
25342565
# result. Logged here rather than at each call site so every diff flow gets it.

‎socketsecurity/core/alert_selection.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ def clone_diff_with_selected_alerts(diff: Diff, selected_alerts: List[Issue]) ->
4141
selected_diff.new_scan_id = getattr(diff, "new_scan_id", None)
4242
selected_diff.old_scan_id = getattr(diff, "old_scan_id", None)
4343
selected_diff.diff_scan_id = getattr(diff, "diff_scan_id", None)
44+
selected_diff.old_scan_url = getattr(diff, "old_scan_url", None)
45+
selected_diff.new_scan_url = getattr(diff, "new_scan_url", None)
4446
return selected_diff
4547

4648

‎socketsecurity/core/classes.py‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -569,8 +569,10 @@ class Diff:
569569
removed_alerts: list[Issue]
570570
id: str
571571
sbom: str
572-
report_url: str
573-
diff_url: str
572+
report_url: Optional[str]
573+
diff_url: Optional[str]
574+
old_scan_url: Optional[str]
575+
new_scan_url: Optional[str]
574576
new_scan_id: Optional[str]
575577
old_scan_id: Optional[str]
576578
diff_scan_id: Optional[str]
@@ -607,6 +609,10 @@ def __init__(self, **kwargs):
607609
self.old_scan_id = None
608610
if not hasattr(self, "diff_scan_id"):
609611
self.diff_scan_id = None
612+
if not hasattr(self, "old_scan_url"):
613+
self.old_scan_url = None
614+
if not hasattr(self, "new_scan_url"):
615+
self.new_scan_url = None
610616

611617
def set_scan_ids(
612618
self,

‎socketsecurity/core/diff_result.py‎

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
1010
class DiffScanResult:
1111
artifacts: DiffArtifacts
1212
diff_scan_id: Optional[str]
13+
diff_url: Optional[str] = None
14+
old_scan_url: Optional[str] = None
15+
new_scan_url: Optional[str] = None
1316

1417

1518
@dataclass
@@ -18,15 +21,38 @@ class PackageComparisonResult:
1821
removed_packages: Dict[str, Package]
1922
packages: Dict[str, Package]
2023
diff_scan_id: Optional[str]
24+
diff_url: Optional[str] = None
25+
old_scan_url: Optional[str] = None
26+
new_scan_url: Optional[str] = None
27+
28+
29+
def normalize_report_url(value: object) -> Optional[str]:
30+
if isinstance(value, str) and value:
31+
return value
32+
return None
33+
34+
35+
def matching_full_scan_url(value: object, scan_id: str) -> Optional[str]:
36+
if not isinstance(value, dict) or value.get("id") != scan_id:
37+
return None
38+
return normalize_report_url(value.get("html_url"))
2139

2240

2341
def create_diff_scan_result(
24-
artifacts: Dict[str, object], diff_scan_id: Optional[str]
42+
artifacts: Dict[str, object],
43+
diff_scan_id: Optional[str],
44+
*,
45+
diff_url: Optional[str] = None,
46+
old_scan_url: Optional[str] = None,
47+
new_scan_url: Optional[str] = None,
2548
) -> DiffScanResult:
2649
return DiffScanResult(
2750
artifacts=DiffArtifacts.from_dict({
2851
key: artifacts.get(key) or []
2952
for key in ("added", "removed", "unchanged", "replaced", "updated")
3053
}),
3154
diff_scan_id=diff_scan_id,
55+
diff_url=diff_url,
56+
old_scan_url=old_scan_url,
57+
new_scan_url=new_scan_url,
3258
)

‎socketsecurity/core/messages.py‎

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from prettytable import PrettyTable
1212

1313
from socketsecurity.core.classes import Diff, Issue, Purl
14+
from socketsecurity.core.report_urls import add_query_params
1415

1516
log = logging.getLogger("socketcli")
1617

@@ -120,10 +121,10 @@ def get_manifest_file_url(diff: Diff, manifest_path: str, config=None) -> str:
120121

121122
# Fallback to Socket file view for API or unknown repository types
122123
if hasattr(diff, 'report_url') and diff.report_url:
123-
# Strip leading slash and URL encode for Socket dashboard
124-
socket_path = clean_path.lstrip('/')
125-
encoded_path = socket_path.replace('/', '%2F')
126-
return f"{diff.report_url}?tab=files&file={encoded_path}"
124+
return add_query_params(
125+
diff.report_url,
126+
{"tab": "files", "file": clean_path.lstrip('/')},
127+
) or ""
127128

128129
return ""
129130

@@ -1105,12 +1106,15 @@ def security_comment_template(diff: Diff, config=None) -> str:
11051106
"""
11061107

11071108
# Close table
1109+
report_link = (
1110+
f"\n\n[View full report]({add_query_params(view_report_url, {'action': 'error,warn'})})"
1111+
if view_report_url
1112+
else ""
1113+
)
11081114
comment += f"""
11091115
</tbody>
11101116
</table>
1111-
<!-- end-socket-alerts-table -->
1112-
1113-
[View full report]({view_report_url}?action=error%2Cwarn)
1117+
<!-- end-socket-alerts-table -->{report_link}
11141118
"""
11151119

11161120
return Messages.normalize_comment_html(comment)
@@ -1281,7 +1285,9 @@ def short_dependency_overview_comment(diff: Diff) -> MdUtils:
12811285
md.new_line("Review the following changes in direct dependencies. Learn more about [socket.dev](https://socket.dev)")
12821286
md.new_line()
12831287
md.new_line("The amount of dependency changes were to long for this comment. Please check out the full report")
1284-
md.new_line(f"To view more information about this report checkout the [Full Report]({diff.diff_url})")
1288+
view_report_url = Messages.get_view_report_url(diff)
1289+
if view_report_url:
1290+
md.new_line(f"To view more information about this report checkout the [Full Report]({view_report_url})")
12851291
return md
12861292

12871293
@staticmethod

‎socketsecurity/core/report_urls.py‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
from typing import Mapping, Optional
2+
from urllib.parse import parse_qsl, urlencode, urlsplit, urlunsplit
3+
4+
5+
def add_query_params(
6+
url: Optional[str], params: Mapping[str, str]
7+
) -> Optional[str]:
8+
if not url:
9+
return None
10+
try:
11+
parts = urlsplit(url)
12+
except ValueError:
13+
return url
14+
replaced_keys = set(params)
15+
query = [
16+
(key, value)
17+
for key, value in parse_qsl(parts.query, keep_blank_values=True)
18+
if key not in replaced_keys
19+
]
20+
query.extend(params.items())
21+
return urlunsplit(
22+
(parts.scheme, parts.netloc, parts.path, urlencode(query), parts.fragment)
23+
)

‎socketsecurity/plugins/jira.py‎

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import base64
2+
from typing import Optional
23

34
import requests
45

56
from socketsecurity.config import CliConfig
67
from socketsecurity.core import log
78
from socketsecurity.core.classes import Diff
9+
from socketsecurity.core.messages import Messages
810

911
from .base import Plugin
1012

@@ -24,8 +26,6 @@ def send(self, diff: Diff, config: CliConfig):
2426
parts = ["Security Issues found in Socket Security results"]
2527
pr = getattr(config, "pr_number", "")
2628
sha = getattr(config, "commit_sha", "")[:8] if getattr(config, "commit_sha", "") else ""
27-
scan_link = getattr(diff, "diff_url", "")
28-
2929
if pr and pr != "0":
3030
parts.append(f"for PR {pr}")
3131
if sha:
@@ -36,18 +36,7 @@ def send(self, diff: Diff, config: CliConfig):
3636
"type": "doc",
3737
"version": 1,
3838
"content": [
39-
{
40-
"type": "paragraph",
41-
"content": [
42-
{"type": "text", "text": "Security issues were found in this scan:"},
43-
{"type": "text", "text": "\n"},
44-
{
45-
"type": "text",
46-
"text": "View Socket Security scan results",
47-
"marks": [{"type": "link", "attrs": {"href": scan_link}}]
48-
}
49-
]
50-
},
39+
self.create_adf_paragraph(diff),
5140
self.create_adf_table_from_diff(diff)
5241
]
5342
}
@@ -81,6 +70,24 @@ def send(self, diff: Diff, config: CliConfig):
8170
else:
8271
log.info(f"Jira ticket created: {response.json().get('key')}")
8372

73+
@staticmethod
74+
def create_adf_paragraph(diff: Diff) -> dict[str, object]:
75+
return {
76+
"type": "paragraph",
77+
"content": [
78+
{"type": "text", "text": "Security issues were found in this scan:"},
79+
{"type": "text", "text": "\n"},
80+
JiraPlugin._scan_link_adf(Messages.get_view_report_url(diff)),
81+
],
82+
}
83+
84+
@staticmethod
85+
def _scan_link_adf(scan_link: Optional[str]) -> dict[str, object]:
86+
text = {"type": "text", "text": "View Socket Security scan results"}
87+
if scan_link:
88+
text["marks"] = [{"type": "link", "attrs": {"href": scan_link}}]
89+
return text
90+
8491
@staticmethod
8592
def flatten_adf_to_text(adf):
8693
def extract_text(node):

‎socketsecurity/plugins/slack.py‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -551,7 +551,7 @@ def _create_reachability_slack_blocks_from_structured(self, notification: dict,
551551
"""
552552
pr = getattr(config, "pr_number", None)
553553
sha = getattr(config, "commit_sha", None)
554-
diff_url = getattr(diff, "diff_url", "") if diff else ""
554+
diff_url = Messages.get_view_report_url(diff) if diff else ""
555555

556556
title_part = ""
557557
if pr:
@@ -690,8 +690,8 @@ def _normalize_url_config(self, url_input):
690690
def create_slack_blocks_from_diff(diff: Diff, config: CliConfig):
691691
pr = getattr(config, "pr_number", None)
692692
sha = getattr(config, "commit_sha", None)
693-
scan_link = getattr(diff, "diff_url", "")
694-
scan = f"<{scan_link}|scan>"
693+
scan_link = Messages.get_view_report_url(diff)
694+
scan = f"<{scan_link}|scan>" if scan_link else "scan"
695695
title_part = ""
696696
if pr:
697697
title_part += f" for PR {pr}"

0 commit comments

Comments
 (0)