Skip to content

Commit c4f5c80

Browse files
committed
Fix orphaned tags and empty tables in PR comments
A whitespace-only line closes a CommonMark HTML block. Optional sections that rendered as empty left one behind inside the alerts table, so the indented closing tags after it were rendered as a literal code block reading `</blockquote></details>` instead of markup. - Drop blank lines from generated comment markup and keep indentation below the four spaces that start a code block. - Collapse alert descriptions, suggestions and license findings onto a single line so multi-line API text cannot break the table either. - Replace the comment body with a short confirmation when no alerts are left to report, instead of keeping the caution banner above a table with no rows. The comment marker is preserved so the same comment is updated later. - Apply ignore-all to the pre-2.0.55 Markdown table format. The check was made once per ignore command and an ignore-all comment produces none, so no rows were removed. Bumps to 2.6.8.
1 parent 3b4f879 commit c4f5c80

7 files changed

Lines changed: 475 additions & 25 deletions

File tree

CHANGELOG.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,28 @@
11
# Changelog
22

3+
## 2.6.8
4+
5+
### Fixed: pull request comments no longer show orphaned tags or an empty table
6+
7+
- Optional sections that rendered as empty, such as the ignore instructions
8+
suppressed by `--disable-ignore`, left a whitespace-only line in the alerts
9+
table. That line closed the surrounding HTML block, and the indented
10+
`</blockquote></details>` tags after it were rendered as a literal code block.
11+
Generated comment markup now omits blank lines and stays under the indentation
12+
that starts a code block.
13+
- Alert descriptions, suggestions and license findings are collapsed onto a
14+
single line so multi-line API text cannot break the table markup either.
15+
- When a pull request has no alerts left to report, the security comment is
16+
replaced with a short confirmation instead of keeping the "Caution" banner
17+
above a table with no rows. This happens both when a later commit resolves
18+
every alert and when every alert is ignored by comment. The comment marker is
19+
preserved, so a commit that reintroduces an alert updates the same comment
20+
rather than posting a second one.
21+
- `@SocketSecurity ignore-all` now applies to comments written by CLI versions
22+
before 2.0.55, which use the older Markdown alerts table. The check was made
23+
once per ignore command, and an ignore-all comment produces none, so no rows
24+
were removed.
25+
326
## 2.6.7
427

528
### Changed: bump pinned @coana-tech/cli to 15.10.23

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ build-backend = "hatchling.build"
66

77
[project]
88
name = "socketsecurity"
9-
version = "2.6.7"
9+
version = "2.6.8"
1010
requires-python = ">= 3.11"
1111
license = {"file" = "LICENSE"}
1212
dependencies = [

socketsecurity/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
__author__ = 'socket.dev'
2-
__version__ = '2.6.7'
2+
__version__ = '2.6.8'
33
USER_AGENT = f'SocketPythonCLI/{__version__}'

socketsecurity/core/messages.py

Lines changed: 97 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -806,6 +806,90 @@ def create_security_comment_gitlab(diff: Diff) -> dict:
806806

807807
return gitlab_report
808808

809+
# A blank line terminates a CommonMark HTML block. When that happens inside
810+
# the alerts table the closing tags that follow are no longer treated as
811+
# markup, and because they are indented four or more spaces they render as a
812+
# literal code block containing `</blockquote></details>` instead.
813+
MAX_HTML_INDENT = 3
814+
815+
@staticmethod
816+
def inline_html_text(value) -> str:
817+
"""
818+
Collapses API supplied text onto a single line.
819+
820+
Alert descriptions and suggestions are interpolated into the comment HTML,
821+
so an embedded newline would otherwise be able to close the surrounding
822+
HTML block early.
823+
824+
:param value: The value to flatten. ``None`` becomes an empty string.
825+
:return: str - The value with all whitespace runs collapsed to a single space.
826+
"""
827+
if value is None:
828+
return ""
829+
return " ".join(str(value).split())
830+
831+
@staticmethod
832+
def normalize_comment_html(comment: str) -> str:
833+
"""
834+
Makes generated comment markup safe for the CommonMark renderers used by
835+
GitHub and GitLab.
836+
837+
Drops whitespace-only lines (an optional section that rendered as empty
838+
leaves one behind) and caps indentation below the four spaces that would
839+
start an indented code block. Intentional separators - lines that are
840+
genuinely empty - are preserved so markdown blocks still break apart.
841+
842+
:param comment: str - The generated comment body.
843+
:return: str - The comment body with unrenderable whitespace removed.
844+
"""
845+
lines = []
846+
for line in comment.split("\n"):
847+
if line and not line.strip():
848+
continue
849+
stripped = line.lstrip()
850+
indent = min(len(line) - len(stripped), Messages.MAX_HTML_INDENT)
851+
lines.append(" " * indent + stripped)
852+
return "\n".join(lines)
853+
854+
@staticmethod
855+
def security_comment_no_alerts_template(view_report_url: str = "") -> str:
856+
"""
857+
Generates the body used when there is nothing left to report.
858+
859+
Alerts raised on an early commit are frequently resolved later in the same
860+
pull request. Rewriting the comment to this body keeps the Socket comment
861+
in place - so a later commit that reintroduces an alert updates it rather
862+
than posting a second comment - without leaving the "Caution" banner above
863+
an empty alerts table.
864+
865+
:param view_report_url: str - Optional link to the full Socket report.
866+
:return: str - The formatted Markdown/HTML string.
867+
"""
868+
lines = [
869+
"<!-- socket-security-comment-actions -->",
870+
"",
871+
"> **✅ Socket Security** ",
872+
"> No dependency alerts to report. Any alerts previously reported on this "
873+
"pull request have been resolved or ignored.",
874+
]
875+
if view_report_url:
876+
lines += ["", f"[View full report]({view_report_url})"]
877+
return "\n".join(lines) + "\n"
878+
879+
@staticmethod
880+
def get_view_report_url(diff: Diff) -> str:
881+
"""
882+
Resolves the report link for a diff, preferring the PR/MR diff view.
883+
884+
:param diff: Diff - Diff report to pull the URL from.
885+
:return: str - The report URL, or an empty string when neither is set.
886+
"""
887+
if getattr(diff, "diff_url", None):
888+
return diff.diff_url
889+
if getattr(diff, "report_url", None):
890+
return diff.report_url
891+
return ""
892+
809893
@staticmethod
810894
def security_comment_template(diff: Diff, config=None) -> str:
811895
"""
@@ -819,7 +903,7 @@ def security_comment_template(diff: Diff, config=None) -> str:
819903
# Group license policy violations by PURL (ecosystem/package@version)
820904
license_groups = {}
821905
security_alerts = []
822-
906+
823907
for alert in diff.new_alerts:
824908
if alert.type == "licenseSpdxDisj":
825909
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:
829913
else:
830914
security_alerts.append(alert)
831915

916+
view_report_url = Messages.get_view_report_url(diff)
917+
918+
# Without this the caution banner would sit above a table with no rows,
919+
# which is how a comment looks once every alert it raised is resolved.
920+
if not security_alerts and not license_groups:
921+
return Messages.security_comment_no_alerts_template(view_report_url)
922+
832923
# Start of the comment
833924
comment = """<!-- socket-security-comment-actions -->
834925
@@ -875,15 +966,15 @@ def security_comment_template(diff: Diff, config=None) -> str:
875966
</td>
876967
<td>
877968
<details {details_open}>
878-
<summary>{alert.pkg_name}@{alert.pkg_version} - {alert.title}</summary>
879-
<p><strong>Note:</strong> {alert.description}</p>
969+
<summary>{alert.pkg_name}@{alert.pkg_version} - {Messages.inline_html_text(alert.title)}</summary>
970+
<p><strong>Note:</strong> {Messages.inline_html_text(alert.description)}</p>
880971
<p><strong>Source:</strong> <a href="{manifest_url}">Manifest File</a></p>
881972
<p>ℹ️ Read more on:
882973
<a href="{alert.purl}">This package</a> |
883974
<a href="{alert.url}">This alert</a> |
884975
<a href="https://socket.dev/alerts/malware">What is known malware?</a></p>
885976
<blockquote>
886-
<p><em>Suggestion:</em> {alert.suggestion}</p>
977+
<p><em>Suggestion:</em> {Messages.inline_html_text(alert.suggestion)}</p>
887978
{ignore_html}
888979
</blockquote>
889980
</details>
@@ -917,7 +1008,7 @@ def security_comment_template(diff: Diff, config=None) -> str:
9171008
<ul>
9181009
"""
9191010
for finding in license_findings:
920-
comment += f" <li>{finding}</li>\n"
1011+
comment += f" <li>{Messages.inline_html_text(finding)}</li>\n"
9211012

9221013

9231014
# Generate proper manifest URL for license violations
@@ -944,13 +1035,6 @@ def security_comment_template(diff: Diff, config=None) -> str:
9441035
"""
9451036

9461037
# Close table
947-
# Use diff_url for PRs, report_url for non-PR scans
948-
view_report_url = ""
949-
if hasattr(diff, 'diff_url') and diff.diff_url:
950-
view_report_url = diff.diff_url
951-
elif hasattr(diff, 'report_url') and diff.report_url:
952-
view_report_url = diff.report_url
953-
9541038
comment += f"""
9551039
</tbody>
9561040
</table>
@@ -959,7 +1043,7 @@ def security_comment_template(diff: Diff, config=None) -> str:
9591043
[View full report]({view_report_url}?action=error%2Cwarn)
9601044
"""
9611045

962-
return comment
1046+
return Messages.normalize_comment_html(comment)
9631047

9641048
@staticmethod
9651049
def get_severity_icon(severity: str) -> str:

socketsecurity/core/scm_comments.py

Lines changed: 48 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
import json
2+
import re
23

34
from requests import Response
45

56
from socketsecurity.core import log
67
from socketsecurity.core.classes import Comment, Issue
8+
from socketsecurity.core.messages import Messages
79

810

911
class Comments:
12+
VIEW_REPORT_PATTERN = re.compile(r"\[View full report\]\(([^)\s]+)\)")
13+
1014
@staticmethod
1115
def process_response(response: Response) -> dict:
1216
output = {}
@@ -84,6 +88,20 @@ def is_heading_line(line) -> bool:
8488
is_heading_line = False
8589
return is_heading_line
8690

91+
@staticmethod
92+
def extract_report_url(body: str) -> str:
93+
"""
94+
Pulls the Socket report link out of an existing comment body so it can be
95+
carried over when the comment is rewritten.
96+
97+
:param body: str - The existing comment body.
98+
:return: str - The report URL without its query string, or "" if absent.
99+
"""
100+
match = Comments.VIEW_REPORT_PATTERN.search(body)
101+
if not match:
102+
return ""
103+
return match.group(1).split("?", 1)[0]
104+
87105
@staticmethod
88106
def process_security_comment(comment: Comment, comments) -> str:
89107
ignore_all, ignore_commands = Comments.get_ignore_options(comments)
@@ -102,6 +120,7 @@ def process_original_security_comment(
102120
) -> str:
103121
start = False
104122
lines = []
123+
kept_alert = False
105124
for line in comment.body_list:
106125
line = line.strip()
107126
if "start-socket-alerts-table" in line:
@@ -114,17 +133,25 @@ def process_original_security_comment(
114133
ecosystem = ecosystem.lstrip("[")
115134
pkg_name, pkg_version = details.split("@")
116135
pkg_name = f"{ecosystem}/{pkg_name}"
117-
ignore = False
118-
for name, version in ignore_commands:
119-
if ignore_all or Comments.is_ignore(pkg_name, pkg_version, name, version):
120-
ignore = True
136+
# ignore_all has to be checked outside the loop: an ignore-all
137+
# comment produces no ignore_commands, so a loop-internal check
138+
# never runs and every row was kept.
139+
ignore = ignore_all or any(
140+
Comments.is_ignore(pkg_name, pkg_version, name, version)
141+
for name, version in ignore_commands
142+
)
121143
if not ignore:
144+
kept_alert = True
122145
lines.append(line)
123146
elif "end-socket-alerts-table" in line:
124147
start = False
125148
lines.append(line)
126149
else:
127150
lines.append(line)
151+
if not kept_alert:
152+
return Messages.security_comment_no_alerts_template(
153+
Comments.extract_report_url("\n".join(comment.body_list))
154+
)
128155
return "\n".join(lines)
129156

130157
@staticmethod
@@ -145,17 +172,21 @@ def process_updated_security_comment(
145172
"""
146173
lines = []
147174
ignore_section = False
175+
kept_alert = False # Whether any alert row survived the ignore commands
148176
pkg_name = pkg_version = "" # Track current package and version
149177

150178
# Loop through the comment lines
151179
for line in comment.body_list:
152-
line = line.strip()
180+
# Match on the stripped line but keep the original, so the markup is
181+
# rewritten with the same indentation it was generated with.
182+
line = line.rstrip("\r")
183+
stripped = line.strip()
153184

154185
# Detect the start of an alert section
155-
if line.startswith("<!-- start-socket-alert-"):
186+
if stripped.startswith("<!-- start-socket-alert-"):
156187
# Extract package name and version from the comment
157188
try:
158-
start_marker = line[len("<!-- start-socket-alert-"):-4] # Strip the comment markers
189+
start_marker = stripped[len("<!-- start-socket-alert-"):-4] # Strip the comment markers
159190
pkg_name, pkg_version = start_marker.split("@") # Extract pkg_name and pkg_version
160191
except ValueError:
161192
pkg_name, pkg_version = "", ""
@@ -168,10 +199,11 @@ def process_updated_security_comment(
168199

169200
# If not ignored, include this start marker
170201
if not ignore_section:
202+
kept_alert = True
171203
lines.append(line)
172204

173205
# Detect the end of an alert section
174-
elif line.startswith("<!-- end-socket-alert-"):
206+
elif stripped.startswith("<!-- end-socket-alert-"):
175207
# Only include if we are not ignoring this section
176208
if not ignore_section:
177209
lines.append(line)
@@ -181,7 +213,14 @@ def process_updated_security_comment(
181213
elif not ignore_section:
182214
lines.append(line)
183215

184-
return "\n".join(lines)
216+
# Every row was ignored, so drop the table rather than leaving the caution
217+
# banner sitting above an empty one.
218+
if not kept_alert:
219+
return Messages.security_comment_no_alerts_template(
220+
Comments.extract_report_url("\n".join(comment.body_list))
221+
)
222+
223+
return Messages.normalize_comment_html("\n".join(lines))
185224

186225
@staticmethod
187226
def extract_alert_details_from_row(row: str, ignore_all: bool, ignore_commands: list[tuple[str, str]]) -> tuple:

0 commit comments

Comments
 (0)