socketsecurity 2.2.83__tar.gz → 2.2.85__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 (124) hide show
  1. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/PKG-INFO +1 -1
  2. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/pyproject.toml +1 -1
  3. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/socketsecurity/__init__.py +1 -1
  4. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/socketsecurity/config.py +32 -0
  5. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/socketsecurity/core/cli_client.py +16 -0
  6. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/socketsecurity/core/scm/gitlab.py +42 -0
  7. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/socketsecurity/core/scm_comments.py +5 -3
  8. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/socketsecurity/core/tools/reachability.py +16 -0
  9. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/socketsecurity/socketcli.py +123 -1
  10. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/tests/unit/test_client.py +50 -1
  11. socketsecurity-2.2.85/tests/unit/test_ignore_telemetry_filtering.py +237 -0
  12. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/uv.lock +1 -1
  13. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/.github/CODEOWNERS +0 -0
  14. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/.github/PULL_REQUEST_TEMPLATE/bug-fix.md +0 -0
  15. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/.github/PULL_REQUEST_TEMPLATE/feature.md +0 -0
  16. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/.github/PULL_REQUEST_TEMPLATE/improvement.md +0 -0
  17. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  18. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/.github/workflows/docker-stable.yml +0 -0
  19. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/.github/workflows/e2e-test.yml +0 -0
  20. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/.github/workflows/pr-preview.yml +0 -0
  21. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/.github/workflows/python-tests.yml +0 -0
  22. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/.github/workflows/release.yml +0 -0
  23. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/.github/workflows/version-check.yml +0 -0
  24. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/.github/zizmor.yml +0 -0
  25. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/.gitignore +0 -0
  26. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/.hooks/sync_version.py +0 -0
  27. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/.pre-commit-config.yaml +0 -0
  28. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/.python-version +0 -0
  29. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/CHANGELOG.md +0 -0
  30. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/Dockerfile +0 -0
  31. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/LICENSE +0 -0
  32. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/Makefile +0 -0
  33. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/README.md +0 -0
  34. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/docs/ci-cd.md +0 -0
  35. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/docs/cli-reference.md +0 -0
  36. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/docs/development.md +0 -0
  37. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/docs/troubleshooting.md +0 -0
  38. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/examples/config/sarif-dashboard-parity.json +0 -0
  39. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/examples/config/sarif-dashboard-parity.toml +0 -0
  40. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/examples/config/sarif-diff-ci-cd.json +0 -0
  41. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/examples/config/sarif-diff-ci-cd.toml +0 -0
  42. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/examples/config/sarif-instance-detail.json +0 -0
  43. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/examples/config/sarif-instance-detail.toml +0 -0
  44. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/instructions/gitlab-commit-status/uat.md +0 -0
  45. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/pytest.ini +0 -0
  46. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/scripts/build_container.sh +0 -0
  47. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/scripts/build_container_flexible.sh +0 -0
  48. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/scripts/deploy-test-docker.sh +0 -0
  49. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/scripts/deploy-test-pypi.sh +0 -0
  50. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/scripts/docker-entrypoint.sh +0 -0
  51. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/scripts/run.sh +0 -0
  52. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/session.md +0 -0
  53. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/socket.yml +0 -0
  54. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/socketsecurity/core/__init__.py +0 -0
  55. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/socketsecurity/core/alert_selection.py +0 -0
  56. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/socketsecurity/core/classes.py +0 -0
  57. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/socketsecurity/core/exceptions.py +0 -0
  58. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/socketsecurity/core/git_interface.py +0 -0
  59. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/socketsecurity/core/helper/__init__.py +0 -0
  60. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/socketsecurity/core/helper/socket_facts_loader.py +0 -0
  61. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/socketsecurity/core/lazy_file_loader.py +0 -0
  62. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/socketsecurity/core/logging.py +0 -0
  63. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/socketsecurity/core/messages.py +0 -0
  64. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/socketsecurity/core/resource_utils.py +0 -0
  65. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/socketsecurity/core/scm/__init__.py +0 -0
  66. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/socketsecurity/core/scm/base.py +0 -0
  67. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/socketsecurity/core/scm/client.py +0 -0
  68. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/socketsecurity/core/scm/github.py +0 -0
  69. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/socketsecurity/core/socket_config.py +0 -0
  70. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/socketsecurity/core/utils.py +0 -0
  71. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/socketsecurity/output.py +0 -0
  72. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/socketsecurity/plugins/__init__.py +0 -0
  73. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/socketsecurity/plugins/base.py +0 -0
  74. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/socketsecurity/plugins/formatters/__init__.py +0 -0
  75. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/socketsecurity/plugins/formatters/slack.py +0 -0
  76. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/socketsecurity/plugins/jira.py +0 -0
  77. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/socketsecurity/plugins/manager.py +0 -0
  78. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/socketsecurity/plugins/slack.py +0 -0
  79. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/socketsecurity/plugins/teams.py +0 -0
  80. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/socketsecurity/plugins/webhook.py +0 -0
  81. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/tests/__init__.py +0 -0
  82. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/tests/core/conftest.py +0 -0
  83. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/tests/core/create_diff_input.json +0 -0
  84. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/tests/core/test_diff_alerts.py +0 -0
  85. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/tests/core/test_diff_generation.py +0 -0
  86. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/tests/core/test_has_manifest_files.py +0 -0
  87. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/tests/core/test_package_and_alerts.py +0 -0
  88. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/tests/core/test_sdk_methods.py +0 -0
  89. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/tests/core/test_supporting_methods.py +0 -0
  90. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/tests/data/fullscans/create_response.json +0 -0
  91. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/tests/data/fullscans/diff/stream_diff.json +0 -0
  92. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/tests/data/fullscans/diff/stream_diff_full.json +0 -0
  93. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/tests/data/fullscans/head_scan/metadata.json +0 -0
  94. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/tests/data/fullscans/head_scan/stream_scan.json +0 -0
  95. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/tests/data/fullscans/head_scan/stream_scan_full.json +0 -0
  96. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/tests/data/fullscans/new_scan/metadata.json +0 -0
  97. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/tests/data/fullscans/new_scan/stream_scan.json +0 -0
  98. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/tests/data/repos/repo_info_error.json +0 -0
  99. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/tests/data/repos/repo_info_no_head.json +0 -0
  100. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/tests/data/repos/repo_info_success.json +0 -0
  101. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/tests/data/settings/security-policy.json +0 -0
  102. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/tests/e2e/fixtures/simple-npm/index.js +0 -0
  103. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/tests/e2e/fixtures/simple-npm/package.json +0 -0
  104. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/tests/e2e/fixtures/simple-pypi/requirements.txt +0 -0
  105. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/tests/e2e/validate-gitlab.sh +0 -0
  106. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/tests/e2e/validate-json.sh +0 -0
  107. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/tests/e2e/validate-reachability.sh +0 -0
  108. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/tests/e2e/validate-sarif.sh +0 -0
  109. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/tests/e2e/validate-scan.sh +0 -0
  110. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/tests/unit/__init__.py +0 -0
  111. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/tests/unit/test_alert_selection.py +0 -0
  112. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/tests/unit/test_cli_config.py +0 -0
  113. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/tests/unit/test_config.py +0 -0
  114. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/tests/unit/test_disable_ignore.py +0 -0
  115. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/tests/unit/test_gitlab_auth.py +0 -0
  116. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/tests/unit/test_gitlab_auth_fallback.py +0 -0
  117. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/tests/unit/test_gitlab_commit_status.py +0 -0
  118. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/tests/unit/test_gitlab_format.py +0 -0
  119. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/tests/unit/test_output.py +0 -0
  120. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/tests/unit/test_slack_plugin.py +0 -0
  121. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/workflows/bitbucket-pipelines.yml +0 -0
  122. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/workflows/buildkite.yml +0 -0
  123. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/workflows/github-actions.yml +0 -0
  124. {socketsecurity-2.2.83 → socketsecurity-2.2.85}/workflows/gitlab-ci.yml +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: socketsecurity
3
- Version: 2.2.83
3
+ Version: 2.2.85
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>
@@ -6,7 +6,7 @@ build-backend = "hatchling.build"
6
6
 
7
7
  [project]
8
8
  name = "socketsecurity"
9
- version = "2.2.83"
9
+ version = "2.2.85"
10
10
  requires-python = ">= 3.11"
11
11
  license = {"file" = "LICENSE"}
12
12
  dependencies = [
@@ -1,3 +1,3 @@
1
1
  __author__ = 'socket.dev'
2
- __version__ = '2.2.83'
2
+ __version__ = '2.2.85'
3
3
  USER_AGENT = f'SocketPythonCLI/{__version__}'
@@ -131,6 +131,10 @@ class CliConfig:
131
131
  reach_additional_params: Optional[List[str]] = None
132
132
  only_facts_file: bool = False
133
133
  reach_use_only_pregenerated_sboms: bool = False
134
+ reach_continue_on_analysis_errors: bool = False
135
+ reach_continue_on_install_errors: bool = False
136
+ reach_continue_on_missing_lock_files: bool = False
137
+ reach_continue_on_no_source_files: bool = False
134
138
  max_purl_batch_size: int = 5000
135
139
  enable_commit_status: bool = False
136
140
  config_file: Optional[str] = None
@@ -236,6 +240,10 @@ class CliConfig:
236
240
  'reach_additional_params': args.reach_additional_params,
237
241
  'only_facts_file': args.only_facts_file,
238
242
  'reach_use_only_pregenerated_sboms': args.reach_use_only_pregenerated_sboms,
243
+ 'reach_continue_on_analysis_errors': args.reach_continue_on_analysis_errors,
244
+ 'reach_continue_on_install_errors': args.reach_continue_on_install_errors,
245
+ 'reach_continue_on_missing_lock_files': args.reach_continue_on_missing_lock_files,
246
+ 'reach_continue_on_no_source_files': args.reach_continue_on_no_source_files,
239
247
  'max_purl_batch_size': args.max_purl_batch_size,
240
248
  'enable_commit_status': args.enable_commit_status,
241
249
  'config_file': args.config_file,
@@ -861,6 +869,30 @@ def create_argument_parser() -> argparse.ArgumentParser:
861
869
  action="store_true",
862
870
  help="When using this option, the scan is created based only on pre-generated CDX and SPDX files in your project. (requires --reach)"
863
871
  )
872
+ reachability_group.add_argument(
873
+ "--reach-continue-on-analysis-errors",
874
+ dest="reach_continue_on_analysis_errors",
875
+ action="store_true",
876
+ help=argparse.SUPPRESS
877
+ )
878
+ reachability_group.add_argument(
879
+ "--reach-continue-on-install-errors",
880
+ dest="reach_continue_on_install_errors",
881
+ action="store_true",
882
+ help=argparse.SUPPRESS
883
+ )
884
+ reachability_group.add_argument(
885
+ "--reach-continue-on-missing-lock-files",
886
+ dest="reach_continue_on_missing_lock_files",
887
+ action="store_true",
888
+ help=argparse.SUPPRESS
889
+ )
890
+ reachability_group.add_argument(
891
+ "--reach-continue-on-no-source-files",
892
+ dest="reach_continue_on_no_source_files",
893
+ action="store_true",
894
+ help=argparse.SUPPRESS
895
+ )
864
896
 
865
897
  parser.add_argument(
866
898
  '--version',
@@ -1,4 +1,5 @@
1
1
  import base64
2
+ import json
2
3
  import logging
3
4
  from typing import Dict, List, Optional, Union
4
5
 
@@ -55,3 +56,18 @@ class CliClient:
55
56
  except requests.exceptions.RequestException as e:
56
57
  logger.error(f"API request failed: {str(e)}")
57
58
  raise APIFailure(f"Request failed: {str(e)}")
59
+
60
+ def post_telemetry_events(self, org_slug: str, events: List[Dict]) -> None:
61
+ """Post telemetry events one at a time to the v0 telemetry API. Fire-and-forget — logs errors but never raises."""
62
+ logger.debug(f"Sending {len(events)} telemetry event(s) to v0/orgs/{org_slug}/telemetry")
63
+ for i, event in enumerate(events):
64
+ try:
65
+ logger.debug(f"Telemetry event {i+1}/{len(events)}: {json.dumps(event)}")
66
+ resp = self.request(
67
+ path=f"orgs/{org_slug}/telemetry",
68
+ method="POST",
69
+ payload=json.dumps(event),
70
+ )
71
+ logger.debug(f"Telemetry event {i+1}/{len(events)} sent: status={resp.status_code}")
72
+ except Exception as e:
73
+ logger.warning(f"Failed to send telemetry event {i+1}/{len(events)}: {e}")
@@ -1,3 +1,4 @@
1
+ import json
1
2
  import os
2
3
  import sys
3
4
  from dataclasses import dataclass
@@ -219,6 +220,24 @@ class Gitlab:
219
220
  base_url=self.config.api_url
220
221
  )
221
222
 
223
+ def has_thumbsup_reaction(self, comment_id: int) -> bool:
224
+ """Best-effort check for 'thumbsup' award emoji on a MR note."""
225
+ if not self.config.mr_project_id or not self.config.mr_iid:
226
+ return False
227
+ path = f"projects/{self.config.mr_project_id}/merge_requests/{self.config.mr_iid}/notes/{comment_id}/award_emoji"
228
+ try:
229
+ response = self._request_with_fallback(
230
+ path=path,
231
+ headers=self.config.headers,
232
+ base_url=self.config.api_url
233
+ )
234
+ for emoji in response.json():
235
+ if emoji.get("name") == "thumbsup":
236
+ return True
237
+ except Exception as e:
238
+ log.debug(f"Could not check award emoji for note {comment_id} (best effort): {e}")
239
+ return False
240
+
222
241
  def get_comments_for_pr(self) -> dict:
223
242
  log.debug(f"Getting Gitlab comments for Repo {self.config.repository} for PR {self.config.mr_iid}")
224
243
  path = f"projects/{self.config.mr_project_id}/merge_requests/{self.config.mr_iid}/notes"
@@ -326,9 +345,32 @@ class Gitlab:
326
345
  except Exception as e:
327
346
  log.error(f"Failed to set commit status: {e}")
328
347
 
348
+ def post_thumbsup_reaction(self, comment_id: int) -> None:
349
+ """Best-effort: add 'thumbsup' award emoji to a MR note."""
350
+ if not self.config.mr_project_id or not self.config.mr_iid:
351
+ return
352
+ path = f"projects/{self.config.mr_project_id}/merge_requests/{self.config.mr_iid}/notes/{comment_id}/award_emoji"
353
+ try:
354
+ headers = {**self.config.headers, "Content-Type": "application/json"}
355
+ self._request_with_fallback(
356
+ path=path,
357
+ payload=json.dumps({"name": "thumbsup"}),
358
+ method="POST",
359
+ headers=headers,
360
+ base_url=self.config.api_url
361
+ )
362
+ except Exception as e:
363
+ log.debug(f"Could not add thumbsup emoji to note {comment_id} (best effort): {e}")
364
+
365
+ def handle_ignore_reactions(self, comments: dict) -> None:
366
+ for comment in comments.get("ignore", []):
367
+ if "SocketSecurity ignore" in comment.body and not self.has_thumbsup_reaction(comment.id):
368
+ self.post_thumbsup_reaction(comment.id)
369
+
329
370
  def remove_comment_alerts(self, comments: dict):
330
371
  security_alert = comments.get("security")
331
372
  if security_alert is not None:
332
373
  # Type narrowing: after None check, mypy knows this is Comment
333
374
  new_body = Comments.process_security_comment(security_alert, comments)
375
+ self.handle_ignore_reactions(comments)
334
376
  self.update_comment(new_body, str(security_alert.id))
@@ -51,11 +51,13 @@ class Comments:
51
51
  for comment in comments["ignore"]:
52
52
  comment: Comment
53
53
  first_line = comment.body_list[0]
54
- if not ignore_all and "SocketSecurity ignore" in first_line:
54
+ if not ignore_all and "socketsecurity ignore" in first_line.lower():
55
55
  try:
56
56
  first_line = first_line.lstrip("@")
57
- _, command = first_line.split("SocketSecurity ")
58
- command = command.strip()
57
+ # Case-insensitive split: find "SocketSecurity " regardless of casing
58
+ lower_line = first_line.lower()
59
+ split_idx = lower_line.index("socketsecurity ") + len("socketsecurity ")
60
+ command = first_line[split_idx:].strip()
59
61
  if command == "ignore-all":
60
62
  ignore_all = True
61
63
  else:
@@ -104,6 +104,10 @@ class ReachabilityAnalyzer:
104
104
  allow_unverified: bool = False,
105
105
  enable_debug: bool = False,
106
106
  use_only_pregenerated_sboms: bool = False,
107
+ continue_on_analysis_errors: bool = False,
108
+ continue_on_install_errors: bool = False,
109
+ continue_on_missing_lock_files: bool = False,
110
+ continue_on_no_source_files: bool = False,
107
111
  ) -> Dict[str, Any]:
108
112
  """
109
113
  Run reachability analysis.
@@ -196,6 +200,18 @@ class ReachabilityAnalyzer:
196
200
  if use_only_pregenerated_sboms:
197
201
  cmd.append("--use-only-pregenerated-sboms")
198
202
 
203
+ if continue_on_analysis_errors:
204
+ cmd.append("--reach-continue-on-analysis-errors")
205
+
206
+ if continue_on_install_errors:
207
+ cmd.append("--reach-continue-on-install-errors")
208
+
209
+ if continue_on_missing_lock_files:
210
+ cmd.append("--reach-continue-on-missing-lock-files")
211
+
212
+ if continue_on_no_source_files:
213
+ cmd.append("--reach-continue-on-no-source-files")
214
+
199
215
  # Add any additional parameters provided by the user
200
216
  if additional_params:
201
217
  cmd.extend(additional_params)
@@ -4,6 +4,8 @@ import sys
4
4
  import traceback
5
5
  import shutil
6
6
  import warnings
7
+ from datetime import datetime, timezone
8
+ from uuid import uuid4
7
9
 
8
10
  from dotenv import load_dotenv
9
11
  from git import InvalidGitRepositoryError, NoSuchPathError
@@ -301,7 +303,11 @@ def main_code():
301
303
  additional_params=config.reach_additional_params,
302
304
  allow_unverified=config.allow_unverified,
303
305
  enable_debug=config.enable_debug,
304
- use_only_pregenerated_sboms=config.reach_use_only_pregenerated_sboms
306
+ use_only_pregenerated_sboms=config.reach_use_only_pregenerated_sboms,
307
+ continue_on_analysis_errors=config.reach_continue_on_analysis_errors,
308
+ continue_on_install_errors=config.reach_continue_on_install_errors,
309
+ continue_on_missing_lock_files=config.reach_continue_on_missing_lock_files,
310
+ continue_on_no_source_files=config.reach_continue_on_no_source_files,
305
311
  )
306
312
 
307
313
  log.info(f"Reachability analysis completed successfully")
@@ -478,6 +484,17 @@ def main_code():
478
484
 
479
485
  # Handle SCM-specific flows
480
486
  log.debug(f"Flow decision: scm={scm is not None}, force_diff_mode={force_diff_mode}, force_api_mode={force_api_mode}, enable_diff={config.enable_diff}")
487
+
488
+ def _is_unprocessed(c):
489
+ """Check if an ignore comment has not yet been marked with '+1' reaction.
490
+ For GitHub, reactions['+1'] is already in the comment response (no extra call).
491
+ For GitLab, has_thumbsup_reaction() makes a lazy API call per comment."""
492
+ if getattr(c, "reactions", {}).get("+1"):
493
+ return False
494
+ if hasattr(scm, "has_thumbsup_reaction") and scm.has_thumbsup_reaction(c.id):
495
+ return False
496
+ return True
497
+
481
498
  if scm is not None and scm.check_event_type() == "comment":
482
499
  # FIXME: This entire flow should be a separate command called "filter_ignored_alerts_in_comments"
483
500
  # It's not related to scanning or diff generation - it just:
@@ -489,6 +506,44 @@ def main_code():
489
506
 
490
507
  if not config.disable_ignore:
491
508
  comments = scm.get_comments_for_pr()
509
+
510
+ # Emit telemetry for ignore comments before +1 reaction is added.
511
+ # The +1 reaction (added by remove_comment_alerts) serves as the "processed" marker.
512
+ if "ignore" in comments:
513
+ unprocessed = [c for c in comments["ignore"] if _is_unprocessed(c)]
514
+ if unprocessed:
515
+ try:
516
+ events = []
517
+ for c in unprocessed:
518
+ single = {"ignore": [c]}
519
+ ignore_all, ignore_commands = Comments.get_ignore_options(single)
520
+ user = getattr(c, "user", None) or getattr(c, "author", None) or {}
521
+ now = datetime.now(timezone.utc).isoformat()
522
+ shared_fields = {
523
+ "event_kind": "user-action",
524
+ "client_action": "ignore",
525
+ "alert_action": "error",
526
+ "event_sender_created_at": now,
527
+ "vcs_provider": integration_type,
528
+ "owner": config.repo.split("/")[0] if "/" in config.repo else "",
529
+ "repo": config.repo,
530
+ "pr_number": pr_number,
531
+ "ignore_all": ignore_all,
532
+ "sender_name": user.get("login") or user.get("username", ""),
533
+ "sender_id": str(user.get("id", "")),
534
+ }
535
+ if ignore_commands:
536
+ for name, version in ignore_commands:
537
+ events.append({**shared_fields, "event_id": str(uuid4()), "artifact_input": f"{name}@{version}"})
538
+ elif ignore_all:
539
+ events.append({**shared_fields, "event_id": str(uuid4())})
540
+
541
+ if events:
542
+ log.debug(f"Ignore telemetry: {len(events)} events to send")
543
+ client.post_telemetry_events(org_slug, events)
544
+ except Exception as e:
545
+ log.warning(f"Failed to send ignore telemetry: {e}")
546
+
492
547
  log.debug("Removing comment alerts")
493
548
  scm.remove_comment_alerts(comments)
494
549
  else:
@@ -504,9 +559,76 @@ def main_code():
504
559
  # FIXME: this overwrites diff.new_alerts, which was previously populated by Core.create_issue_alerts
505
560
  if not config.disable_ignore:
506
561
  log.debug("Removing comment alerts")
562
+ alerts_before = list(diff.new_alerts)
507
563
  diff.new_alerts = Comments.remove_alerts(comments, diff.new_alerts)
564
+
565
+ ignored_alerts = [a for a in alerts_before if a not in diff.new_alerts]
566
+ # Emit telemetry per-comment so each event carries the comment author.
567
+ unprocessed_ignore = [
568
+ c for c in comments.get("ignore", [])
569
+ if _is_unprocessed(c)
570
+ ]
571
+ if ignored_alerts and unprocessed_ignore:
572
+ try:
573
+ events = []
574
+ now = datetime.now(timezone.utc).isoformat()
575
+ for c in unprocessed_ignore:
576
+ single = {"ignore": [c]}
577
+ c_ignore_all, c_ignore_commands = Comments.get_ignore_options(single)
578
+ user = getattr(c, "user", None) or getattr(c, "author", None) or {}
579
+ sender_name = user.get("login") or user.get("username", "")
580
+ sender_id = str(user.get("id", ""))
581
+
582
+ # Match this comment's targets to the actual ignored alerts
583
+ matched_alerts = []
584
+ if c_ignore_all:
585
+ matched_alerts = ignored_alerts
586
+ else:
587
+ for alert in ignored_alerts:
588
+ full_name = f"{alert.pkg_type}/{alert.pkg_name}"
589
+ purl = (full_name, alert.pkg_version)
590
+ purl_star = (full_name, "*")
591
+ if purl in c_ignore_commands or purl_star in c_ignore_commands:
592
+ matched_alerts.append(alert)
593
+
594
+ shared_fields = {
595
+ "event_kind": "user-action",
596
+ "client_action": "ignore",
597
+ "event_sender_created_at": now,
598
+ "vcs_provider": integration_type,
599
+ "owner": config.repo.split("/")[0] if "/" in config.repo else "",
600
+ "repo": config.repo,
601
+ "pr_number": pr_number,
602
+ "ignore_all": c_ignore_all,
603
+ "sender_name": sender_name,
604
+ "sender_id": sender_id,
605
+ }
606
+ if matched_alerts:
607
+ for alert in matched_alerts:
608
+ # Derive alert_action from the alert's resolved action flags
609
+ if getattr(alert, "error", False):
610
+ alert_action = "error"
611
+ elif getattr(alert, "warn", False):
612
+ alert_action = "warn"
613
+ elif getattr(alert, "monitor", False):
614
+ alert_action = "monitor"
615
+ else:
616
+ alert_action = "error"
617
+ events.append({**shared_fields, "alert_action": alert_action, "event_id": str(uuid4()), "artifact_purl": alert.purl})
618
+ elif c_ignore_all:
619
+ events.append({**shared_fields, "event_id": str(uuid4())})
620
+
621
+ if events:
622
+ client.post_telemetry_events(org_slug, events)
623
+
624
+ # Mark ignore comments as processed with +1 reaction
625
+ if hasattr(scm, "handle_ignore_reactions"):
626
+ scm.handle_ignore_reactions(comments)
627
+ except Exception as e:
628
+ log.warning(f"Failed to send ignore telemetry: {e}")
508
629
  else:
509
630
  log.info("Ignore commands disabled (--disable-ignore), all alerts will be reported")
631
+
510
632
  log.debug("Creating Dependency Overview Comment")
511
633
 
512
634
  overview_comment = Messages.dependency_overview_template(diff)
@@ -122,4 +122,53 @@ def test_request_with_payload(client):
122
122
 
123
123
  args, kwargs = mock_request.call_args
124
124
  assert kwargs['method'] == "POST"
125
- assert kwargs['data'] == payload
125
+ assert kwargs['data'] == payload
126
+
127
+
128
+ def test_post_telemetry_events_sends_individually(client):
129
+ """Test that telemetry events are posted one at a time to v0 API"""
130
+ import json
131
+
132
+ events = [
133
+ {"event_kind": "user-action", "client_action": "ignore_alerts", "artifact_purl": "pkg:npm/foo@1.0.0"},
134
+ {"event_kind": "user-action", "client_action": "ignore_alerts", "artifact_purl": "pkg:npm/bar@2.0.0"},
135
+ ]
136
+
137
+ with patch('requests.request') as mock_request:
138
+ mock_response = Mock()
139
+ mock_response.status_code = 201
140
+ mock_request.return_value = mock_response
141
+
142
+ client.post_telemetry_events("test-org", events)
143
+
144
+ assert mock_request.call_count == 2
145
+
146
+ first_call = mock_request.call_args_list[0]
147
+ assert first_call.kwargs['url'] == "https://api.socket.dev/v0/orgs/test-org/telemetry"
148
+ assert first_call.kwargs['method'] == "POST"
149
+ assert first_call.kwargs['data'] == json.dumps(events[0])
150
+
151
+ second_call = mock_request.call_args_list[1]
152
+ assert second_call.kwargs['data'] == json.dumps(events[1])
153
+
154
+
155
+ def test_post_telemetry_events_continues_on_failure(client):
156
+ """Test that a failed event does not prevent subsequent events from being sent"""
157
+ import json
158
+
159
+ events = [
160
+ {"event_kind": "user-action", "artifact_purl": "pkg:npm/foo@1.0.0"},
161
+ {"event_kind": "user-action", "artifact_purl": "pkg:npm/bar@2.0.0"},
162
+ ]
163
+
164
+ with patch('requests.request') as mock_request:
165
+ mock_response = Mock()
166
+ mock_response.status_code = 201
167
+ mock_request.side_effect = [
168
+ requests.exceptions.ConnectionError("timeout"),
169
+ mock_response,
170
+ ]
171
+
172
+ client.post_telemetry_events("test-org", events)
173
+
174
+ assert mock_request.call_count == 2
@@ -0,0 +1,237 @@
1
+ """Tests for the +1 reaction dedup logic used to filter ignore comments for telemetry."""
2
+
3
+ from unittest.mock import Mock
4
+
5
+ from socketsecurity.core.classes import Comment
6
+ from socketsecurity.core.scm_comments import Comments
7
+
8
+
9
+ def _make_comment(body: str, thumbs_up: int = 0, comment_id: int = 1, user: dict | None = None) -> Comment:
10
+ return Comment(
11
+ id=comment_id,
12
+ body=body,
13
+ body_list=body.split("\n"),
14
+ reactions={"+1": thumbs_up},
15
+ user=user or {"login": "test-user", "id": 123},
16
+ )
17
+
18
+
19
+ def _filter_unprocessed(comments: list[Comment], scm=None) -> list[Comment]:
20
+ """Mirrors the _is_unprocessed logic in socketcli.py."""
21
+ def _is_unprocessed(c):
22
+ if getattr(c, "reactions", {}).get("+1"):
23
+ return False
24
+ if hasattr(scm, "has_thumbsup_reaction") and scm.has_thumbsup_reaction(c.id):
25
+ return False
26
+ return True
27
+
28
+ return [c for c in comments if _is_unprocessed(c)]
29
+
30
+
31
+ class TestUnprocessedIgnoreFiltering:
32
+ def test_returns_comments_without_thumbsup(self):
33
+ comments = [
34
+ _make_comment("SocketSecurity ignore npm/lodash@4.17.21", thumbs_up=0, comment_id=1),
35
+ _make_comment("SocketSecurity ignore npm/express@4.18.2", thumbs_up=0, comment_id=2),
36
+ ]
37
+ result = _filter_unprocessed(comments)
38
+ assert len(result) == 2
39
+
40
+ def test_excludes_comments_with_thumbsup(self):
41
+ comments = [
42
+ _make_comment("SocketSecurity ignore npm/lodash@4.17.21", thumbs_up=1, comment_id=1),
43
+ _make_comment("SocketSecurity ignore npm/express@4.18.2", thumbs_up=0, comment_id=2),
44
+ ]
45
+ result = _filter_unprocessed(comments)
46
+ assert len(result) == 1
47
+ assert result[0].id == 2
48
+
49
+ def test_returns_empty_when_all_processed(self):
50
+ comments = [
51
+ _make_comment("SocketSecurity ignore npm/lodash@4.17.21", thumbs_up=1, comment_id=1),
52
+ _make_comment("SocketSecurity ignore-all", thumbs_up=2, comment_id=2),
53
+ ]
54
+ result = _filter_unprocessed(comments)
55
+ assert len(result) == 0
56
+
57
+ def test_handles_missing_reactions_attr(self):
58
+ c = Comment(id=1, body="SocketSecurity ignore npm/foo@1.0.0", body_list=["SocketSecurity ignore npm/foo@1.0.0"])
59
+ # No reactions attribute set at all
60
+ result = _filter_unprocessed([c])
61
+ assert len(result) == 1
62
+
63
+ def test_handles_empty_reactions_dict(self):
64
+ c = _make_comment("SocketSecurity ignore npm/foo@1.0.0", comment_id=1)
65
+ c.reactions = {}
66
+ result = _filter_unprocessed([c])
67
+ assert len(result) == 1
68
+
69
+ def test_handles_reactions_with_thumbsup_zero(self):
70
+ c = _make_comment("SocketSecurity ignore npm/foo@1.0.0", thumbs_up=0, comment_id=1)
71
+ result = _filter_unprocessed([c])
72
+ assert len(result) == 1
73
+
74
+
75
+ class TestUnprocessedIgnoreFilteringWithScmFallback:
76
+ """Tests for the has_thumbsup_reaction fallback path (GitLab)."""
77
+
78
+ def test_scm_fallback_excludes_processed_comments(self):
79
+ """When inline reactions['+1'] is 0 but scm says it has thumbsup, exclude it."""
80
+ comments = [
81
+ _make_comment("SocketSecurity ignore npm/lodash@4.17.21", thumbs_up=0, comment_id=1),
82
+ _make_comment("SocketSecurity ignore npm/express@4.18.2", thumbs_up=0, comment_id=2),
83
+ ]
84
+ scm = Mock()
85
+ scm.has_thumbsup_reaction = Mock(side_effect=lambda cid: cid == 1)
86
+
87
+ result = _filter_unprocessed(comments, scm=scm)
88
+ assert len(result) == 1
89
+ assert result[0].id == 2
90
+
91
+ def test_scm_fallback_not_called_when_inline_thumbsup_present(self):
92
+ """When inline reactions['+1'] is truthy, scm.has_thumbsup_reaction should not be called."""
93
+ comments = [
94
+ _make_comment("SocketSecurity ignore npm/lodash@4.17.21", thumbs_up=1, comment_id=1),
95
+ ]
96
+ scm = Mock()
97
+ scm.has_thumbsup_reaction = Mock(return_value=False)
98
+
99
+ result = _filter_unprocessed(comments, scm=scm)
100
+ assert len(result) == 0
101
+ scm.has_thumbsup_reaction.assert_not_called()
102
+
103
+ def test_scm_without_has_thumbsup_reaction_skips_fallback(self):
104
+ """When scm doesn't have has_thumbsup_reaction (e.g. GitHub), only inline check runs."""
105
+ comments = [
106
+ _make_comment("SocketSecurity ignore npm/foo@1.0.0", thumbs_up=0, comment_id=1),
107
+ ]
108
+ scm = Mock(spec=[]) # no methods at all
109
+
110
+ result = _filter_unprocessed(comments, scm=scm)
111
+ assert len(result) == 1
112
+
113
+ def test_scm_fallback_returns_all_unprocessed(self):
114
+ """When scm says none have thumbsup, all are returned."""
115
+ comments = [
116
+ _make_comment("SocketSecurity ignore npm/foo@1.0.0", thumbs_up=0, comment_id=1),
117
+ _make_comment("SocketSecurity ignore npm/bar@2.0.0", thumbs_up=0, comment_id=2),
118
+ ]
119
+ scm = Mock()
120
+ scm.has_thumbsup_reaction = Mock(return_value=False)
121
+
122
+ result = _filter_unprocessed(comments, scm=scm)
123
+ assert len(result) == 2
124
+
125
+
126
+ class TestUnprocessedIgnoreFilteringWithCommentsParsing:
127
+ """Integration: filter unprocessed comments, then parse ignore options."""
128
+
129
+ def test_only_new_artifacts_are_parsed(self):
130
+ comments = [
131
+ _make_comment("SocketSecurity ignore npm/lodash@4.17.21", thumbs_up=1, comment_id=1),
132
+ _make_comment("SocketSecurity ignore npm/express@4.18.2", thumbs_up=0, comment_id=2),
133
+ _make_comment("SocketSecurity ignore npm/axios@1.6.0", thumbs_up=0, comment_id=3),
134
+ ]
135
+ unprocessed = _filter_unprocessed(comments)
136
+ unprocessed_comments = {"ignore": unprocessed}
137
+ ignore_all, ignore_commands = Comments.get_ignore_options(unprocessed_comments)
138
+
139
+ assert not ignore_all
140
+ assert ("npm/express", "4.18.2") in ignore_commands
141
+ assert ("npm/axios", "1.6.0") in ignore_commands
142
+ assert ("npm/lodash", "4.17.21") not in ignore_commands
143
+
144
+ def test_ignore_all_from_unprocessed(self):
145
+ comments = [
146
+ _make_comment("SocketSecurity ignore npm/lodash@4.17.21", thumbs_up=1, comment_id=1),
147
+ _make_comment("SocketSecurity ignore-all", thumbs_up=0, comment_id=2),
148
+ ]
149
+ unprocessed = _filter_unprocessed(comments)
150
+ unprocessed_comments = {"ignore": unprocessed}
151
+ ignore_all, ignore_commands = Comments.get_ignore_options(unprocessed_comments)
152
+
153
+ assert ignore_all
154
+
155
+ def test_no_unprocessed_means_no_telemetry(self):
156
+ comments = [
157
+ _make_comment("SocketSecurity ignore npm/lodash@4.17.21", thumbs_up=1, comment_id=1),
158
+ _make_comment("SocketSecurity ignore npm/express@4.18.2", thumbs_up=2, comment_id=2),
159
+ ]
160
+ unprocessed = _filter_unprocessed(comments)
161
+ assert len(unprocessed) == 0
162
+
163
+
164
+ def _build_event(comment, ignore_all=False, ignore_commands=None, artifact_input=None, artifact_purl=None):
165
+ """Mirrors the event construction logic in socketcli.py."""
166
+ from datetime import datetime, timezone
167
+ from uuid import uuid4
168
+
169
+ user = getattr(comment, "user", None) or getattr(comment, "author", None) or {}
170
+ shared_fields = {
171
+ "event_kind": "user-action",
172
+ "client_action": "ignore",
173
+ "alert_action": "error",
174
+ "event_sender_created_at": datetime.now(timezone.utc).isoformat(),
175
+ "vcs_provider": "github",
176
+ "owner": "test-owner",
177
+ "repo": "test-owner/test-repo",
178
+ "pr_number": 1,
179
+ "ignore_all": ignore_all,
180
+ "sender_name": user.get("login") or user.get("username", ""),
181
+ "sender_id": str(user.get("id", "")),
182
+ }
183
+ if artifact_input:
184
+ return {**shared_fields, "event_id": str(uuid4()), "artifact_input": artifact_input}
185
+ if artifact_purl:
186
+ return {**shared_fields, "event_id": str(uuid4()), "artifact_purl": artifact_purl}
187
+ return {**shared_fields, "event_id": str(uuid4())}
188
+
189
+
190
+ class TestTelemetryEventPayloadShape:
191
+ """Tests that telemetry event payloads contain the required fields."""
192
+
193
+ def test_per_artifact_event_has_required_fields(self):
194
+ c = _make_comment("SocketSecurity ignore npm/lodash@4.17.21", user={"login": "alice", "id": 1})
195
+ event = _build_event(c, artifact_input="npm/lodash@4.17.21")
196
+
197
+ assert event["event_kind"] == "user-action"
198
+ assert event["client_action"] == "ignore"
199
+ assert event["alert_action"] == "error"
200
+ assert event["vcs_provider"] == "github"
201
+ assert event["sender_name"] == "alice"
202
+ assert event["sender_id"] == "1"
203
+ assert event["artifact_input"] == "npm/lodash@4.17.21"
204
+ assert "event_id" in event
205
+ assert "event_sender_created_at" in event
206
+
207
+ def test_ignore_all_event_has_required_fields(self):
208
+ c = _make_comment("SocketSecurity ignore-all", user={"login": "bob", "id": 2})
209
+ event = _build_event(c, ignore_all=True)
210
+
211
+ assert event["event_kind"] == "user-action"
212
+ assert event["client_action"] == "ignore"
213
+ assert event["alert_action"] == "error"
214
+ assert event["ignore_all"] is True
215
+ assert event["sender_name"] == "bob"
216
+ assert "artifact_input" not in event
217
+ assert "artifact_purl" not in event
218
+
219
+ def test_push_flow_event_uses_artifact_purl(self):
220
+ c = _make_comment("SocketSecurity ignore npm/lodash@4.17.21", user={"login": "alice", "id": 1})
221
+ event = _build_event(c, artifact_purl="pkg:npm/lodash@4.17.21")
222
+
223
+ assert event["artifact_purl"] == "pkg:npm/lodash@4.17.21"
224
+ assert event["alert_action"] == "error"
225
+ assert "artifact_input" not in event
226
+
227
+ def test_gitlab_author_populates_sender(self):
228
+ c = Comment(
229
+ id=1, body="SocketSecurity ignore npm/foo@1.0.0",
230
+ body_list=["SocketSecurity ignore npm/foo@1.0.0"],
231
+ reactions={"+1": 0},
232
+ author={"username": "gitlab-dev", "id": 42},
233
+ )
234
+ event = _build_event(c, artifact_input="npm/foo@1.0.0")
235
+
236
+ assert event["sender_name"] == "gitlab-dev"
237
+ assert event["sender_id"] == "42"
@@ -1168,7 +1168,7 @@ wheels = [
1168
1168
 
1169
1169
  [[package]]
1170
1170
  name = "socketsecurity"
1171
- version = "2.2.82"
1171
+ version = "2.2.84"
1172
1172
  source = { editable = "." }
1173
1173
  dependencies = [
1174
1174
  { name = "bs4" },
File without changes