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
8 changes: 8 additions & 0 deletions docs/cli-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,14 @@ 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.

Each selected alert in standard Socket JSON also includes `package_url` and
`info_url`. `package_url` preserves the existing alert `url` value, which is a
package overview link; `info_url` links to documentation for the alert's type.
Either field is JSON null when its source value is unavailable. The existing
`url` and `purl` fields retain their meanings and values. These per-alert links
are distinct from the scan-reference URLs above and do not identify an
individual alert or triage page.

Standard Socket JSON also includes four package-change arrays:
`new_packages`, `updated_packages`, `removed_packages`, and `replaced_packages`.
Rows contain `id`, `name`, `version`, `ecosystem`, `direct`, `author`, `size`,
Expand Down
35 changes: 29 additions & 6 deletions socketsecurity/core/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@
from datetime import datetime, timezone
from html import escape
from pathlib import Path
from typing import Optional

from mdutils import MdUtils
from prettytable import PrettyTable

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

log = logging.getLogger("socketcli")

Expand Down Expand Up @@ -945,6 +946,25 @@ def get_view_report_url(diff: Diff) -> str:
return diff.report_url
return ""

@staticmethod
def _format_alert_links(
package_url: Optional[str], alert_type: Optional[str]
) -> str:
links = build_alert_links(package_url, alert_type)
anchors: list[str] = []
if links.package_url:
anchors.append(
f'<a href="{Messages.html_attr(links.package_url)}">This package</a>'
)
if links.info_url:
anchors.append(
f'<a href="{Messages.html_attr(links.info_url)}">'
"About this alert type</a>"
)
if not anchors:
return ""
return f"<p>ℹ️ Read more on: {' | '.join(anchors)}</p>"

@staticmethod
def security_comment_template(diff: Diff, config=None) -> str:
"""
Expand Down Expand Up @@ -1011,6 +1031,9 @@ def security_comment_template(diff: Diff, config=None) -> str:
)
# Generate proper manifest URL
manifest_url = Messages.get_manifest_file_url(diff, alert.manifests, config)
alert_links = Messages._format_alert_links(
getattr(alert, "url", None), alert.type
)
pkg_label = Messages.html_text(f"{alert.pkg_name}@{alert.pkg_version}")
pkg_marker = Messages.comment_marker_text(f"{alert.pkg_name}@{alert.pkg_version}")
# Generate a table row for each alert
Expand All @@ -1033,10 +1056,7 @@ def security_comment_template(diff: Diff, config=None) -> str:
<p><strong>Note:</strong> {Messages.html_text(alert.description)}</p>
{patched_version_html}
<p><strong>Source:</strong> <a href="{Messages.html_attr(manifest_url)}">Manifest File</a></p>
<p>ℹ️ Read more on:
<a href="{Messages.html_attr(alert.purl)}">This package</a> |
<a href="{Messages.html_attr(alert.url)}">This alert</a> |
<a href="https://socket.dev/alerts/malware">What is known malware?</a></p>
{alert_links}
<blockquote>
<p><em>Suggestion:</em> {Messages.html_text(alert.suggestion)}</p>
{ignore_html}
Expand Down Expand Up @@ -1084,6 +1104,9 @@ def security_comment_template(diff: Diff, config=None) -> str:

# Generate proper manifest URL for license violations
license_manifest_url = Messages.get_manifest_file_url(diff, first_alert.manifests, config)
alert_links = Messages._format_alert_links(
getattr(first_alert, "url", None), first_alert.type
)

license_ignore_html = (
f"<p><em>Mark the package as acceptable risk:</em> To ignore this alert only in this pull request, reply with the comment "
Expand All @@ -1093,7 +1116,7 @@ def security_comment_template(diff: Diff, config=None) -> str:
) if show_ignore else ""
comment += f""" </ul>
<p><strong>From:</strong> <a href="{Messages.html_attr(license_manifest_url)}">Manifest File</a></p>
<p>ℹ️ Read more on: <a href="{Messages.html_attr(first_alert.purl)}">This package</a> | <a href="https://socket.dev/alerts/license">What is a license policy violation?</a></p>
{alert_links}
<blockquote>
<p><em>Next steps:</em> Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at <strong>support@socket.dev</strong>.</p>
<p><em>Suggestion:</em> Find a package that does not violate your license policy or adjust your policy to allow this package's license.</p>
Expand Down
10 changes: 10 additions & 0 deletions socketsecurity/core/report_output.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
)
from socketsecurity.core.classes import Diff, Purl
from socketsecurity.core.messages import Messages
from socketsecurity.core.report_urls import build_alert_links


@dataclass(frozen=True)
Expand Down Expand Up @@ -141,6 +142,15 @@ def build_socket_json_report(
)
selected_diff = clone_diff_with_selected_alerts(diff_report, selected_alerts)
report = Messages.create_security_comment_json(selected_diff)
for alert_row in report["new_alerts"]:
package_url = alert_row.get("url")
alert_type = alert_row.get("type")
links = build_alert_links(
package_url if isinstance(package_url, str) else None,
alert_type if isinstance(alert_type, str) else None,
)
alert_row["package_url"] = links.package_url
alert_row["info_url"] = links.info_url
legal_flag = getattr(config, "legal", False)
repo = getattr(config, "repo", None)
branch = getattr(config, "branch", None)
Expand Down
22 changes: 21 additions & 1 deletion socketsecurity/core/report_urls.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,25 @@
from dataclasses import dataclass
from typing import Mapping, Optional
from urllib.parse import parse_qsl, urlencode, urlsplit, urlunsplit
from urllib.parse import parse_qsl, quote, urlencode, urlsplit, urlunsplit


@dataclass(frozen=True)
class AlertLinks:
package_url: Optional[str]
info_url: Optional[str]


def build_alert_links(
package_url: Optional[str], alert_type: Optional[str]
) -> AlertLinks:
return AlertLinks(
package_url=package_url or None,
info_url=(
f"https://socket.dev/alerts/{quote(alert_type, safe='')}"
if alert_type
else None
),
)


def add_query_params(
Expand Down
16 changes: 16 additions & 0 deletions tests/core/test_json_package_changes_transport.py
Original file line number Diff line number Diff line change
Expand Up @@ -487,12 +487,28 @@ def test_transitive_alert_and_package_json_keep_ancestor_namespaces(
assert alert_diff.new_alerts[0].introduced_by == [
tuple(row) for row in expected_sources
]
issue = alert_diff.new_alerts[0]
expected_package_url = "https://socket.dev/pypi/package/leaf/overview/2.0.0"
assert issue.url == expected_package_url
_, source_text = Messages.create_sources(alert_diff.new_alerts[0], "raw")
assert source_text == "npm/@one/widget@1.0.0;npm/@two/widget@1.0.0"
comment = Messages.security_comment_template(alert_diff)
assert f'<a href="{expected_package_url}">This package</a>' in comment
assert (
'<a href="https://socket.dev/alerts/envVars">About this alert type</a>'
in comment
)
assert f'href="{issue.purl}"' not in comment
alert_report = build_socket_json_report(
alert_diff, CliConfig(api_token="fictional-local-only", repo="fixture-repo")
)
assert alert_report["new_alerts"][0]["introduced_by"] == expected_sources
assert alert_report["new_alerts"][0]["url"] == issue.url
assert alert_report["new_alerts"][0]["purl"] == issue.purl
assert alert_report["new_alerts"][0]["package_url"] == expected_package_url
assert alert_report["new_alerts"][0]["info_url"] == (
"https://socket.dev/alerts/envVars"
)
assert alert_report["scan_failed"] is False

full_diff = core.create_diff_report(
Expand Down
82 changes: 82 additions & 0 deletions tests/unit/test_report_output.py
Original file line number Diff line number Diff line change
Expand Up @@ -671,3 +671,85 @@ def test_full_scan_package_inventory_does_not_become_change_lists():
assert report["updated_packages"] == []
assert report["removed_packages"] == []
assert report["replaced_packages"] == []


def test_alert_json_links_are_additive_and_do_not_mutate_real_models():
issue = _issue("envVars", warn=True)
issue.url = "https://packages.example.test/@scope/pkg?view=a&mode=b#source"
issue.purl = "pkg:npm/@scope/pkg@1.2.3"
issue.props = {"nested": {"value": ["kept"]}}
diff = _diff(new_alerts=[issue])
diff_before = copy.deepcopy(diff.__dict__)
issue_before = copy.deepcopy(issue.__dict__)
original_alert = json.loads(str(issue))

report = build_socket_json_report(diff, _config())
serialized = json.loads(json.dumps(report))
alert_row = serialized["new_alerts"][0]

assert alert_row == {
**original_alert,
"package_url": "https://packages.example.test/@scope/pkg?view=a&mode=b#source",
"info_url": "https://socket.dev/alerts/envVars",
}
assert alert_row["url"] == original_alert["url"]
assert alert_row["purl"] == "pkg:npm/@scope/pkg@1.2.3"
assert set(report) == {
"scan_failed",
"new_alerts",
"full_scan_id",
"diff_url",
"report_url",
"diff_scan_id",
"old_scan_id",
"new_scan_id",
"old_scan_url",
"new_scan_url",
"repo",
"branch",
"commit_sha",
"legal_mode",
"new_packages",
"updated_packages",
"removed_packages",
"replaced_packages",
}
assert len(report) == 18
assert diff.__dict__ == diff_before
assert issue.__dict__ == issue_before
assert "package_url" not in issue.__dict__
assert "info_url" not in issue.__dict__


def test_alert_json_links_follow_existing_selection_and_emit_nulls():
new_alerts = [_issue("envVars", warn=True), _issue("highCVE", warn=True)]
unchanged = _issue("unchangedType", warn=True)
del new_alerts[1].url
unchanged.type = ""
unchanged.url = ""
diff = _diff(new_alerts=new_alerts, unchanged_alerts=[unchanged])

ordinary = json.loads(json.dumps(build_socket_json_report(diff, _config())))
strict = json.loads(
json.dumps(build_socket_json_report(diff, _config(strict_blocking=True)))
)

assert [row["type"] for row in ordinary["new_alerts"]] == ["envVars", "highCVE"]
assert [row["type"] for row in strict["new_alerts"]] == [
"envVars",
"highCVE",
"",
]
assert ordinary["new_alerts"][0]["info_url"] == (
"https://socket.dev/alerts/envVars"
)
assert "url" not in ordinary["new_alerts"][1]
assert ordinary["new_alerts"][1]["package_url"] is None
assert ordinary["new_alerts"][1]["info_url"] == (
"https://socket.dev/alerts/highCVE"
)
assert strict["new_alerts"][2]["package_url"] is None
assert strict["new_alerts"][2]["info_url"] is None
assert ordinary["scan_failed"] is False
assert strict["scan_failed"] is False
assert build_socket_json_report(_diff(), _config())["new_alerts"] == []
Loading
Loading