diff --git a/CHANGELOG.md b/CHANGELOG.md index 080ed11..1666695 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,28 @@ # Changelog +## 2.6.8 + +### Fixed: pull request comments no longer show orphaned tags or an empty table + +- Optional sections that rendered as empty, such as the ignore instructions + suppressed by `--disable-ignore`, left a whitespace-only line in the alerts + table. That line closed the surrounding HTML block, and the indented + `` tags after it were rendered as a literal code block. + Generated comment markup now omits blank lines and stays under the indentation + that starts a code block. +- Alert descriptions, suggestions and license findings are collapsed onto a + single line so multi-line API text cannot break the table markup either. +- When a pull request has no alerts left to report, the security comment is + replaced with a short confirmation instead of keeping the "Caution" banner + above a table with no rows. This happens both when a later commit resolves + every alert and when every alert is ignored by comment. The comment marker is + preserved, so a commit that reintroduces an alert updates the same comment + rather than posting a second one. +- `@SocketSecurity ignore-all` now applies to comments written by CLI versions + before 2.0.55, which use the older Markdown alerts table. The check was made + once per ignore command, and an ignore-all comment produces none, so no rows + were removed. + ## 2.6.7 ### Changed: bump pinned @coana-tech/cli to 15.10.23 diff --git a/pyproject.toml b/pyproject.toml index e2716b3..f206c59 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" [project] name = "socketsecurity" -version = "2.6.7" +version = "2.6.8" requires-python = ">= 3.11" license = {"file" = "LICENSE"} dependencies = [ diff --git a/socketsecurity/__init__.py b/socketsecurity/__init__.py index 6af51dd..cb041be 100644 --- a/socketsecurity/__init__.py +++ b/socketsecurity/__init__.py @@ -1,3 +1,3 @@ __author__ = 'socket.dev' -__version__ = '2.6.7' +__version__ = '2.6.8' USER_AGENT = f'SocketPythonCLI/{__version__}' diff --git a/socketsecurity/core/messages.py b/socketsecurity/core/messages.py index 319e454..673dde5 100644 --- a/socketsecurity/core/messages.py +++ b/socketsecurity/core/messages.py @@ -806,6 +806,90 @@ def create_security_comment_gitlab(diff: Diff) -> dict: return gitlab_report + # A blank line terminates a CommonMark HTML block. When that happens inside + # the alerts table the closing tags that follow are no longer treated as + # markup, and because they are indented four or more spaces they render as a + # literal code block containing `` instead. + MAX_HTML_INDENT = 3 + + @staticmethod + def inline_html_text(value) -> str: + """ + Collapses API supplied text onto a single line. + + Alert descriptions and suggestions are interpolated into the comment HTML, + so an embedded newline would otherwise be able to close the surrounding + HTML block early. + + :param value: The value to flatten. ``None`` becomes an empty string. + :return: str - The value with all whitespace runs collapsed to a single space. + """ + if value is None: + return "" + return " ".join(str(value).split()) + + @staticmethod + def normalize_comment_html(comment: str) -> str: + """ + Makes generated comment markup safe for the CommonMark renderers used by + GitHub and GitLab. + + Drops whitespace-only lines (an optional section that rendered as empty + leaves one behind) and caps indentation below the four spaces that would + start an indented code block. Intentional separators - lines that are + genuinely empty - are preserved so markdown blocks still break apart. + + :param comment: str - The generated comment body. + :return: str - The comment body with unrenderable whitespace removed. + """ + lines = [] + for line in comment.split("\n"): + if line and not line.strip(): + continue + stripped = line.lstrip() + indent = min(len(line) - len(stripped), Messages.MAX_HTML_INDENT) + lines.append(" " * indent + stripped) + return "\n".join(lines) + + @staticmethod + def security_comment_no_alerts_template(view_report_url: str = "") -> str: + """ + Generates the body used when there is nothing left to report. + + Alerts raised on an early commit are frequently resolved later in the same + pull request. Rewriting the comment to this body keeps the Socket comment + in place - so a later commit that reintroduces an alert updates it rather + than posting a second comment - without leaving the "Caution" banner above + an empty alerts table. + + :param view_report_url: str - Optional link to the full Socket report. + :return: str - The formatted Markdown/HTML string. + """ + lines = [ + "", + "", + "> **✅ Socket Security** ", + "> No dependency alerts to report. Any alerts previously reported on this " + "pull request have been resolved or ignored.", + ] + if view_report_url: + lines += ["", f"[View full report]({view_report_url})"] + return "\n".join(lines) + "\n" + + @staticmethod + def get_view_report_url(diff: Diff) -> str: + """ + Resolves the report link for a diff, preferring the PR/MR diff view. + + :param diff: Diff - Diff report to pull the URL from. + :return: str - The report URL, or an empty string when neither is set. + """ + if getattr(diff, "diff_url", None): + return diff.diff_url + if getattr(diff, "report_url", None): + return diff.report_url + return "" + @staticmethod def security_comment_template(diff: Diff, config=None) -> str: """ @@ -819,7 +903,7 @@ def security_comment_template(diff: Diff, config=None) -> str: # Group license policy violations by PURL (ecosystem/package@version) license_groups = {} security_alerts = [] - + for alert in diff.new_alerts: if alert.type == "licenseSpdxDisj": purl_key = f"{alert.pkg_type}/{alert.pkg_name}@{alert.pkg_version}" @@ -829,6 +913,13 @@ def security_comment_template(diff: Diff, config=None) -> str: else: security_alerts.append(alert) + view_report_url = Messages.get_view_report_url(diff) + + # Without this the caution banner would sit above a table with no rows, + # which is how a comment looks once every alert it raised is resolved. + if not security_alerts and not license_groups: + return Messages.security_comment_no_alerts_template(view_report_url) + # Start of the comment comment = """ @@ -875,15 +966,15 @@ def security_comment_template(diff: Diff, config=None) -> str:
- {alert.pkg_name}@{alert.pkg_version} - {alert.title} -

Note: {alert.description}

+ {alert.pkg_name}@{alert.pkg_version} - {Messages.inline_html_text(alert.title)} +

Note: {Messages.inline_html_text(alert.description)}

Source: Manifest File

â„šī¸ Read more on: This package | This alert | What is known malware?

-

Suggestion: {alert.suggestion}

+

Suggestion: {Messages.inline_html_text(alert.suggestion)}

{ignore_html}
@@ -917,7 +1008,7 @@ def security_comment_template(diff: Diff, config=None) -> str: