socketsecurity 2.6.11__tar.gz → 2.7.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (162) hide show
  1. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/CHANGELOG.md +59 -1
  2. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/PKG-INFO +2 -2
  3. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/docs/cli-reference.md +2 -3
  4. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/pyproject.toml +2 -2
  5. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/socketsecurity/__init__.py +1 -1
  6. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/socketsecurity/config.py +2 -2
  7. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/socketsecurity/core/__init__.py +107 -23
  8. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/socketsecurity/core/messages.py +97 -13
  9. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/socketsecurity/core/scm_comments.py +48 -9
  10. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/socketsecurity/core/tools/reachability.py +3 -8
  11. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/socketsecurity/socketcli.py +36 -17
  12. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/tests/core/test_facts_compression.py +1 -1
  13. socketsecurity-2.7.1/tests/core/test_invalid_facts_marker.py +278 -0
  14. socketsecurity-2.7.1/tests/unit/test_pr_comment_rendering.py +304 -0
  15. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/tests/unit/test_socketcli.py +44 -1
  16. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/uv.lock +23 -23
  17. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/.github/CODEOWNERS +0 -0
  18. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/.github/PULL_REQUEST_TEMPLATE/bug-fix.md +0 -0
  19. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/.github/PULL_REQUEST_TEMPLATE/feature.md +0 -0
  20. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/.github/PULL_REQUEST_TEMPLATE/improvement.md +0 -0
  21. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  22. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/.github/actions/setup-docker/action.yml +0 -0
  23. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/.github/actions/setup-hatch/action.yml +0 -0
  24. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/.github/actions/setup-sfw/action.yml +0 -0
  25. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/.github/dependabot.yml +0 -0
  26. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/.github/workflows/dependency-review.yml +0 -0
  27. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/.github/workflows/docker-stable.yml +0 -0
  28. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/.github/workflows/e2e-test.yml +0 -0
  29. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/.github/workflows/package-check.yml +0 -0
  30. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/.github/workflows/pr-preview.yml +0 -0
  31. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/.github/workflows/python-tests.yml +0 -0
  32. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/.github/workflows/release.yml +0 -0
  33. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/.github/workflows/version-check.yml +0 -0
  34. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/.github/zizmor.yml +0 -0
  35. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/.gitignore +0 -0
  36. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/.hooks/sync_version.py +0 -0
  37. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/.pre-commit-config.yaml +0 -0
  38. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/.python-version +0 -0
  39. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/CONTRIBUTING.md +0 -0
  40. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/Dockerfile +0 -0
  41. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/Dockerfile.preview +0 -0
  42. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/LICENSE +0 -0
  43. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/Makefile +0 -0
  44. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/README.md +0 -0
  45. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/benchmarks/manifest_discovery.py +0 -0
  46. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/docs/ci-cd.md +0 -0
  47. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/docs/development.md +0 -0
  48. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/docs/troubleshooting.md +0 -0
  49. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/examples/config/sarif-dashboard-parity.json +0 -0
  50. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/examples/config/sarif-dashboard-parity.toml +0 -0
  51. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/examples/config/sarif-diff-ci-cd.json +0 -0
  52. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/examples/config/sarif-diff-ci-cd.toml +0 -0
  53. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/examples/config/sarif-instance-detail.json +0 -0
  54. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/examples/config/sarif-instance-detail.toml +0 -0
  55. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/instructions/gitlab-commit-status/uat.md +0 -0
  56. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/pytest.ini +0 -0
  57. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/scripts/build_container.sh +0 -0
  58. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/scripts/build_container_flexible.sh +0 -0
  59. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/scripts/deploy-test-docker.sh +0 -0
  60. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/scripts/deploy-test-pypi.sh +0 -0
  61. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/scripts/docker-entrypoint.sh +0 -0
  62. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/scripts/run.sh +0 -0
  63. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/session.md +0 -0
  64. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/socket.yml +0 -0
  65. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/socketsecurity/core/alert_selection.py +0 -0
  66. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/socketsecurity/core/classes.py +0 -0
  67. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/socketsecurity/core/cli_client.py +0 -0
  68. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/socketsecurity/core/cli_run.py +0 -0
  69. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/socketsecurity/core/exceptions.py +0 -0
  70. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/socketsecurity/core/git_interface.py +0 -0
  71. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/socketsecurity/core/helper/__init__.py +0 -0
  72. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/socketsecurity/core/helper/socket_facts_loader.py +0 -0
  73. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/socketsecurity/core/lazy_file_loader.py +0 -0
  74. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/socketsecurity/core/log_uploader.py +0 -0
  75. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/socketsecurity/core/logging.py +0 -0
  76. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/socketsecurity/core/resource_utils.py +0 -0
  77. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/socketsecurity/core/scm/__init__.py +0 -0
  78. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/socketsecurity/core/scm/base.py +0 -0
  79. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/socketsecurity/core/scm/client.py +0 -0
  80. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/socketsecurity/core/scm/github.py +0 -0
  81. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/socketsecurity/core/scm/gitlab.py +0 -0
  82. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/socketsecurity/core/socket_config.py +0 -0
  83. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/socketsecurity/core/streaming.py +0 -0
  84. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/socketsecurity/core/utils.py +0 -0
  85. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/socketsecurity/fossa_compat.py +0 -0
  86. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/socketsecurity/output.py +0 -0
  87. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/socketsecurity/plugins/__init__.py +0 -0
  88. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/socketsecurity/plugins/base.py +0 -0
  89. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/socketsecurity/plugins/formatters/__init__.py +0 -0
  90. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/socketsecurity/plugins/formatters/slack.py +0 -0
  91. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/socketsecurity/plugins/jira.py +0 -0
  92. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/socketsecurity/plugins/manager.py +0 -0
  93. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/socketsecurity/plugins/slack.py +0 -0
  94. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/socketsecurity/plugins/teams.py +0 -0
  95. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/socketsecurity/plugins/webhook.py +0 -0
  96. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/tests/__init__.py +0 -0
  97. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/tests/core/conftest.py +0 -0
  98. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/tests/core/create_diff_input.json +0 -0
  99. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/tests/core/test_diff_alerts.py +0 -0
  100. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/tests/core/test_diff_generation.py +0 -0
  101. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/tests/core/test_diff_scan_polling.py +0 -0
  102. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/tests/core/test_has_manifest_files.py +0 -0
  103. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/tests/core/test_package_and_alerts.py +0 -0
  104. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/tests/core/test_sdk_methods.py +0 -0
  105. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/tests/core/test_supporting_methods.py +0 -0
  106. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/tests/data/fullscans/create_response.json +0 -0
  107. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/tests/data/fullscans/diff/stream_diff.json +0 -0
  108. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/tests/data/fullscans/diff/stream_diff_full.json +0 -0
  109. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/tests/data/fullscans/head_scan/metadata.json +0 -0
  110. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/tests/data/fullscans/head_scan/stream_scan.json +0 -0
  111. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/tests/data/fullscans/head_scan/stream_scan_full.json +0 -0
  112. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/tests/data/fullscans/new_scan/metadata.json +0 -0
  113. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/tests/data/fullscans/new_scan/stream_scan.json +0 -0
  114. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/tests/data/repos/repo_info_error.json +0 -0
  115. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/tests/data/repos/repo_info_no_head.json +0 -0
  116. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/tests/data/repos/repo_info_success.json +0 -0
  117. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/tests/data/settings/security-policy.json +0 -0
  118. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/tests/e2e/fixtures/simple-npm/index.js +0 -0
  119. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/tests/e2e/fixtures/simple-npm/package.json +0 -0
  120. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/tests/e2e/fixtures/simple-pypi/requirements.txt +0 -0
  121. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/tests/e2e/reach-facts-probe.sh +0 -0
  122. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/tests/e2e/validate-gitlab.sh +0 -0
  123. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/tests/e2e/validate-json.sh +0 -0
  124. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/tests/e2e/validate-reachability.sh +0 -0
  125. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/tests/e2e/validate-sarif.sh +0 -0
  126. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/tests/e2e/validate-scan.sh +0 -0
  127. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/tests/fixtures/fossa/README.md +0 -0
  128. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/tests/fixtures/fossa/fossa-analyze-empty.json +0 -0
  129. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/tests/fixtures/fossa/fossa-analyze-populated.json +0 -0
  130. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/tests/fixtures/fossa/fossa-sbom-empty-deep.json +0 -0
  131. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/tests/fixtures/fossa/fossa-sbom-populated.json +0 -0
  132. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/tests/unit/__init__.py +0 -0
  133. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/tests/unit/test_alert_selection.py +0 -0
  134. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/tests/unit/test_cli_config.py +0 -0
  135. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/tests/unit/test_cli_run.py +0 -0
  136. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/tests/unit/test_client.py +0 -0
  137. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/tests/unit/test_config.py +0 -0
  138. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/tests/unit/test_dependency_overview.py +0 -0
  139. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/tests/unit/test_disable_ignore.py +0 -0
  140. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/tests/unit/test_exclude_paths.py +0 -0
  141. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/tests/unit/test_fossa_compat.py +0 -0
  142. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/tests/unit/test_fossa_parity.py +0 -0
  143. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/tests/unit/test_full_scan_retry.py +0 -0
  144. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/tests/unit/test_git_interface.py +0 -0
  145. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/tests/unit/test_github_buildkite_config.py +0 -0
  146. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/tests/unit/test_gitlab_auth.py +0 -0
  147. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/tests/unit/test_gitlab_auth_fallback.py +0 -0
  148. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/tests/unit/test_gitlab_commit_status.py +0 -0
  149. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/tests/unit/test_gitlab_format.py +0 -0
  150. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/tests/unit/test_ignore_telemetry_filtering.py +0 -0
  151. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/tests/unit/test_include_dirs.py +0 -0
  152. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/tests/unit/test_log_uploader.py +0 -0
  153. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/tests/unit/test_manifest_discovery.py +0 -0
  154. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/tests/unit/test_output.py +0 -0
  155. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/tests/unit/test_reachability.py +0 -0
  156. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/tests/unit/test_slack_plugin.py +0 -0
  157. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/tests/unit/test_streaming.py +0 -0
  158. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/tests/unit/test_tier1_finalize.py +0 -0
  159. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/workflows/bitbucket-pipelines.yml +0 -0
  160. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/workflows/buildkite.yml +0 -0
  161. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/workflows/github-actions.yml +0 -0
  162. {socketsecurity-2.6.11 → socketsecurity-2.7.1}/workflows/gitlab-ci.yml +0 -0
@@ -1,5 +1,64 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.7.1
4
+
5
+ ### Changed: bump pinned @coana-tech/cli to 15.10.36
6
+
7
+ - Bumped the pinned reachability engine (`@coana-tech/cli`) from `15.10.32` to
8
+ `15.10.36`. See the [Coana changelogs](https://docs.coana.tech/changelogs) for
9
+ engine changes.
10
+
11
+ ## 2.7.0
12
+
13
+ ### Fixed: unreadable reachability facts no longer report a blocking package
14
+
15
+ - Scans with no supported manifest files uploaded a zero-byte `.socket.facts.json`
16
+ placeholder. The API cannot parse that, and answers by adding a
17
+ `generic/invalid-socket-facts@1.0.0` artifact to the scan, which the CLI then reported
18
+ as a new blocking package with no manifest file and no introducing dependency —
19
+ failing the run and, on pull requests, leaving a security comment that could not be
20
+ acted on. The placeholder is now an empty but well-formed facts document.
21
+ - When the API does report `generic/invalid-socket-facts` (a diagnostic for a facts file
22
+ it could not parse, not a real dependency), the CLI now excludes it from scan results
23
+ and logs a warning instead. It no longer blocks a run, appears in reports, or triggers
24
+ a pull request comment.
25
+ - Each placeholder is written to its own temporary directory. Two CLI runs sharing a
26
+ temporary directory previously used the same path and could remove each other's
27
+ placeholder mid-upload.
28
+
29
+ ### Fixed: pull request comments no longer show orphaned tags or an empty table
30
+
31
+ - Optional sections that rendered as empty, such as the ignore instructions
32
+ suppressed by `--disable-ignore`, left a whitespace-only line in the alerts
33
+ table. That line closed the surrounding HTML block, and the indented
34
+ `</blockquote></details>` tags after it were rendered as a literal code block.
35
+ Generated comment markup now omits blank lines and stays under the indentation
36
+ that starts a code block.
37
+ - Alert descriptions, suggestions and license findings are collapsed onto a
38
+ single line so multi-line API text cannot break the table markup either.
39
+ - When a pull request has no alerts left to report, the security comment is
40
+ replaced with a short confirmation instead of keeping the "Caution" banner
41
+ above a table with no rows. This happens both when a later commit resolves
42
+ every alert and when every alert is ignored by comment. The comment marker is
43
+ preserved, so a commit that reintroduces an alert updates the same comment
44
+ rather than posting a second one.
45
+ - `@SocketSecurity ignore-all` now applies to comments written by CLI versions
46
+ before 2.0.55, which use the older Markdown alerts table. The check was made
47
+ once per ignore command, and an ignore-all comment produces none, so no rows
48
+ were removed.
49
+
50
+ ### Fixed: `--disable-security-issue` and `--disable-overview` now suppress the comment entirely
51
+
52
+ - Both flags were checked only after testing whether a comment of that type was
53
+ already on the pull request, so they suppressed the first post and then
54
+ updated that comment on every later run. `--disable-security-issue` in
55
+ particular kept refreshing an existing comment with the full alerts table.
56
+ - The flags now mean the CLI does not manage that comment at all. An existing
57
+ comment is left untouched rather than being rewritten, since a body claiming
58
+ no alerts would be inaccurate when reporting is merely switched off.
59
+ - The decision moved into `should_write_comment()` so it is covered directly by
60
+ tests.
61
+
3
62
  ## 2.6.11
4
63
 
5
64
  ### Changed: bump pinned @coana-tech/cli to 15.10.32
@@ -31,7 +90,6 @@
31
90
  - Bumped the pinned reachability engine (`@coana-tech/cli`) from `15.10.23` to
32
91
  `15.10.25`. See the [Coana changelogs](https://docs.coana.tech/changelogs) for
33
92
  engine changes.
34
-
35
93
  ## 2.6.7
36
94
 
37
95
  ### Changed: bump pinned @coana-tech/cli to 15.10.23
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: socketsecurity
3
- Version: 2.6.11
3
+ Version: 2.7.1
4
4
  Summary: Socket Security CLI for CI/CD
5
5
  Project-URL: Homepage, https://socket.dev
6
6
  Author-email: Douglas Coburn <douglas@socket.dev>
@@ -47,7 +47,7 @@ Requires-Dist: socketdev==3.5.0
47
47
  Provides-Extra: dev
48
48
  Requires-Dist: hatch==1.18.0; extra == 'dev'
49
49
  Requires-Dist: pre-commit==4.6.2; extra == 'dev'
50
- Requires-Dist: ruff==0.16.3; extra == 'dev'
50
+ Requires-Dist: ruff==0.16.4; extra == 'dev'
51
51
  Requires-Dist: twine==7.0.0; extra == 'dev'
52
52
  Requires-Dist: uv==0.12.5; extra == 'dev'
53
53
  Provides-Extra: test
@@ -156,7 +156,7 @@ socketcli [-h] [--api-token API_TOKEN] [--repo REPO] [--workspace WORKSPACE] [--
156
156
  [--reach] [--reach-version REACH_VERSION] [--reach-analysis-timeout REACH_ANALYSIS_TIMEOUT]
157
157
  [--reach-analysis-memory-limit REACH_ANALYSIS_MEMORY_LIMIT] [--reach-concurrency REACH_CONCURRENCY] [--reach-ecosystems REACH_ECOSYSTEMS]
158
158
  [--reach-min-severity <level>] [--reach-skip-cache] [--reach-disable-analytics] [--reach-enable-analysis-splitting] [--reach-detailed-analysis-log-file]
159
- [--reach-lazy-mode] [--reach-use-only-pregenerated-sboms] [--reach-debug] [--reach-disable-external-tool-checks]
159
+ [--reach-use-only-pregenerated-sboms] [--reach-debug] [--reach-disable-external-tool-checks]
160
160
  [--reach-output-file REACH_OUTPUT_FILE] [--only-facts-file] [--version]
161
161
  ````
162
162
 
@@ -275,7 +275,7 @@ If you don't want to provide the Socket API Token every time then you can use th
275
275
  | Parameter | Required | Default | Description |
276
276
  |:---------------------------------|:---------|:--------|:---------------------------------------------------------------------------------------------------------------------------|
277
277
  | `--reach` | False | False | Enable reachability analysis to identify which vulnerable functions are actually called by your code. Creates a full application reachability scan (`scan_type=socket_tier1`). |
278
- | `--reach-version` | False | 15.10.32 | Version of @coana-tech/cli to use. Defaults to the pinned version that ships with this CLI release, so the engine only changes when you upgrade the Socket CLI. Pass `latest` to always use the newest published version (opt-in auto-update), or an explicit version (e.g. `1.2.3`) to pin it. |
278
+ | `--reach-version` | False | 15.10.36 | Version of @coana-tech/cli to use. Defaults to the pinned version that ships with this CLI release, so the engine only changes when you upgrade the Socket CLI. Pass `latest` to always use the newest published version (opt-in auto-update), or an explicit version (e.g. `1.2.3`) to pin it. |
279
279
  | `--reach-analysis-timeout` | False | 10m | Timeout for each reachability analysis run, e.g. `90s`, `10m` or `1h`. Omitted by default, so coana applies its own default (`10m`). Alias: `--reach-timeout` |
280
280
  | `--reach-analysis-memory-limit` | False | 8GB | Memory limit for each reachability analysis run, e.g. `512MB` or `8GB`. Omitted by default, so coana applies its own default (`8GB`). Alias: `--reach-memory-limit` |
281
281
  | `--reach-concurrency` | False | 1 | Control parallel analysis execution (must be >= 1). Omitted by default, so coana applies its own default. |
@@ -286,7 +286,6 @@ If you don't want to provide the Socket API Token every time then you can use th
286
286
  | `--reach-disable-analytics` | False | False | Disable analytics collection during reachability analysis |
287
287
  | `--reach-enable-analysis-splitting` | False | False | Enable analysis splitting/bucketing (a legacy performance feature). Splitting is disabled by default. |
288
288
  | `--reach-detailed-analysis-log-file` | False | False | Write a detailed analysis log file; its path is printed to stdout |
289
- | `--reach-lazy-mode` | False | False | Enable lazy mode (experimental performance feature) |
290
289
  | `--reach-use-only-pregenerated-sboms` | False | False | Build the scan only from pre-generated CycloneDX (CDX) and SPDX files in your project (requires --reach) |
291
290
  | `--reach-debug` | False | False | Enable coana debug output (`--debug`) for the analysis, independent of the global `--enable-debug` |
292
291
  | `--reach-disable-external-tool-checks` | False | False | Disable coana's external tool availability checks (passes `--disable-external-tool-checks`) |
@@ -6,7 +6,7 @@ build-backend = "hatchling.build"
6
6
 
7
7
  [project]
8
8
  name = "socketsecurity"
9
- version = "2.6.11"
9
+ version = "2.7.1"
10
10
  requires-python = ">= 3.11"
11
11
  license = {"file" = "LICENSE"}
12
12
  dependencies = [
@@ -47,7 +47,7 @@ test = [
47
47
  "pytest-watch==4.2.0"
48
48
  ]
49
49
  dev = [
50
- "ruff==0.16.3",
50
+ "ruff==0.16.4",
51
51
  "twine==7.0.0", # for building
52
52
  "uv==0.12.5", # for dependency management
53
53
  "pre-commit==4.6.2",
@@ -1,3 +1,3 @@
1
1
  __author__ = 'socket.dev'
2
- __version__ = '2.6.11'
2
+ __version__ = '2.7.1'
3
3
  USER_AGENT = f'SocketPythonCLI/{__version__}'
@@ -176,7 +176,7 @@ class CliConfig:
176
176
  reach_disable_analysis_splitting: bool = False # Deprecated, kept for backwards compatibility
177
177
  reach_enable_analysis_splitting: bool = False
178
178
  reach_detailed_analysis_log_file: bool = False
179
- reach_lazy_mode: bool = False
179
+ reach_lazy_mode: bool = False # Deprecated, kept for backwards compatibility
180
180
  reach_ecosystems: Optional[List[str]] = None
181
181
  reach_exclude_paths: Optional[List[str]] = None
182
182
  reach_skip_cache: bool = False
@@ -1091,7 +1091,7 @@ def create_argument_parser() -> argparse.ArgumentParser:
1091
1091
  "--reach-lazy-mode",
1092
1092
  dest="reach_lazy_mode",
1093
1093
  action="store_true",
1094
- help="Enable lazy mode for reachability analysis. This is an experimental feature for improving performance"
1094
+ help=argparse.SUPPRESS # Deprecated, kept for backwards compatibility (no-op)
1095
1095
  )
1096
1096
  reachability_group.add_argument(
1097
1097
  "--reach-output-file",
@@ -1,3 +1,4 @@
1
+ import atexit
1
2
  import copy
2
3
  import fnmatch
3
4
  import importlib
@@ -6,6 +7,7 @@ import logging
6
7
  import os
7
8
  import random
8
9
  import re
10
+ import shutil
9
11
  import sys
10
12
  import tarfile
11
13
  import tempfile
@@ -69,6 +71,16 @@ SOCKET_FACTS_BROTLI_LGWIN = 24
69
71
  # Stream the facts file in 1 MiB chunks so large files aren't held fully in memory.
70
72
  SOCKET_FACTS_BROTLI_CHUNK_SIZE = 1024 * 1024
71
73
 
74
+ # Placeholder facts document (see empty_head_scan_file). A zero-byte file does not parse,
75
+ # and the API answers an unparseable facts file with the marker artifact below.
76
+ SOCKET_FACTS_EMPTY_DOCUMENT = '{"components": []}'
77
+
78
+ # Synthetic artifact the API adds when an uploaded ``.socket.facts.json`` could not be
79
+ # parsed. A diagnostic, not a dependency, so it is dropped from scan results and reported
80
+ # as a warning instead.
81
+ INVALID_FACTS_MARKER_TYPE = "generic"
82
+ INVALID_FACTS_MARKER_NAME = "invalid-socket-facts"
83
+
72
84
  # Full application reachability finalize retry policy. The finalize call links the reachability
73
85
  # scan to the full scan and can fail transiently (network/API blips); a few backoff retries make it robust.
74
86
  TIER1_FINALIZE_MAX_ATTEMPTS = 3
@@ -108,6 +120,17 @@ DIFF_SCAN_POLL_MAX_INTERVAL_SECONDS = 10.0
108
120
  DIFF_SCAN_POLL_BACKOFF_MULTIPLIER = 1.5
109
121
  DIFF_SCAN_POLL_TIMEOUT_SECONDS = 30 * 60.0
110
122
 
123
+ # Temp dirs holding placeholder facts files (see Core.empty_head_scan_file). Call sites unlink
124
+ # the file itself once the upload finishes; the now-empty directory is removed at process exit
125
+ # so a run that raises mid-scan doesn't leak one.
126
+ _PLACEHOLDER_FACTS_DIRS: List[str] = []
127
+
128
+
129
+ @atexit.register
130
+ def _cleanup_placeholder_facts_dirs() -> None:
131
+ for placeholder_dir in _PLACEHOLDER_FACTS_DIRS:
132
+ shutil.rmtree(placeholder_dir, ignore_errors=True)
133
+
111
134
 
112
135
  def _humanize_alert_type(alert_type: str) -> str:
113
136
  """Convert a camelCase/PascalCase alert type into a Title-Cased label.
@@ -209,13 +232,54 @@ class Core:
209
232
  )
210
233
  if not hasattr(response, "artifacts") or not response.artifacts:
211
234
  return {}
212
- return response.artifacts
235
+ artifacts = {
236
+ artifact_id: artifact
237
+ for artifact_id, artifact in response.artifacts.items()
238
+ if not Core.is_invalid_facts_marker(artifact)
239
+ }
240
+ Core.warn_if_invalid_facts_marker(len(artifacts) != len(response.artifacts))
241
+ return artifacts
213
242
 
214
243
  def get_sbom_data_list(self, artifacts_dict: Dict[str, SocketArtifact]) -> list[SocketArtifact]:
215
244
  """Converts artifacts dictionary to a list."""
216
245
  return list(artifacts_dict.values())
217
246
 
247
+ @staticmethod
248
+ def is_invalid_facts_marker(artifact) -> bool:
249
+ """True for the API's ``generic/invalid-socket-facts`` unparseable-facts marker.
250
+
251
+ Treated as a package it becomes a blocking alert with an empty "Introduced by" and
252
+ "Manifest File" that no developer can act on, so callers drop it and report the parse
253
+ failure through ``warn_if_invalid_facts_marker`` instead.
218
254
 
255
+ Matches any version; the API pins it to 1.0.0 but the version carries no meaning.
256
+
257
+ Args:
258
+ artifact: A ``SocketArtifact`` or diff artifact (anything with ``type``/``name``).
259
+
260
+ Returns:
261
+ True if the artifact is the marker rather than a real package.
262
+ """
263
+ return (
264
+ getattr(artifact, "type", None) == INVALID_FACTS_MARKER_TYPE
265
+ and getattr(artifact, "name", None) == INVALID_FACTS_MARKER_NAME
266
+ )
267
+
268
+ @staticmethod
269
+ def warn_if_invalid_facts_marker(found: bool) -> None:
270
+ """Log the parse failure that ``is_invalid_facts_marker`` stands for.
271
+
272
+ Dropping the marker silently would hide a real, if non-blocking, problem: the scan ran
273
+ without the reachability data it was supposed to carry.
274
+ """
275
+ if not found:
276
+ return
277
+ log.warning(
278
+ "Socket could not parse the uploaded .socket.facts.json, so reachability facts "
279
+ "were not applied to this scan. Ignoring the "
280
+ f"{INVALID_FACTS_MARKER_TYPE}/{INVALID_FACTS_MARKER_NAME} marker returned for it; "
281
+ "other scan results are unaffected."
282
+ )
219
283
 
220
284
  def create_sbom_output(self, diff: Diff) -> dict:
221
285
  """Creates CycloneDX output for a given diff."""
@@ -809,20 +873,31 @@ class Core:
809
873
  @staticmethod
810
874
  def empty_head_scan_file() -> List[str]:
811
875
  """
812
- Creates a temporary empty file for baseline scans when no head scan exists.
813
-
876
+ Creates a temporary placeholder manifest for scans with no manifest files.
877
+
878
+ Used for baseline scans when a repository has no head scan yet, and for the new scan
879
+ when no supported manifest files were found. The API rejects unsupported filenames, so
880
+ the placeholder must be named ``.socket.facts.json`` - which means it must also parse
881
+ as a facts document. A zero-byte file does not, and the API answers that by adding a
882
+ blocking ``generic/invalid-socket-facts@1.0.0`` artifact to the scan.
883
+
884
+ Each call gets its own temp directory. The path used to be a fixed
885
+ ``$TMPDIR/.socket.facts.json``, so two runs sharing a temp dir could delete or
886
+ truncate each other's placeholder mid-upload.
887
+
814
888
  Returns:
815
- List containing path to a temporary empty file
889
+ List containing path to a temporary placeholder facts file
816
890
  """
817
- # Create a temporary directory and then create our specific filename
818
- temp_dir = tempfile.gettempdir()
819
- temp_path = os.path.join(temp_dir, '.socket.facts.json')
820
-
821
- # Create the empty file
822
- with open(temp_path, 'w'):
823
- pass # Creates an empty file
824
-
825
- log.debug(f"Created temporary empty file for baseline scan: {temp_path}")
891
+ # Own directory per call so concurrent runs can't clobber each other's placeholder;
892
+ # the basename must stay exactly SOCKET_FACTS_FILENAME to pass the API's validator.
893
+ temp_dir = tempfile.mkdtemp(prefix='socket_baseline_')
894
+ _PLACEHOLDER_FACTS_DIRS.append(temp_dir)
895
+ temp_path = os.path.join(temp_dir, SOCKET_FACTS_FILENAME)
896
+
897
+ with open(temp_path, 'w') as f:
898
+ f.write(SOCKET_FACTS_EMPTY_DOCUMENT)
899
+
900
+ log.debug(f"Created temporary placeholder facts file for baseline scan: {temp_path}")
826
901
  return [temp_path]
827
902
 
828
903
  def finalize_tier1_scan(self, full_scan_id: str, facts_file_path: str) -> bool:
@@ -959,7 +1034,7 @@ class Core:
959
1034
  exactly ``.socket.facts.json.br``, so compressing here keeps a large facts file under
960
1035
  the server's per-file size cap without changing the stored result. Files whose
961
1036
  basename is not exactly ``.socket.facts.json`` are left untouched (the server only
962
- matches that exact name), as are empty placeholder files (e.g. baseline scans).
1037
+ matches that exact name), as are zero-byte files.
963
1038
 
964
1039
  Compression never blocks an upload: if it fails for any reason (missing optional
965
1040
  ``brotli`` dependency, unwritable directory, etc.) the original plain file is used.
@@ -1780,16 +1855,25 @@ class Core:
1780
1855
 
1781
1856
  diff_end = time.time()
1782
1857
  log.info(f"Diff Report Gathered in {diff_end - diff_start:.2f} seconds")
1858
+
1859
+ # Left in, the invalid-socket-facts marker reads as a newly added blocking package.
1860
+ # Drop it from every bucket before the counts below, which should describe what the
1861
+ # CLI actually reports on.
1862
+ marker_found = False
1863
+ buckets: Dict[str, List] = {}
1864
+ for name in ("added", "removed", "unchanged", "replaced", "updated"):
1865
+ bucket = getattr(diff_artifacts, name)
1866
+ buckets[name] = [a for a in bucket if not Core.is_invalid_facts_marker(a)]
1867
+ marker_found = marker_found or len(buckets[name]) != len(bucket)
1868
+ Core.warn_if_invalid_facts_marker(marker_found)
1869
+
1783
1870
  log.info("Diff report artifact counts:")
1784
- log.info(f"Added: {len(diff_artifacts.added)}")
1785
- log.info(f"Removed: {len(diff_artifacts.removed)}")
1786
- log.info(f"Unchanged: {len(diff_artifacts.unchanged)}")
1787
- log.info(f"Replaced: {len(diff_artifacts.replaced)}")
1788
- log.info(f"Updated: {len(diff_artifacts.updated)}")
1789
-
1790
- added_artifacts = diff_artifacts.added + diff_artifacts.updated
1791
- removed_artifacts = diff_artifacts.removed + diff_artifacts.replaced
1792
- unchanged_artifacts = diff_artifacts.unchanged
1871
+ for name, bucket in buckets.items():
1872
+ log.info(f"{name.capitalize()}: {len(bucket)}")
1873
+
1874
+ added_artifacts = buckets["added"] + buckets["updated"]
1875
+ removed_artifacts = buckets["removed"] + buckets["replaced"]
1876
+ unchanged_artifacts = buckets["unchanged"]
1793
1877
 
1794
1878
  added_packages: Dict[str, Package] = {}
1795
1879
  removed_packages: Dict[str, Package] = {}
@@ -806,6 +806,90 @@ class Messages:
806
806
 
807
807
  return gitlab_report
808
808
 
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
+
809
893
  @staticmethod
810
894
  def security_comment_template(diff: Diff, config=None) -> str:
811
895
  """
@@ -819,7 +903,7 @@ class Messages:
819
903
  # Group license policy violations by PURL (ecosystem/package@version)
820
904
  license_groups = {}
821
905
  security_alerts = []
822
-
906
+
823
907
  for alert in diff.new_alerts:
824
908
  if alert.type == "licenseSpdxDisj":
825
909
  purl_key = f"{alert.pkg_type}/{alert.pkg_name}@{alert.pkg_version}"
@@ -829,6 +913,13 @@ class Messages:
829
913
  else:
830
914
  security_alerts.append(alert)
831
915
 
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
+
832
923
  # Start of the comment
833
924
  comment = """<!-- socket-security-comment-actions -->
834
925
 
@@ -875,15 +966,15 @@ class Messages:
875
966
  </td>
876
967
  <td>
877
968
  <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>
880
971
  <p><strong>Source:</strong> <a href="{manifest_url}">Manifest File</a></p>
881
972
  <p>ℹ️ Read more on:
882
973
  <a href="{alert.purl}">This package</a> |
883
974
  <a href="{alert.url}">This alert</a> |
884
975
  <a href="https://socket.dev/alerts/malware">What is known malware?</a></p>
885
976
  <blockquote>
886
- <p><em>Suggestion:</em> {alert.suggestion}</p>
977
+ <p><em>Suggestion:</em> {Messages.inline_html_text(alert.suggestion)}</p>
887
978
  {ignore_html}
888
979
  </blockquote>
889
980
  </details>
@@ -917,7 +1008,7 @@ class Messages:
917
1008
  <ul>
918
1009
  """
919
1010
  for finding in license_findings:
920
- comment += f" <li>{finding}</li>\n"
1011
+ comment += f" <li>{Messages.inline_html_text(finding)}</li>\n"
921
1012
 
922
1013
 
923
1014
  # Generate proper manifest URL for license violations
@@ -944,13 +1035,6 @@ class Messages:
944
1035
  """
945
1036
 
946
1037
  # 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
-
954
1038
  comment += f"""
955
1039
  </tbody>
956
1040
  </table>
@@ -959,7 +1043,7 @@ class Messages:
959
1043
  [View full report]({view_report_url}?action=error%2Cwarn)
960
1044
  """
961
1045
 
962
- return comment
1046
+ return Messages.normalize_comment_html(comment)
963
1047
 
964
1048
  @staticmethod
965
1049
  def get_severity_icon(severity: str) -> str:
@@ -1,12 +1,16 @@
1
1
  import json
2
+ import re
2
3
 
3
4
  from requests import Response
4
5
 
5
6
  from socketsecurity.core import log
6
7
  from socketsecurity.core.classes import Comment, Issue
8
+ from socketsecurity.core.messages import Messages
7
9
 
8
10
 
9
11
  class Comments:
12
+ VIEW_REPORT_PATTERN = re.compile(r"\[View full report\]\(([^)\s]+)\)")
13
+
10
14
  @staticmethod
11
15
  def process_response(response: Response) -> dict:
12
16
  output = {}
@@ -84,6 +88,20 @@ class Comments:
84
88
  is_heading_line = False
85
89
  return is_heading_line
86
90
 
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
+
87
105
  @staticmethod
88
106
  def process_security_comment(comment: Comment, comments) -> str:
89
107
  ignore_all, ignore_commands = Comments.get_ignore_options(comments)
@@ -102,6 +120,7 @@ class Comments:
102
120
  ) -> str:
103
121
  start = False
104
122
  lines = []
123
+ kept_alert = False
105
124
  for line in comment.body_list:
106
125
  line = line.strip()
107
126
  if "start-socket-alerts-table" in line:
@@ -114,17 +133,25 @@ class Comments:
114
133
  ecosystem = ecosystem.lstrip("[")
115
134
  pkg_name, pkg_version = details.split("@")
116
135
  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
+ )
121
143
  if not ignore:
144
+ kept_alert = True
122
145
  lines.append(line)
123
146
  elif "end-socket-alerts-table" in line:
124
147
  start = False
125
148
  lines.append(line)
126
149
  else:
127
150
  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
+ )
128
155
  return "\n".join(lines)
129
156
 
130
157
  @staticmethod
@@ -145,17 +172,21 @@ class Comments:
145
172
  """
146
173
  lines = []
147
174
  ignore_section = False
175
+ kept_alert = False # Whether any alert row survived the ignore commands
148
176
  pkg_name = pkg_version = "" # Track current package and version
149
177
 
150
178
  # Loop through the comment lines
151
179
  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()
153
184
 
154
185
  # Detect the start of an alert section
155
- if line.startswith("<!-- start-socket-alert-"):
186
+ if stripped.startswith("<!-- start-socket-alert-"):
156
187
  # Extract package name and version from the comment
157
188
  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
159
190
  pkg_name, pkg_version = start_marker.split("@") # Extract pkg_name and pkg_version
160
191
  except ValueError:
161
192
  pkg_name, pkg_version = "", ""
@@ -168,10 +199,11 @@ class Comments:
168
199
 
169
200
  # If not ignored, include this start marker
170
201
  if not ignore_section:
202
+ kept_alert = True
171
203
  lines.append(line)
172
204
 
173
205
  # Detect the end of an alert section
174
- elif line.startswith("<!-- end-socket-alert-"):
206
+ elif stripped.startswith("<!-- end-socket-alert-"):
175
207
  # Only include if we are not ignoring this section
176
208
  if not ignore_section:
177
209
  lines.append(line)
@@ -181,7 +213,14 @@ class Comments:
181
213
  elif not ignore_section:
182
214
  lines.append(line)
183
215
 
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))
185
224
 
186
225
  @staticmethod
187
226
  def extract_alert_details_from_row(row: str, ignore_all: bool, ignore_commands: list[tuple[str, str]]) -> tuple: