Skip to content

Commit 717d0af

Browse files
authored
Fix pull request comment rendering and disable flags (#322)
* 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. * Make --disable-security-issue and --disable-overview suppress comments Both flags were checked only after testing whether a comment of that type was already on the pull request, so they suppressed the first post and then updated that comment on every later run. --disable-security-issue in particular kept refreshing an existing comment with the full alerts table. The flags now mean the CLI does not manage that comment at all. An existing comment is left untouched rather than rewritten, since a body claiming no alerts would be inaccurate when reporting is merely switched off. Moves the decision into should_write_comment() so it is covered by tests directly; main_code() had no harness for this block. Bumps to 2.7.0 rather than a patch, since these flags change behavior.
1 parent 1d193c7 commit 717d0af

9 files changed

Lines changed: 566 additions & 44 deletions

File tree

CHANGELOG.md

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Changelog
22

3-
## 2.6.12
3+
## 2.7.0
44

55
### Fixed: unreadable reachability facts no longer report a blocking package
66

@@ -18,6 +18,39 @@
1818
temporary directory previously used the same path and could remove each other's
1919
placeholder mid-upload.
2020

21+
### Fixed: pull request comments no longer show orphaned tags or an empty table
22+
23+
- Optional sections that rendered as empty, such as the ignore instructions
24+
suppressed by `--disable-ignore`, left a whitespace-only line in the alerts
25+
table. That line closed the surrounding HTML block, and the indented
26+
`</blockquote></details>` tags after it were rendered as a literal code block.
27+
Generated comment markup now omits blank lines and stays under the indentation
28+
that starts a code block.
29+
- Alert descriptions, suggestions and license findings are collapsed onto a
30+
single line so multi-line API text cannot break the table markup either.
31+
- When a pull request has no alerts left to report, the security comment is
32+
replaced with a short confirmation instead of keeping the "Caution" banner
33+
above a table with no rows. This happens both when a later commit resolves
34+
every alert and when every alert is ignored by comment. The comment marker is
35+
preserved, so a commit that reintroduces an alert updates the same comment
36+
rather than posting a second one.
37+
- `@SocketSecurity ignore-all` now applies to comments written by CLI versions
38+
before 2.0.55, which use the older Markdown alerts table. The check was made
39+
once per ignore command, and an ignore-all comment produces none, so no rows
40+
were removed.
41+
42+
### Fixed: `--disable-security-issue` and `--disable-overview` now suppress the comment entirely
43+
44+
- Both flags were checked only after testing whether a comment of that type was
45+
already on the pull request, so they suppressed the first post and then
46+
updated that comment on every later run. `--disable-security-issue` in
47+
particular kept refreshing an existing comment with the full alerts table.
48+
- The flags now mean the CLI does not manage that comment at all. An existing
49+
comment is left untouched rather than being rewritten, since a body claiming
50+
no alerts would be inaccurate when reporting is merely switched off.
51+
- The decision moved into `should_write_comment()` so it is covered directly by
52+
tests.
53+
2154
## 2.6.11
2255

2356
### Changed: bump pinned @coana-tech/cli to 15.10.32
@@ -49,7 +82,6 @@
4982
- Bumped the pinned reachability engine (`@coana-tech/cli`) from `15.10.23` to
5083
`15.10.25`. See the [Coana changelogs](https://docs.coana.tech/changelogs) for
5184
engine changes.
52-
5385
## 2.6.7
5486

5587
### 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.12"
9+
version = "2.7.0"
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.12'
2+
__version__ = '2.7.0'
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)