socketsecurity 2.6.11__tar.gz → 2.7.0__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.0}/CHANGELOG.md +51 -1
  2. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/PKG-INFO +2 -2
  3. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/pyproject.toml +2 -2
  4. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/socketsecurity/__init__.py +1 -1
  5. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/socketsecurity/core/__init__.py +107 -23
  6. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/socketsecurity/core/messages.py +97 -13
  7. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/socketsecurity/core/scm_comments.py +48 -9
  8. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/socketsecurity/socketcli.py +36 -16
  9. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/tests/core/test_facts_compression.py +1 -1
  10. socketsecurity-2.7.0/tests/core/test_invalid_facts_marker.py +278 -0
  11. socketsecurity-2.7.0/tests/unit/test_pr_comment_rendering.py +304 -0
  12. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/tests/unit/test_socketcli.py +44 -1
  13. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/uv.lock +23 -23
  14. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/.github/CODEOWNERS +0 -0
  15. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/.github/PULL_REQUEST_TEMPLATE/bug-fix.md +0 -0
  16. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/.github/PULL_REQUEST_TEMPLATE/feature.md +0 -0
  17. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/.github/PULL_REQUEST_TEMPLATE/improvement.md +0 -0
  18. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  19. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/.github/actions/setup-docker/action.yml +0 -0
  20. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/.github/actions/setup-hatch/action.yml +0 -0
  21. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/.github/actions/setup-sfw/action.yml +0 -0
  22. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/.github/dependabot.yml +0 -0
  23. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/.github/workflows/dependency-review.yml +0 -0
  24. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/.github/workflows/docker-stable.yml +0 -0
  25. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/.github/workflows/e2e-test.yml +0 -0
  26. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/.github/workflows/package-check.yml +0 -0
  27. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/.github/workflows/pr-preview.yml +0 -0
  28. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/.github/workflows/python-tests.yml +0 -0
  29. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/.github/workflows/release.yml +0 -0
  30. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/.github/workflows/version-check.yml +0 -0
  31. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/.github/zizmor.yml +0 -0
  32. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/.gitignore +0 -0
  33. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/.hooks/sync_version.py +0 -0
  34. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/.pre-commit-config.yaml +0 -0
  35. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/.python-version +0 -0
  36. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/CONTRIBUTING.md +0 -0
  37. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/Dockerfile +0 -0
  38. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/Dockerfile.preview +0 -0
  39. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/LICENSE +0 -0
  40. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/Makefile +0 -0
  41. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/README.md +0 -0
  42. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/benchmarks/manifest_discovery.py +0 -0
  43. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/docs/ci-cd.md +0 -0
  44. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/docs/cli-reference.md +0 -0
  45. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/docs/development.md +0 -0
  46. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/docs/troubleshooting.md +0 -0
  47. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/examples/config/sarif-dashboard-parity.json +0 -0
  48. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/examples/config/sarif-dashboard-parity.toml +0 -0
  49. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/examples/config/sarif-diff-ci-cd.json +0 -0
  50. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/examples/config/sarif-diff-ci-cd.toml +0 -0
  51. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/examples/config/sarif-instance-detail.json +0 -0
  52. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/examples/config/sarif-instance-detail.toml +0 -0
  53. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/instructions/gitlab-commit-status/uat.md +0 -0
  54. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/pytest.ini +0 -0
  55. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/scripts/build_container.sh +0 -0
  56. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/scripts/build_container_flexible.sh +0 -0
  57. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/scripts/deploy-test-docker.sh +0 -0
  58. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/scripts/deploy-test-pypi.sh +0 -0
  59. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/scripts/docker-entrypoint.sh +0 -0
  60. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/scripts/run.sh +0 -0
  61. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/session.md +0 -0
  62. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/socket.yml +0 -0
  63. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/socketsecurity/config.py +0 -0
  64. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/socketsecurity/core/alert_selection.py +0 -0
  65. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/socketsecurity/core/classes.py +0 -0
  66. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/socketsecurity/core/cli_client.py +0 -0
  67. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/socketsecurity/core/cli_run.py +0 -0
  68. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/socketsecurity/core/exceptions.py +0 -0
  69. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/socketsecurity/core/git_interface.py +0 -0
  70. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/socketsecurity/core/helper/__init__.py +0 -0
  71. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/socketsecurity/core/helper/socket_facts_loader.py +0 -0
  72. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/socketsecurity/core/lazy_file_loader.py +0 -0
  73. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/socketsecurity/core/log_uploader.py +0 -0
  74. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/socketsecurity/core/logging.py +0 -0
  75. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/socketsecurity/core/resource_utils.py +0 -0
  76. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/socketsecurity/core/scm/__init__.py +0 -0
  77. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/socketsecurity/core/scm/base.py +0 -0
  78. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/socketsecurity/core/scm/client.py +0 -0
  79. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/socketsecurity/core/scm/github.py +0 -0
  80. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/socketsecurity/core/scm/gitlab.py +0 -0
  81. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/socketsecurity/core/socket_config.py +0 -0
  82. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/socketsecurity/core/streaming.py +0 -0
  83. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/socketsecurity/core/tools/reachability.py +0 -0
  84. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/socketsecurity/core/utils.py +0 -0
  85. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/socketsecurity/fossa_compat.py +0 -0
  86. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/socketsecurity/output.py +0 -0
  87. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/socketsecurity/plugins/__init__.py +0 -0
  88. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/socketsecurity/plugins/base.py +0 -0
  89. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/socketsecurity/plugins/formatters/__init__.py +0 -0
  90. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/socketsecurity/plugins/formatters/slack.py +0 -0
  91. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/socketsecurity/plugins/jira.py +0 -0
  92. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/socketsecurity/plugins/manager.py +0 -0
  93. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/socketsecurity/plugins/slack.py +0 -0
  94. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/socketsecurity/plugins/teams.py +0 -0
  95. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/socketsecurity/plugins/webhook.py +0 -0
  96. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/tests/__init__.py +0 -0
  97. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/tests/core/conftest.py +0 -0
  98. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/tests/core/create_diff_input.json +0 -0
  99. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/tests/core/test_diff_alerts.py +0 -0
  100. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/tests/core/test_diff_generation.py +0 -0
  101. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/tests/core/test_diff_scan_polling.py +0 -0
  102. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/tests/core/test_has_manifest_files.py +0 -0
  103. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/tests/core/test_package_and_alerts.py +0 -0
  104. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/tests/core/test_sdk_methods.py +0 -0
  105. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/tests/core/test_supporting_methods.py +0 -0
  106. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/tests/data/fullscans/create_response.json +0 -0
  107. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/tests/data/fullscans/diff/stream_diff.json +0 -0
  108. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/tests/data/fullscans/diff/stream_diff_full.json +0 -0
  109. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/tests/data/fullscans/head_scan/metadata.json +0 -0
  110. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/tests/data/fullscans/head_scan/stream_scan.json +0 -0
  111. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/tests/data/fullscans/head_scan/stream_scan_full.json +0 -0
  112. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/tests/data/fullscans/new_scan/metadata.json +0 -0
  113. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/tests/data/fullscans/new_scan/stream_scan.json +0 -0
  114. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/tests/data/repos/repo_info_error.json +0 -0
  115. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/tests/data/repos/repo_info_no_head.json +0 -0
  116. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/tests/data/repos/repo_info_success.json +0 -0
  117. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/tests/data/settings/security-policy.json +0 -0
  118. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/tests/e2e/fixtures/simple-npm/index.js +0 -0
  119. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/tests/e2e/fixtures/simple-npm/package.json +0 -0
  120. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/tests/e2e/fixtures/simple-pypi/requirements.txt +0 -0
  121. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/tests/e2e/reach-facts-probe.sh +0 -0
  122. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/tests/e2e/validate-gitlab.sh +0 -0
  123. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/tests/e2e/validate-json.sh +0 -0
  124. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/tests/e2e/validate-reachability.sh +0 -0
  125. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/tests/e2e/validate-sarif.sh +0 -0
  126. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/tests/e2e/validate-scan.sh +0 -0
  127. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/tests/fixtures/fossa/README.md +0 -0
  128. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/tests/fixtures/fossa/fossa-analyze-empty.json +0 -0
  129. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/tests/fixtures/fossa/fossa-analyze-populated.json +0 -0
  130. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/tests/fixtures/fossa/fossa-sbom-empty-deep.json +0 -0
  131. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/tests/fixtures/fossa/fossa-sbom-populated.json +0 -0
  132. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/tests/unit/__init__.py +0 -0
  133. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/tests/unit/test_alert_selection.py +0 -0
  134. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/tests/unit/test_cli_config.py +0 -0
  135. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/tests/unit/test_cli_run.py +0 -0
  136. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/tests/unit/test_client.py +0 -0
  137. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/tests/unit/test_config.py +0 -0
  138. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/tests/unit/test_dependency_overview.py +0 -0
  139. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/tests/unit/test_disable_ignore.py +0 -0
  140. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/tests/unit/test_exclude_paths.py +0 -0
  141. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/tests/unit/test_fossa_compat.py +0 -0
  142. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/tests/unit/test_fossa_parity.py +0 -0
  143. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/tests/unit/test_full_scan_retry.py +0 -0
  144. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/tests/unit/test_git_interface.py +0 -0
  145. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/tests/unit/test_github_buildkite_config.py +0 -0
  146. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/tests/unit/test_gitlab_auth.py +0 -0
  147. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/tests/unit/test_gitlab_auth_fallback.py +0 -0
  148. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/tests/unit/test_gitlab_commit_status.py +0 -0
  149. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/tests/unit/test_gitlab_format.py +0 -0
  150. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/tests/unit/test_ignore_telemetry_filtering.py +0 -0
  151. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/tests/unit/test_include_dirs.py +0 -0
  152. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/tests/unit/test_log_uploader.py +0 -0
  153. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/tests/unit/test_manifest_discovery.py +0 -0
  154. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/tests/unit/test_output.py +0 -0
  155. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/tests/unit/test_reachability.py +0 -0
  156. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/tests/unit/test_slack_plugin.py +0 -0
  157. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/tests/unit/test_streaming.py +0 -0
  158. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/tests/unit/test_tier1_finalize.py +0 -0
  159. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/workflows/bitbucket-pipelines.yml +0 -0
  160. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/workflows/buildkite.yml +0 -0
  161. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/workflows/github-actions.yml +0 -0
  162. {socketsecurity-2.6.11 → socketsecurity-2.7.0}/workflows/gitlab-ci.yml +0 -0
@@ -1,5 +1,56 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.7.0
4
+
5
+ ### Fixed: unreadable reachability facts no longer report a blocking package
6
+
7
+ - Scans with no supported manifest files uploaded a zero-byte `.socket.facts.json`
8
+ placeholder. The API cannot parse that, and answers by adding a
9
+ `generic/invalid-socket-facts@1.0.0` artifact to the scan, which the CLI then reported
10
+ as a new blocking package with no manifest file and no introducing dependency —
11
+ failing the run and, on pull requests, leaving a security comment that could not be
12
+ acted on. The placeholder is now an empty but well-formed facts document.
13
+ - When the API does report `generic/invalid-socket-facts` (a diagnostic for a facts file
14
+ it could not parse, not a real dependency), the CLI now excludes it from scan results
15
+ and logs a warning instead. It no longer blocks a run, appears in reports, or triggers
16
+ a pull request comment.
17
+ - Each placeholder is written to its own temporary directory. Two CLI runs sharing a
18
+ temporary directory previously used the same path and could remove each other's
19
+ placeholder mid-upload.
20
+
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
+
3
54
  ## 2.6.11
4
55
 
5
56
  ### Changed: bump pinned @coana-tech/cli to 15.10.32
@@ -31,7 +82,6 @@
31
82
  - Bumped the pinned reachability engine (`@coana-tech/cli`) from `15.10.23` to
32
83
  `15.10.25`. See the [Coana changelogs](https://docs.coana.tech/changelogs) for
33
84
  engine changes.
34
-
35
85
  ## 2.6.7
36
86
 
37
87
  ### 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.0
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
@@ -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.0"
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.0'
3
3
  USER_AGENT = f'SocketPythonCLI/{__version__}'
@@ -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:
@@ -107,6 +107,27 @@ def get_api_request_timeout(config: CliConfig) -> int:
107
107
  return config.timeout if config.timeout is not None else DEFAULT_API_TIMEOUT
108
108
 
109
109
 
110
+ def should_write_comment(disabled: bool, has_findings: bool, update_existing: bool) -> bool:
111
+ """
112
+ Decides whether a Socket comment should be written for this run.
113
+
114
+ :param disabled: bool - The comment type is switched off by flag.
115
+ :param has_findings: bool - There is something to report this run.
116
+ :param update_existing: bool - A comment of this type is already on the PR.
117
+ :return: bool - True when the comment should be posted or updated.
118
+ """
119
+ if disabled:
120
+ # The flag means the CLI does not manage this comment at all. Checking
121
+ # update_existing first only suppressed the very first post, so any
122
+ # comment already on the pull request kept being updated.
123
+ return False
124
+ if not has_findings:
125
+ # Nothing to report: refresh an existing comment so a resolved alerts
126
+ # table is cleared, but do not open a new one.
127
+ return update_existing
128
+ return True
129
+
130
+
110
131
  def build_socket_sdk(config: CliConfig) -> socketdev:
111
132
  cli_user_agent_string = f"SocketPythonCLI/{config.version}"
112
133
  return socketdev(
@@ -780,9 +801,6 @@ def main_code():
780
801
 
781
802
  security_comment = Messages.security_comment_template(diff, config)
782
803
 
783
- new_security_comment = True
784
- new_overview_comment = True
785
-
786
804
  update_old_security_comment = (
787
805
  security_comment is None or
788
806
  security_comment == "" or
@@ -795,20 +813,22 @@ def main_code():
795
813
  (len(comments) != 0 and comments.get("overview") is not None)
796
814
  )
797
815
 
798
- if len(diff.new_alerts) == 0 or config.disable_security_issue:
799
- if not update_old_security_comment:
800
- new_security_comment = False
801
- log.debug("No new alerts or security issue comment disabled")
802
- else:
803
- log.debug("Updated security comment with no new alerts")
804
-
816
+ new_security_comment = should_write_comment(
817
+ config.disable_security_issue,
818
+ len(diff.new_alerts) > 0,
819
+ update_old_security_comment,
820
+ )
821
+ if not new_security_comment:
822
+ log.debug("Security issue comment disabled, or no alerts and none to update")
823
+
805
824
  # FIXME: diff.new_packages is never populated, neither is removed_packages
806
- if (len(diff.new_packages) == 0) or config.disable_overview:
807
- if not update_old_overview_comment:
808
- new_overview_comment = False
809
- log.debug("No new/removed packages or Dependency Overview comment disabled")
810
- else:
811
- log.debug("Updated overview comment with no dependencies")
825
+ new_overview_comment = should_write_comment(
826
+ config.disable_overview,
827
+ len(diff.new_packages) > 0,
828
+ update_old_overview_comment,
829
+ )
830
+ if not new_overview_comment:
831
+ log.debug("Dependency Overview comment disabled, or no packages and none to update")
812
832
 
813
833
  log.debug(f"Adding comments for {config.scm}")
814
834
  scm.add_socket_comments(