From e5e4dfbcdf13c6a869976e55f1e0e4af4440781c Mon Sep 17 00:00:00 2001 From: Douglas Coburn Date: Fri, 18 Apr 2025 08:15:45 -0700 Subject: [PATCH 1/2] Changed Dependency Overview to new template --- pyproject.toml | 2 +- socketsecurity/__init__.py | 2 +- socketsecurity/core/__init__.py | 18 ++++++---- socketsecurity/core/classes.py | 2 +- socketsecurity/core/messages.py | 63 ++++++++++++++++++++++----------- 5 files changed, 57 insertions(+), 30 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index d500e71..f6ba24a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" [project] name = "socketsecurity" -version = "2.0.52" +version = "2.0.53" requires-python = ">= 3.10" license = {"file" = "LICENSE"} dependencies = [ diff --git a/socketsecurity/__init__.py b/socketsecurity/__init__.py index 61801ac..097cd85 100644 --- a/socketsecurity/__init__.py +++ b/socketsecurity/__init__.py @@ -1,2 +1,2 @@ __author__ = 'socket.dev' -__version__ = '2.0.52' +__version__ = '2.0.53' diff --git a/socketsecurity/core/__init__.py b/socketsecurity/core/__init__.py index a5925a6..007af25 100644 --- a/socketsecurity/core/__init__.py +++ b/socketsecurity/core/__init__.py @@ -21,7 +21,7 @@ FullScan, Issue, Package, - Purl, + Purl ) from socketsecurity.core.exceptions import APIResourceNotFound from socketsecurity.core.licenses import Licenses @@ -644,7 +644,7 @@ def create_diff_report( seen_removed_packages = set() for package_id, package in added_packages.items(): - purl = Core.create_purl(package_id, added_packages) + purl = self.create_purl(package_id, added_packages) base_purl = f"{purl.ecosystem}/{purl.name}@{purl.version}" if (not direct_only or package.direct) and base_purl not in seen_new_packages: @@ -658,7 +658,7 @@ def create_diff_report( ) for package_id, package in removed_packages.items(): - purl = Core.create_purl(package_id, removed_packages) + purl = self.create_purl(package_id, removed_packages) base_purl = f"{purl.ecosystem}/{purl.name}@{purl.version}" if (not direct_only or package.direct) and base_purl not in seen_removed_packages: @@ -682,8 +682,13 @@ def create_diff_report( return diff - @staticmethod - def create_purl(package_id: str, packages: dict[str, Package]) -> Purl: + def get_all_scores(self, packages: dict[str, Package]) -> dict[str, Package]: + components = [] + for package_id in packages: + package = packages[package_id] + return packages + + def create_purl(self, package_id: str, packages: dict[str, Package]) -> Purl: """ Creates the extended PURL data for package identification and tracking. @@ -707,7 +712,8 @@ def create_purl(package_id: str, packages: dict[str, Package]) -> Purl: size=package.size, transitives=package.transitives, url=package.url, - purl=package.purl + purl=package.purl, + scores=package.score ) return purl diff --git a/socketsecurity/core/classes.py b/socketsecurity/core/classes.py index 416cd06..aefb0ab 100644 --- a/socketsecurity/core/classes.py +++ b/socketsecurity/core/classes.py @@ -370,7 +370,6 @@ def __init__(self, **kwargs): def __str__(self): return json.dumps(self.__dict__) - class Purl: """ Represents a Package URL (PURL) with extended metadata. @@ -392,6 +391,7 @@ class Purl: author_url: str url: str purl: str + scores: dict[str, int] def __init__(self, **kwargs): if kwargs: diff --git a/socketsecurity/core/messages.py b/socketsecurity/core/messages.py index db4c85f..a7a6e14 100644 --- a/socketsecurity/core/messages.py +++ b/socketsecurity/core/messages.py @@ -456,11 +456,9 @@ def dependency_overview_template(diff: Diff) -> str: md = MdUtils(file_name="markdown_overview_temp.md") md.new_line("") md.new_header(level=1, title="Socket Security: Dependency Overview") - md.new_line("New and removed dependencies detected. Learn more about [socket.dev](https://socket.dev)") + md.new_line("Review the following changes in direct dependencies. Learn more about [socket.dev](https://socket.dev)") md.new_line() md = Messages.create_added_table(diff, md) - if len(diff.removed_packages) > 0: - md = Messages.create_remove_line(diff, md) md.create_md_file() if len(md.file_data_text.lstrip()) >= 65500: md = Messages.short_dependency_overview_comment(diff) @@ -471,7 +469,7 @@ def short_dependency_overview_comment(diff: Diff) -> MdUtils: md = MdUtils(file_name="markdown_overview_temp.md") md.new_line("") md.new_header(level=1, title="Socket Security: Dependency Overview") - md.new_line("New and removed dependencies detected. Learn more about [socket.dev](https://socket.dev)") + 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})") @@ -498,40 +496,63 @@ def create_remove_line(diff: Diff, md: MdUtils) -> MdUtils: def create_added_table(diff: Diff, md: MdUtils) -> MdUtils: """ Create the Added packages table for the Dependency Overview template - :param diff: Diff - Diff report with the Added packages information + :param diff: Diff - Diff report with the Added package information :param md: MdUtils - Main markdown variable :return: """ + # Table column headers overview_table = [ + "Diff", "Package", - "Direct", - "Capabilities", - "Transitives", - "Size", - "Author" + "Supply Chain
Security", + "Vulnerability", + "Quality", + "Maintenance", + "License" ] num_of_overview_columns = len(overview_table) + count = 0 for added in diff.new_packages: - added: Purl - package_url = Messages.create_purl_link(added) - capabilities = ", ".join(added.capabilities) + added: Purl # Ensure `added` has scores and relevant attributes. + + package_url = f"[{added.purl}]({added.url})" + diff_badge = f"[![+](https://github-app-statics.socket.dev/diff-added.svg)]({added.url})" + + # Scores dynamically converted to badge URLs and linked + def score_to_badge(score): + score_percent = int(score * 100) # Convert to integer percentage + return f"[![{score_percent}](https://github-app-statics.socket.dev/score-{score_percent}.svg)]({added.url})" + + # Generate badges for each score type + supply_chain_risk_badge = score_to_badge(added.scores.get("supplyChain", 100)) + vulnerability_badge = score_to_badge(added.scores.get("vulnerability", 100)) + quality_badge = score_to_badge(added.scores.get("quality", 100)) + maintenance_badge = score_to_badge(added.scores.get("maintenance", 100)) + license_badge = score_to_badge(added.scores.get("license", 100)) + + # Add the row for this package row = [ + diff_badge, package_url, - added.direct, - capabilities, - added.transitives, - f"{added.size} KB", - added.author_url + supply_chain_risk_badge, + vulnerability_badge, + quality_badge, + maintenance_badge, + license_badge ] overview_table.extend(row) - count += 1 - num_of_overview_rows = count + 1 + count += 1 # Count total packages + + # Calculate total rows for table + num_of_overview_rows = count + 1 # Include header row + + # Generate Markdown table md.new_table( columns=num_of_overview_columns, rows=num_of_overview_rows, text=overview_table, - text_align="left" + text_align="center" ) return md From d35da29889ff0dc7138d2e209cc09028393d400a Mon Sep 17 00:00:00 2001 From: Douglas Coburn Date: Mon, 21 Apr 2025 07:31:38 -0700 Subject: [PATCH 2/2] Updated the Security Issue comment to the new template --- pyproject.toml | 2 +- socketsecurity/__init__.py | 2 +- socketsecurity/core/messages.py | 107 ++++++++++++++++++++++----- socketsecurity/core/scm_comments.py | 110 +++++++++++++++++++++++++++- 4 files changed, 196 insertions(+), 25 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index f6ba24a..1799b19 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" [project] name = "socketsecurity" -version = "2.0.53" +version = "2.0.54" requires-python = ">= 3.10" license = {"file" = "LICENSE"} dependencies = [ diff --git a/socketsecurity/__init__.py b/socketsecurity/__init__.py index 097cd85..92958d3 100644 --- a/socketsecurity/__init__.py +++ b/socketsecurity/__init__.py @@ -1,2 +1,2 @@ __author__ = 'socket.dev' -__version__ = '2.0.53' +__version__ = '2.0.54' diff --git a/socketsecurity/core/messages.py b/socketsecurity/core/messages.py index a7a6e14..b86b37f 100644 --- a/socketsecurity/core/messages.py +++ b/socketsecurity/core/messages.py @@ -302,26 +302,95 @@ def create_security_comment_json(diff: Diff) -> dict: @staticmethod def security_comment_template(diff: Diff) -> str: """ - Creates the security comment template - :param diff: Diff - Diff report with the data needed for the template - :return: + Generates the security comment template in the new required format. + Dynamically determines placement of the alerts table if markers like `` are used. + + :param diff: Diff - Contains the detected vulnerabilities and warnings. + :return: str - The formatted Markdown/HTML string. """ - md = MdUtils(file_name="markdown_security_temp.md") - md.new_line("") - md.new_header(level=1, title="Socket Security: Issues Report") - md.new_line("Potential security issues detected. Learn more about [socket.dev](https://socket.dev)") - md.new_line("To accept the risk, merge this PR and you will not be notified again.") - md.new_line() - md.new_line("") - md, ignore_commands, next_steps = Messages.create_security_alert_table(diff, md) - md.new_line("") - md.new_line() - md = Messages.create_next_steps(md, next_steps) - md = Messages.create_deeper_look(md) - md = Messages.create_remove_package(md) - md = Messages.create_acceptable_risk(md, ignore_commands) - md.create_md_file() - return md.file_data_text.lstrip() + # Start of the comment + comment = """ + +> **❗️ Caution** +> **Review the following alerts detected in dependencies.** +> +> According to your organization’s Security Policy, you **must** resolve all **“Block”** alerts before proceeding. It’s recommended to resolve **“Warn”** alerts too. +> Learn more about [Socket for GitHub](https://socket.dev?utm_medium=gh). + + + + + + + + + + + + """ + + # Loop through alerts, dynamically generating rows + for alert in diff.new_alerts: + severity_icon = Messages.get_severity_icon(alert.severity) + action = "Block" if alert.error else "Warn" + details_open = "" + # Generate a table row for each alert + comment += f""" + + + + + + + + """ + + # Close table and comment + comment += """ + +
ActionSeverityAlert (click for details)
{action} + {alert.severity} + +
+ {alert.pkg_name}@{alert.pkg_version} - {alert.title} +

Note: {alert.description}

+

Source: Manifest File

+

ℹ️ Read more on: + This package | + This alert | + What is known malware?

+
+

Suggestion: {alert.suggestion}

+

Mark as acceptable risk: To ignore this alert only in this pull request, reply with:
+ @SocketSecurity ignore {alert.pkg_name}@{alert.pkg_version}
+ Or ignore all future alerts with:
+ @SocketSecurity ignore-all

+
+
+
+ + +[View full report](https://socket.dev/...&action=error%2Cwarn) + """ + + return comment + + @staticmethod + def get_severity_icon(severity: str) -> str: + """ + Maps severity levels to their corresponding badge/icon URLs. + + :param severity: str - Severity level (e.g., "Critical", "High"). + :return: str - Badge/icon URL. + """ + severity_map = { + "critical": "https://github-app-statics.socket.dev/severity-3.svg", + "high": "https://github-app-statics.socket.dev/severity-2.svg", + "medium": "https://github-app-statics.socket.dev/severity-1.svg", + "low": "https://github-app-statics.socket.dev/severity-0.svg", + } + return severity_map.get(severity.lower(), "https://github-app-statics.socket.dev/severity-0.svg") + @staticmethod def create_next_steps(md: MdUtils, next_steps: dict): diff --git a/socketsecurity/core/scm_comments.py b/socketsecurity/core/scm_comments.py index 7bcb203..bd1b4d7 100644 --- a/socketsecurity/core/scm_comments.py +++ b/socketsecurity/core/scm_comments.py @@ -84,9 +84,22 @@ def is_heading_line(line) -> bool: @staticmethod def process_security_comment(comment: Comment, comments) -> str: - lines = [] - start = False ignore_all, ignore_commands = Comments.get_ignore_options(comments) + if "start-socket-alerts-table" in "".join(comment.body_list): + new_body = Comments.process_original_security_comment(comment, ignore_all, ignore_commands) + else: + new_body = Comments.process_updated_security_comment(comment, ignore_all, ignore_commands) + + return new_body + + @staticmethod + def process_original_security_comment( + comment: Comment, + ignore_all: bool, + ignore_commands: list[tuple[str, str]] + ) -> str: + start = False + lines = [] for line in comment.body_list: line = line.strip() if "start-socket-alerts-table" in line: @@ -110,8 +123,97 @@ def process_security_comment(comment: Comment, comments) -> str: lines.append(line) else: lines.append(line) - new_body = "\n".join(lines) - return new_body + return "\n".join(lines) + + @staticmethod + def process_updated_security_comment( + comment: Comment, + ignore_all: bool, + ignore_commands: list[tuple[str, str]] + ) -> str: + """ + Processes an updated security comment containing an HTML table with alert sections. + Removes entire sections marked by start and end hidden comments if the alert matches + ignore conditions. + + :param comment: Comment - The raw comment object containing the existing information. + :param ignore_all: bool - Flag to ignore all alerts. + :param ignore_commands: list of tuples - Specific ignore commands representing (pkg_name, pkg_version). + :return: str - The updated comment as a single string. + """ + lines = [] + ignore_section = False + pkg_name = pkg_version = "" # Track current package and version + + # Loop through the comment lines + for line in comment.body_list: + line = line.strip() + + # Detect the start of an alert section + if line.startswith("