socketsecurity 2.2.83__tar.gz → 2.2.86__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.
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/PKG-INFO +2 -2
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/pyproject.toml +2 -2
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/socketsecurity/__init__.py +1 -1
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/socketsecurity/config.py +32 -0
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/socketsecurity/core/classes.py +4 -2
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/socketsecurity/core/cli_client.py +16 -0
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/socketsecurity/core/messages.py +20 -5
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/socketsecurity/core/scm/gitlab.py +42 -0
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/socketsecurity/core/scm_comments.py +5 -3
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/socketsecurity/core/tools/reachability.py +16 -0
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/socketsecurity/socketcli.py +123 -1
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/tests/unit/test_client.py +50 -1
- socketsecurity-2.2.86/tests/unit/test_dependency_overview.py +67 -0
- socketsecurity-2.2.86/tests/unit/test_ignore_telemetry_filtering.py +237 -0
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/uv.lock +5 -5
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/.github/CODEOWNERS +0 -0
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/.github/PULL_REQUEST_TEMPLATE/bug-fix.md +0 -0
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/.github/PULL_REQUEST_TEMPLATE/feature.md +0 -0
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/.github/PULL_REQUEST_TEMPLATE/improvement.md +0 -0
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/.github/workflows/docker-stable.yml +0 -0
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/.github/workflows/e2e-test.yml +0 -0
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/.github/workflows/pr-preview.yml +0 -0
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/.github/workflows/python-tests.yml +0 -0
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/.github/workflows/release.yml +0 -0
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/.github/workflows/version-check.yml +0 -0
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/.github/zizmor.yml +0 -0
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/.gitignore +0 -0
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/.hooks/sync_version.py +0 -0
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/.pre-commit-config.yaml +0 -0
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/.python-version +0 -0
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/CHANGELOG.md +0 -0
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/Dockerfile +0 -0
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/LICENSE +0 -0
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/Makefile +0 -0
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/README.md +0 -0
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/docs/ci-cd.md +0 -0
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/docs/cli-reference.md +0 -0
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/docs/development.md +0 -0
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/docs/troubleshooting.md +0 -0
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/examples/config/sarif-dashboard-parity.json +0 -0
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/examples/config/sarif-dashboard-parity.toml +0 -0
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/examples/config/sarif-diff-ci-cd.json +0 -0
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/examples/config/sarif-diff-ci-cd.toml +0 -0
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/examples/config/sarif-instance-detail.json +0 -0
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/examples/config/sarif-instance-detail.toml +0 -0
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/instructions/gitlab-commit-status/uat.md +0 -0
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/pytest.ini +0 -0
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/scripts/build_container.sh +0 -0
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/scripts/build_container_flexible.sh +0 -0
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/scripts/deploy-test-docker.sh +0 -0
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/scripts/deploy-test-pypi.sh +0 -0
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/scripts/docker-entrypoint.sh +0 -0
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/scripts/run.sh +0 -0
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/session.md +0 -0
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/socket.yml +0 -0
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/socketsecurity/core/__init__.py +0 -0
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/socketsecurity/core/alert_selection.py +0 -0
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/socketsecurity/core/exceptions.py +0 -0
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/socketsecurity/core/git_interface.py +0 -0
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/socketsecurity/core/helper/__init__.py +0 -0
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/socketsecurity/core/helper/socket_facts_loader.py +0 -0
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/socketsecurity/core/lazy_file_loader.py +0 -0
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/socketsecurity/core/logging.py +0 -0
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/socketsecurity/core/resource_utils.py +0 -0
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/socketsecurity/core/scm/__init__.py +0 -0
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/socketsecurity/core/scm/base.py +0 -0
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/socketsecurity/core/scm/client.py +0 -0
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/socketsecurity/core/scm/github.py +0 -0
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/socketsecurity/core/socket_config.py +0 -0
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/socketsecurity/core/utils.py +0 -0
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/socketsecurity/output.py +0 -0
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/socketsecurity/plugins/__init__.py +0 -0
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/socketsecurity/plugins/base.py +0 -0
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/socketsecurity/plugins/formatters/__init__.py +0 -0
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/socketsecurity/plugins/formatters/slack.py +0 -0
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/socketsecurity/plugins/jira.py +0 -0
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/socketsecurity/plugins/manager.py +0 -0
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/socketsecurity/plugins/slack.py +0 -0
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/socketsecurity/plugins/teams.py +0 -0
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/socketsecurity/plugins/webhook.py +0 -0
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/tests/__init__.py +0 -0
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/tests/core/conftest.py +0 -0
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/tests/core/create_diff_input.json +0 -0
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/tests/core/test_diff_alerts.py +0 -0
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/tests/core/test_diff_generation.py +0 -0
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/tests/core/test_has_manifest_files.py +0 -0
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/tests/core/test_package_and_alerts.py +0 -0
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/tests/core/test_sdk_methods.py +0 -0
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/tests/core/test_supporting_methods.py +0 -0
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/tests/data/fullscans/create_response.json +0 -0
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/tests/data/fullscans/diff/stream_diff.json +0 -0
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/tests/data/fullscans/diff/stream_diff_full.json +0 -0
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/tests/data/fullscans/head_scan/metadata.json +0 -0
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/tests/data/fullscans/head_scan/stream_scan.json +0 -0
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/tests/data/fullscans/head_scan/stream_scan_full.json +0 -0
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/tests/data/fullscans/new_scan/metadata.json +0 -0
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/tests/data/fullscans/new_scan/stream_scan.json +0 -0
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/tests/data/repos/repo_info_error.json +0 -0
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/tests/data/repos/repo_info_no_head.json +0 -0
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/tests/data/repos/repo_info_success.json +0 -0
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/tests/data/settings/security-policy.json +0 -0
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/tests/e2e/fixtures/simple-npm/index.js +0 -0
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/tests/e2e/fixtures/simple-npm/package.json +0 -0
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/tests/e2e/fixtures/simple-pypi/requirements.txt +0 -0
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/tests/e2e/validate-gitlab.sh +0 -0
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/tests/e2e/validate-json.sh +0 -0
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/tests/e2e/validate-reachability.sh +0 -0
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/tests/e2e/validate-sarif.sh +0 -0
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/tests/e2e/validate-scan.sh +0 -0
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/tests/unit/__init__.py +0 -0
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/tests/unit/test_alert_selection.py +0 -0
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/tests/unit/test_cli_config.py +0 -0
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/tests/unit/test_config.py +0 -0
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/tests/unit/test_disable_ignore.py +0 -0
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/tests/unit/test_gitlab_auth.py +0 -0
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/tests/unit/test_gitlab_auth_fallback.py +0 -0
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/tests/unit/test_gitlab_commit_status.py +0 -0
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/tests/unit/test_gitlab_format.py +0 -0
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/tests/unit/test_output.py +0 -0
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/tests/unit/test_slack_plugin.py +0 -0
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/workflows/bitbucket-pipelines.yml +0 -0
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/workflows/buildkite.yml +0 -0
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/workflows/github-actions.yml +0 -0
- {socketsecurity-2.2.83 → socketsecurity-2.2.86}/workflows/gitlab-ci.yml +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: socketsecurity
|
|
3
|
-
Version: 2.2.
|
|
3
|
+
Version: 2.2.86
|
|
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>
|
|
@@ -41,7 +41,7 @@ Requires-Dist: packaging
|
|
|
41
41
|
Requires-Dist: prettytable
|
|
42
42
|
Requires-Dist: python-dotenv
|
|
43
43
|
Requires-Dist: requests
|
|
44
|
-
Requires-Dist: socketdev<4.0.0,>=3.0.
|
|
44
|
+
Requires-Dist: socketdev<4.0.0,>=3.0.33
|
|
45
45
|
Provides-Extra: dev
|
|
46
46
|
Requires-Dist: hatch; extra == 'dev'
|
|
47
47
|
Requires-Dist: pre-commit; extra == 'dev'
|
|
@@ -6,7 +6,7 @@ build-backend = "hatchling.build"
|
|
|
6
6
|
|
|
7
7
|
[project]
|
|
8
8
|
name = "socketsecurity"
|
|
9
|
-
version = "2.2.
|
|
9
|
+
version = "2.2.86"
|
|
10
10
|
requires-python = ">= 3.11"
|
|
11
11
|
license = {"file" = "LICENSE"}
|
|
12
12
|
dependencies = [
|
|
@@ -16,7 +16,7 @@ dependencies = [
|
|
|
16
16
|
'GitPython',
|
|
17
17
|
'packaging',
|
|
18
18
|
'python-dotenv',
|
|
19
|
-
"socketdev>=3.0.
|
|
19
|
+
"socketdev>=3.0.33,<4.0.0",
|
|
20
20
|
"bs4>=0.0.2",
|
|
21
21
|
"markdown>=3.10",
|
|
22
22
|
]
|
|
@@ -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',
|
|
@@ -207,7 +207,7 @@ class Package():
|
|
|
207
207
|
name=data["name"],
|
|
208
208
|
version=data["version"],
|
|
209
209
|
type=data["type"],
|
|
210
|
-
score=data.get("score"
|
|
210
|
+
score=data.get("score") or data.get("scores") or {},
|
|
211
211
|
alerts=data.get("alerts", []),
|
|
212
212
|
author=data.get("author", []),
|
|
213
213
|
size=data.get("size"),
|
|
@@ -236,7 +236,7 @@ class Package():
|
|
|
236
236
|
name=data["name"],
|
|
237
237
|
version=data["version"],
|
|
238
238
|
type=data["type"],
|
|
239
|
-
score=data.get("score"
|
|
239
|
+
score=data.get("score") or data.get("scores") or {},
|
|
240
240
|
alerts=data.get("alerts", []),
|
|
241
241
|
author=data.get("author", []),
|
|
242
242
|
size=data.get("size"),
|
|
@@ -448,6 +448,8 @@ class Purl:
|
|
|
448
448
|
self.capabilities = []
|
|
449
449
|
if not hasattr(self, "is_new"):
|
|
450
450
|
self.is_new = False
|
|
451
|
+
if not hasattr(self, "scores") or self.scores is None:
|
|
452
|
+
self.scores = {}
|
|
451
453
|
self.author_url = Purl.generate_author_data(self.author, self.ecosystem)
|
|
452
454
|
|
|
453
455
|
@staticmethod
|
|
@@ -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}")
|
|
@@ -1179,12 +1179,27 @@ class Messages:
|
|
|
1179
1179
|
score_percent = int(score * 100) # Convert to integer percentage
|
|
1180
1180
|
return f"[]({added.url})"
|
|
1181
1181
|
|
|
1182
|
+
def get_score_for_badge(score_name: str) -> float:
|
|
1183
|
+
scores = getattr(added, "scores", None)
|
|
1184
|
+
if isinstance(scores, dict):
|
|
1185
|
+
raw_score = scores.get(score_name)
|
|
1186
|
+
else:
|
|
1187
|
+
raw_score = getattr(scores, score_name, None) if scores is not None else None
|
|
1188
|
+
|
|
1189
|
+
if raw_score is None:
|
|
1190
|
+
return 1.0
|
|
1191
|
+
|
|
1192
|
+
score = float(raw_score)
|
|
1193
|
+
if score > 1:
|
|
1194
|
+
score = score / 100
|
|
1195
|
+
return max(0.0, min(score, 1.0))
|
|
1196
|
+
|
|
1182
1197
|
# Generate badges for each score type
|
|
1183
|
-
supply_chain_risk_badge = score_to_badge(
|
|
1184
|
-
vulnerability_badge = score_to_badge(
|
|
1185
|
-
quality_badge = score_to_badge(
|
|
1186
|
-
maintenance_badge = score_to_badge(
|
|
1187
|
-
license_badge = score_to_badge(
|
|
1198
|
+
supply_chain_risk_badge = score_to_badge(get_score_for_badge("supplyChain"))
|
|
1199
|
+
vulnerability_badge = score_to_badge(get_score_for_badge("vulnerability"))
|
|
1200
|
+
quality_badge = score_to_badge(get_score_for_badge("quality"))
|
|
1201
|
+
maintenance_badge = score_to_badge(get_score_for_badge("maintenance"))
|
|
1202
|
+
license_badge = score_to_badge(get_score_for_badge("license"))
|
|
1188
1203
|
|
|
1189
1204
|
# Add the row for this package
|
|
1190
1205
|
row = [
|
|
@@ -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 "
|
|
54
|
+
if not ignore_all and "socketsecurity ignore" in first_line.lower():
|
|
55
55
|
try:
|
|
56
56
|
first_line = first_line.lstrip("@")
|
|
57
|
-
|
|
58
|
-
|
|
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,67 @@
|
|
|
1
|
+
from socketsecurity.core.classes import Diff, Package, Purl
|
|
2
|
+
from socketsecurity.core.messages import Messages
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def _make_purl(name: str, scores) -> Purl:
|
|
6
|
+
return Purl(
|
|
7
|
+
id=f"pkg:npm/{name}@1.0.0",
|
|
8
|
+
name=name,
|
|
9
|
+
version="1.0.0",
|
|
10
|
+
ecosystem="npm",
|
|
11
|
+
direct=True,
|
|
12
|
+
introduced_by=[("direct", "package.json")],
|
|
13
|
+
author=["test-author"],
|
|
14
|
+
size=1000,
|
|
15
|
+
transitives=0,
|
|
16
|
+
url=f"https://socket.dev/npm/package/{name}/overview/1.0.0",
|
|
17
|
+
purl=f"pkg:npm/{name}@1.0.0",
|
|
18
|
+
scores=scores,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_package_from_diff_artifact_normalizes_null_score():
|
|
23
|
+
package = Package.from_diff_artifact(
|
|
24
|
+
{
|
|
25
|
+
"id": "pkg:npm/example@1.0.0",
|
|
26
|
+
"name": "example",
|
|
27
|
+
"version": "1.0.0",
|
|
28
|
+
"type": "npm",
|
|
29
|
+
"diffType": "added",
|
|
30
|
+
"score": None,
|
|
31
|
+
"alerts": [],
|
|
32
|
+
"author": [],
|
|
33
|
+
"topLevelAncestors": [],
|
|
34
|
+
"direct": True,
|
|
35
|
+
"manifestFiles": [],
|
|
36
|
+
}
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
assert package.score == {}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_dependency_overview_template_defaults_missing_or_null_scores(tmp_path, monkeypatch):
|
|
43
|
+
monkeypatch.chdir(tmp_path)
|
|
44
|
+
|
|
45
|
+
diff = Diff(
|
|
46
|
+
id="test-diff",
|
|
47
|
+
diff_url="https://socket.dev/test-diff",
|
|
48
|
+
new_packages=[
|
|
49
|
+
_make_purl("missing-scores", None),
|
|
50
|
+
_make_purl(
|
|
51
|
+
"partial-scores",
|
|
52
|
+
{
|
|
53
|
+
"supplyChain": 0.42,
|
|
54
|
+
"vulnerability": None,
|
|
55
|
+
},
|
|
56
|
+
),
|
|
57
|
+
],
|
|
58
|
+
removed_packages=[],
|
|
59
|
+
new_alerts=[],
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
comment = Messages.dependency_overview_template(diff)
|
|
63
|
+
|
|
64
|
+
assert "Socket Security: Dependency Overview" in comment
|
|
65
|
+
assert "score-42.svg" in comment
|
|
66
|
+
assert "score-100.svg" in comment
|
|
67
|
+
assert "score-10000.svg" not in comment
|