Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 48 additions & 17 deletions socketsecurity/core/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,11 @@
DiffScanResult,
PackageComparisonResult,
create_diff_scan_result,
matching_full_scan_url,
normalize_report_url,
)
from socketsecurity.core.exceptions import APIResourceNotFound
from socketsecurity.core.report_urls import add_query_params

from .resource_utils import check_file_count_against_ulimit
from .socket_config import SocketConfig
Expand Down Expand Up @@ -1315,8 +1318,9 @@ def create_full_scan_with_report_url(
raise

# Construct report URL
base_socket = "https://socket.dev/dashboard/org"
diff.report_url = f"{base_socket}/{self.config.org_slug}/sbom/{new_full_scan.id}"
diff.old_scan_url = None
diff.new_scan_url = normalize_report_url(new_full_scan.html_report_url)
diff.report_url = diff.new_scan_url
diff.diff_url = diff.report_url
diff.set_scan_ids(new_full_scan.id)
diff.is_full_scan = True
Expand Down Expand Up @@ -2050,6 +2054,9 @@ def _get_diff_scan_result(
# them, so omit_unchanged and the bounded cached-polling contract cannot be
# bypassed by an eager response.
artifacts_dict = None
completed_diff_url = None
completed_old_scan_url = None
completed_new_scan_url = None

# cached=true is the polling contract (202 while computing, 200 when
# ready). The API ignores omit_license_details when cached=true - cached
Expand Down Expand Up @@ -2098,6 +2105,13 @@ def _get_diff_scan_result(
f"Error fetching diff scan {diff_scan_id}: unexpected response: {str(response)[:500]}"
)
artifacts_dict = scan["artifacts"]
completed_diff_url = normalize_report_url(scan.get("html_url"))
completed_old_scan_url = matching_full_scan_url(
scan.get("before_full_scan"), head_full_scan_id
)
completed_new_scan_url = matching_full_scan_url(
scan.get("after_full_scan"), new_full_scan_id
)
log.info(
"Diff scan comparison ready in "
f"{time.monotonic() - poll_start:.2f}s: id={diff_scan_id}, "
Expand All @@ -2115,7 +2129,13 @@ def _get_diff_scan_result(
last_interval = interval
interval = min(interval * DIFF_SCAN_POLL_BACKOFF_MULTIPLIER, DIFF_SCAN_POLL_MAX_INTERVAL_SECONDS)

return create_diff_scan_result(artifacts_dict, diff_scan_id)
return create_diff_scan_result(
artifacts_dict,
diff_scan_id,
diff_url=completed_diff_url,
old_scan_url=completed_old_scan_url,
new_scan_url=completed_new_scan_url,
)

def get_diff_scan_artifacts(
self,
Expand Down Expand Up @@ -2231,18 +2251,20 @@ def _get_package_comparison(

if comparison is None:
try:
diff_artifacts = (
self.sdk.fullscans.stream_diff(
stream_response = self.sdk.fullscans.stream_diff(
self.config.org_slug,
head_full_scan_id,
new_full_scan_id,
use_types=True,
include_license_details=str(include_license_details).lower()
).data.artifacts
)
)
diff_artifacts = stream_response.data.artifacts
comparison = DiffScanResult(
artifacts=diff_artifacts,
diff_scan_id=None,
diff_url=normalize_report_url(
getattr(stream_response.data, "diff_report_url", None)
),
)
except APIFailure as e:
log.error(f"API Error: {e}")
Expand Down Expand Up @@ -2327,6 +2349,9 @@ def _get_package_comparison(
removed_packages=removed_packages,
packages=packages,
diff_scan_id=comparison.diff_scan_id,
diff_url=comparison.diff_url,
old_scan_url=comparison.old_scan_url,
new_scan_url=comparison.new_scan_url,
)

def get_added_and_removed_packages(
Expand Down Expand Up @@ -2409,6 +2434,7 @@ def create_new_diff(
head_full_scan_id = self.resolve_base_full_scan_id(params)

# If no head scan exists, create an empty baseline scan
baseline_report_url = None
if head_full_scan_id is None:
log.info("No previous scan found - creating empty baseline scan")
new_params = copy.deepcopy(params.__dict__)
Expand All @@ -2424,6 +2450,7 @@ def create_new_diff(
try:
head_full_scan = self.create_full_scan(empty_files, tmp_params, base_paths=base_paths)
head_full_scan_id = head_full_scan.id
baseline_report_url = normalize_report_url(head_full_scan.html_report_url)
log.info(
"Baseline selected: source=empty "
f"scan_id={json.dumps(head_full_scan_id)}"
Expand Down Expand Up @@ -2513,22 +2540,26 @@ def create_new_diff(
diff = self.create_diff_report(added_packages, removed_packages, unchanged_packages)
diff.packages = packages

base_socket = "https://socket.dev/dashboard/org"
diff.set_scan_ids(
new_full_scan.id,
old_scan_id=head_full_scan_id,
diff_scan_id=comparison.diff_scan_id,
)

report_url = f"{base_socket}/{self.config.org_slug}/sbom/{diff.id}"
if not params.include_license_details:
report_url += "?include_license_details=false"
diff.report_url = report_url

if head_full_scan_id is not None:
diff.diff_url = f"{base_socket}/{self.config.org_slug}/diff/{head_full_scan_id}/{diff.id}"
else:
diff.diff_url = diff.report_url
old_scan_url = comparison.old_scan_url or baseline_report_url
new_scan_url = comparison.new_scan_url or normalize_report_url(
new_full_scan.html_report_url
)
diff.old_scan_url = old_scan_url
diff.new_scan_url = new_scan_url
diff.report_url = (
new_scan_url
if params.include_license_details
else add_query_params(
new_scan_url, {"include_license_details": "false"}
)
)
diff.diff_url = comparison.diff_url

# PR/MR runs previously logged only the scan IDs, so a CI log had no link to the
# result. Logged here rather than at each call site so every diff flow gets it.
Expand Down
2 changes: 2 additions & 0 deletions socketsecurity/core/alert_selection.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ def clone_diff_with_selected_alerts(diff: Diff, selected_alerts: List[Issue]) ->
selected_diff.new_scan_id = getattr(diff, "new_scan_id", None)
selected_diff.old_scan_id = getattr(diff, "old_scan_id", None)
selected_diff.diff_scan_id = getattr(diff, "diff_scan_id", None)
selected_diff.old_scan_url = getattr(diff, "old_scan_url", None)
selected_diff.new_scan_url = getattr(diff, "new_scan_url", None)
return selected_diff


Expand Down
10 changes: 8 additions & 2 deletions socketsecurity/core/classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -569,8 +569,10 @@ class Diff:
removed_alerts: list[Issue]
id: str
sbom: str
report_url: str
diff_url: str
report_url: Optional[str]
diff_url: Optional[str]
old_scan_url: Optional[str]
new_scan_url: Optional[str]
new_scan_id: Optional[str]
old_scan_id: Optional[str]
diff_scan_id: Optional[str]
Expand Down Expand Up @@ -607,6 +609,10 @@ def __init__(self, **kwargs):
self.old_scan_id = None
if not hasattr(self, "diff_scan_id"):
self.diff_scan_id = None
if not hasattr(self, "old_scan_url"):
self.old_scan_url = None
if not hasattr(self, "new_scan_url"):
self.new_scan_url = None

def set_scan_ids(
self,
Expand Down
28 changes: 27 additions & 1 deletion socketsecurity/core/diff_result.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@
class DiffScanResult:
artifacts: DiffArtifacts
diff_scan_id: Optional[str]
diff_url: Optional[str] = None
old_scan_url: Optional[str] = None
new_scan_url: Optional[str] = None


@dataclass
Expand All @@ -18,15 +21,38 @@ class PackageComparisonResult:
removed_packages: Dict[str, Package]
packages: Dict[str, Package]
diff_scan_id: Optional[str]
diff_url: Optional[str] = None
old_scan_url: Optional[str] = None
new_scan_url: Optional[str] = None


def normalize_report_url(value: object) -> Optional[str]:
if isinstance(value, str) and value:
return value
return None


def matching_full_scan_url(value: object, scan_id: str) -> Optional[str]:
if not isinstance(value, dict) or value.get("id") != scan_id:
return None
return normalize_report_url(value.get("html_url"))


def create_diff_scan_result(
artifacts: Dict[str, object], diff_scan_id: Optional[str]
artifacts: Dict[str, object],
diff_scan_id: Optional[str],
*,
diff_url: Optional[str] = None,
old_scan_url: Optional[str] = None,
new_scan_url: Optional[str] = None,
) -> DiffScanResult:
return DiffScanResult(
artifacts=DiffArtifacts.from_dict({
key: artifacts.get(key) or []
for key in ("added", "removed", "unchanged", "replaced", "updated")
}),
diff_scan_id=diff_scan_id,
diff_url=diff_url,
old_scan_url=old_scan_url,
new_scan_url=new_scan_url,
)
22 changes: 14 additions & 8 deletions socketsecurity/core/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from prettytable import PrettyTable

from socketsecurity.core.classes import Diff, Issue, Purl
from socketsecurity.core.report_urls import add_query_params

log = logging.getLogger("socketcli")

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

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

return ""

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

# Close table
report_link = (
f"\n\n[View full report]({add_query_params(view_report_url, {'action': 'error,warn'})})"
if view_report_url
else ""
)
comment += f"""
</tbody>
</table>
<!-- end-socket-alerts-table -->

[View full report]({view_report_url}?action=error%2Cwarn)
<!-- end-socket-alerts-table -->{report_link}
"""

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

@staticmethod
Expand Down
23 changes: 23 additions & 0 deletions socketsecurity/core/report_urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from typing import Mapping, Optional
from urllib.parse import parse_qsl, urlencode, urlsplit, urlunsplit


def add_query_params(
url: Optional[str], params: Mapping[str, str]
) -> Optional[str]:
if not url:
return None
try:
parts = urlsplit(url)
except ValueError:
return url
replaced_keys = set(params)
query = [
(key, value)
for key, value in parse_qsl(parts.query, keep_blank_values=True)
if key not in replaced_keys
]
query.extend(params.items())
return urlunsplit(
(parts.scheme, parts.netloc, parts.path, urlencode(query), parts.fragment)
)
35 changes: 21 additions & 14 deletions socketsecurity/plugins/jira.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import base64
from typing import Optional

import requests

from socketsecurity.config import CliConfig
from socketsecurity.core import log
from socketsecurity.core.classes import Diff
from socketsecurity.core.messages import Messages

from .base import Plugin

Expand All @@ -24,8 +26,6 @@ def send(self, diff: Diff, config: CliConfig):
parts = ["Security Issues found in Socket Security results"]
pr = getattr(config, "pr_number", "")
sha = getattr(config, "commit_sha", "")[:8] if getattr(config, "commit_sha", "") else ""
scan_link = getattr(diff, "diff_url", "")

if pr and pr != "0":
parts.append(f"for PR {pr}")
if sha:
Expand All @@ -36,18 +36,7 @@ def send(self, diff: Diff, config: CliConfig):
"type": "doc",
"version": 1,
"content": [
{
"type": "paragraph",
"content": [
{"type": "text", "text": "Security issues were found in this scan:"},
{"type": "text", "text": "\n"},
{
"type": "text",
"text": "View Socket Security scan results",
"marks": [{"type": "link", "attrs": {"href": scan_link}}]
}
]
},
self.create_adf_paragraph(diff),
self.create_adf_table_from_diff(diff)
]
}
Expand Down Expand Up @@ -81,6 +70,24 @@ def send(self, diff: Diff, config: CliConfig):
else:
log.info(f"Jira ticket created: {response.json().get('key')}")

@staticmethod
def create_adf_paragraph(diff: Diff) -> dict[str, object]:
return {
"type": "paragraph",
"content": [
{"type": "text", "text": "Security issues were found in this scan:"},
{"type": "text", "text": "\n"},
JiraPlugin._scan_link_adf(Messages.get_view_report_url(diff)),
],
}

@staticmethod
def _scan_link_adf(scan_link: Optional[str]) -> dict[str, object]:
text = {"type": "text", "text": "View Socket Security scan results"}
if scan_link:
text["marks"] = [{"type": "link", "attrs": {"href": scan_link}}]
return text

@staticmethod
def flatten_adf_to_text(adf):
def extract_text(node):
Expand Down
6 changes: 3 additions & 3 deletions socketsecurity/plugins/slack.py
Original file line number Diff line number Diff line change
Expand Up @@ -551,7 +551,7 @@ def _create_reachability_slack_blocks_from_structured(self, notification: dict,
"""
pr = getattr(config, "pr_number", None)
sha = getattr(config, "commit_sha", None)
diff_url = getattr(diff, "diff_url", "") if diff else ""
diff_url = Messages.get_view_report_url(diff) if diff else ""

title_part = ""
if pr:
Expand Down Expand Up @@ -690,8 +690,8 @@ def _normalize_url_config(self, url_input):
def create_slack_blocks_from_diff(diff: Diff, config: CliConfig):
pr = getattr(config, "pr_number", None)
sha = getattr(config, "commit_sha", None)
scan_link = getattr(diff, "diff_url", "")
scan = f"<{scan_link}|scan>"
scan_link = Messages.get_view_report_url(diff)
scan = f"<{scan_link}|scan>" if scan_link else "scan"
title_part = ""
if pr:
title_part += f" for PR {pr}"
Expand Down
Loading
Loading