socketsecurity 2.2.59__tar.gz → 2.2.60__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.59 → socketsecurity-2.2.60}/PKG-INFO +40 -10
- {socketsecurity-2.2.59 → socketsecurity-2.2.60}/README.md +37 -8
- {socketsecurity-2.2.59 → socketsecurity-2.2.60}/pyproject.toml +3 -2
- {socketsecurity-2.2.59 → socketsecurity-2.2.60}/socketsecurity/__init__.py +1 -1
- {socketsecurity-2.2.59 → socketsecurity-2.2.60}/socketsecurity/config.py +27 -5
- socketsecurity-2.2.60/socketsecurity/core/helper/socket_facts_loader.py +387 -0
- socketsecurity-2.2.60/socketsecurity/plugins/formatters/__init__.py +5 -0
- socketsecurity-2.2.60/socketsecurity/plugins/formatters/slack.py +272 -0
- socketsecurity-2.2.60/socketsecurity/plugins/slack.py +491 -0
- {socketsecurity-2.2.59 → socketsecurity-2.2.60}/socketsecurity/socketcli.py +3 -3
- {socketsecurity-2.2.59 → socketsecurity-2.2.60}/uv.lock +285 -274
- socketsecurity-2.2.59/socketsecurity/plugins/slack.py +0 -95
- {socketsecurity-2.2.59 → socketsecurity-2.2.60}/.github/CODEOWNERS +0 -0
- {socketsecurity-2.2.59 → socketsecurity-2.2.60}/.github/PULL_REQUEST_TEMPLATE/bug-fix.md +0 -0
- {socketsecurity-2.2.59 → socketsecurity-2.2.60}/.github/PULL_REQUEST_TEMPLATE/feature.md +0 -0
- {socketsecurity-2.2.59 → socketsecurity-2.2.60}/.github/PULL_REQUEST_TEMPLATE/improvement.md +0 -0
- {socketsecurity-2.2.59 → socketsecurity-2.2.60}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {socketsecurity-2.2.59 → socketsecurity-2.2.60}/.github/workflows/docker-stable.yml +0 -0
- {socketsecurity-2.2.59 → socketsecurity-2.2.60}/.github/workflows/pr-preview.yml +0 -0
- {socketsecurity-2.2.59 → socketsecurity-2.2.60}/.github/workflows/release.yml +0 -0
- {socketsecurity-2.2.59 → socketsecurity-2.2.60}/.github/workflows/version-check.yml +0 -0
- {socketsecurity-2.2.59 → socketsecurity-2.2.60}/.gitignore +0 -0
- {socketsecurity-2.2.59 → socketsecurity-2.2.60}/.hooks/sync_version.py +0 -0
- {socketsecurity-2.2.59 → socketsecurity-2.2.60}/.pre-commit-config.yaml +0 -0
- {socketsecurity-2.2.59 → socketsecurity-2.2.60}/.python-version +0 -0
- {socketsecurity-2.2.59 → socketsecurity-2.2.60}/CHANGELOG.md +0 -0
- {socketsecurity-2.2.59 → socketsecurity-2.2.60}/Dockerfile +0 -0
- {socketsecurity-2.2.59 → socketsecurity-2.2.60}/LICENSE +0 -0
- {socketsecurity-2.2.59 → socketsecurity-2.2.60}/Makefile +0 -0
- {socketsecurity-2.2.59 → socketsecurity-2.2.60}/docs/README.md +0 -0
- {socketsecurity-2.2.59 → socketsecurity-2.2.60}/pytest.ini +0 -0
- {socketsecurity-2.2.59 → socketsecurity-2.2.60}/scripts/build_container.sh +0 -0
- {socketsecurity-2.2.59 → socketsecurity-2.2.60}/scripts/build_container_flexible.sh +0 -0
- {socketsecurity-2.2.59 → socketsecurity-2.2.60}/scripts/deploy-test-docker.sh +0 -0
- {socketsecurity-2.2.59 → socketsecurity-2.2.60}/scripts/deploy-test-pypi.sh +0 -0
- {socketsecurity-2.2.59 → socketsecurity-2.2.60}/scripts/docker-entrypoint.sh +0 -0
- {socketsecurity-2.2.59 → socketsecurity-2.2.60}/scripts/run.sh +0 -0
- {socketsecurity-2.2.59 → socketsecurity-2.2.60}/socketsecurity/core/__init__.py +0 -0
- {socketsecurity-2.2.59 → socketsecurity-2.2.60}/socketsecurity/core/classes.py +0 -0
- {socketsecurity-2.2.59 → socketsecurity-2.2.60}/socketsecurity/core/cli_client.py +0 -0
- {socketsecurity-2.2.59 → socketsecurity-2.2.60}/socketsecurity/core/exceptions.py +0 -0
- {socketsecurity-2.2.59 → socketsecurity-2.2.60}/socketsecurity/core/git_interface.py +0 -0
- {socketsecurity-2.2.59 → socketsecurity-2.2.60}/socketsecurity/core/helper/__init__.py +0 -0
- {socketsecurity-2.2.59 → socketsecurity-2.2.60}/socketsecurity/core/lazy_file_loader.py +0 -0
- {socketsecurity-2.2.59 → socketsecurity-2.2.60}/socketsecurity/core/logging.py +0 -0
- {socketsecurity-2.2.59 → socketsecurity-2.2.60}/socketsecurity/core/messages.py +0 -0
- {socketsecurity-2.2.59 → socketsecurity-2.2.60}/socketsecurity/core/resource_utils.py +0 -0
- {socketsecurity-2.2.59 → socketsecurity-2.2.60}/socketsecurity/core/scm/__init__.py +0 -0
- {socketsecurity-2.2.59 → socketsecurity-2.2.60}/socketsecurity/core/scm/base.py +0 -0
- {socketsecurity-2.2.59 → socketsecurity-2.2.60}/socketsecurity/core/scm/client.py +0 -0
- {socketsecurity-2.2.59 → socketsecurity-2.2.60}/socketsecurity/core/scm/github.py +0 -0
- {socketsecurity-2.2.59 → socketsecurity-2.2.60}/socketsecurity/core/scm/gitlab.py +0 -0
- {socketsecurity-2.2.59 → socketsecurity-2.2.60}/socketsecurity/core/scm_comments.py +0 -0
- {socketsecurity-2.2.59 → socketsecurity-2.2.60}/socketsecurity/core/socket_config.py +0 -0
- {socketsecurity-2.2.59 → socketsecurity-2.2.60}/socketsecurity/core/tools/reachability.py +0 -0
- {socketsecurity-2.2.59 → socketsecurity-2.2.60}/socketsecurity/core/utils.py +0 -0
- {socketsecurity-2.2.59 → socketsecurity-2.2.60}/socketsecurity/output.py +0 -0
- {socketsecurity-2.2.59 → socketsecurity-2.2.60}/socketsecurity/plugins/__init__.py +0 -0
- {socketsecurity-2.2.59 → socketsecurity-2.2.60}/socketsecurity/plugins/base.py +0 -0
- {socketsecurity-2.2.59 → socketsecurity-2.2.60}/socketsecurity/plugins/jira.py +0 -0
- {socketsecurity-2.2.59 → socketsecurity-2.2.60}/socketsecurity/plugins/manager.py +0 -0
- {socketsecurity-2.2.59 → socketsecurity-2.2.60}/socketsecurity/plugins/teams.py +0 -0
- {socketsecurity-2.2.59 → socketsecurity-2.2.60}/socketsecurity/plugins/webhook.py +0 -0
- {socketsecurity-2.2.59 → socketsecurity-2.2.60}/tests/__init__.py +0 -0
- {socketsecurity-2.2.59 → socketsecurity-2.2.60}/tests/core/conftest.py +0 -0
- {socketsecurity-2.2.59 → socketsecurity-2.2.60}/tests/core/create_diff_input.json +0 -0
- {socketsecurity-2.2.59 → socketsecurity-2.2.60}/tests/core/test_diff_generation.py +0 -0
- {socketsecurity-2.2.59 → socketsecurity-2.2.60}/tests/core/test_package_and_alerts.py +0 -0
- {socketsecurity-2.2.59 → socketsecurity-2.2.60}/tests/core/test_sdk_methods.py +0 -0
- {socketsecurity-2.2.59 → socketsecurity-2.2.60}/tests/core/test_supporting_methods.py +0 -0
- {socketsecurity-2.2.59 → socketsecurity-2.2.60}/tests/data/fullscans/create_response.json +0 -0
- {socketsecurity-2.2.59 → socketsecurity-2.2.60}/tests/data/fullscans/diff/stream_diff.json +0 -0
- {socketsecurity-2.2.59 → socketsecurity-2.2.60}/tests/data/fullscans/diff/stream_diff_full.json +0 -0
- {socketsecurity-2.2.59 → socketsecurity-2.2.60}/tests/data/fullscans/head_scan/metadata.json +0 -0
- {socketsecurity-2.2.59 → socketsecurity-2.2.60}/tests/data/fullscans/head_scan/stream_scan.json +0 -0
- {socketsecurity-2.2.59 → socketsecurity-2.2.60}/tests/data/fullscans/head_scan/stream_scan_full.json +0 -0
- {socketsecurity-2.2.59 → socketsecurity-2.2.60}/tests/data/fullscans/new_scan/metadata.json +0 -0
- {socketsecurity-2.2.59 → socketsecurity-2.2.60}/tests/data/fullscans/new_scan/stream_scan.json +0 -0
- {socketsecurity-2.2.59 → socketsecurity-2.2.60}/tests/data/repos/repo_info_error.json +0 -0
- {socketsecurity-2.2.59 → socketsecurity-2.2.60}/tests/data/repos/repo_info_no_head.json +0 -0
- {socketsecurity-2.2.59 → socketsecurity-2.2.60}/tests/data/repos/repo_info_success.json +0 -0
- {socketsecurity-2.2.59 → socketsecurity-2.2.60}/tests/data/settings/security-policy.json +0 -0
- {socketsecurity-2.2.59 → socketsecurity-2.2.60}/tests/unit/__init__.py +0 -0
- {socketsecurity-2.2.59 → socketsecurity-2.2.60}/tests/unit/test_cli_config.py +0 -0
- {socketsecurity-2.2.59 → socketsecurity-2.2.60}/tests/unit/test_client.py +0 -0
- {socketsecurity-2.2.59 → socketsecurity-2.2.60}/tests/unit/test_config.py +0 -0
- {socketsecurity-2.2.59 → socketsecurity-2.2.60}/tests/unit/test_gitlab_auth.py +0 -0
- {socketsecurity-2.2.59 → socketsecurity-2.2.60}/tests/unit/test_gitlab_auth_fallback.py +0 -0
- {socketsecurity-2.2.59 → socketsecurity-2.2.60}/tests/unit/test_output.py +0 -0
- {socketsecurity-2.2.59 → socketsecurity-2.2.60}/workflows/bitbucket-pipelines.yml +0 -0
- {socketsecurity-2.2.59 → socketsecurity-2.2.60}/workflows/github-actions.yml +0 -0
- {socketsecurity-2.2.59 → socketsecurity-2.2.60}/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.60
|
|
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>
|
|
@@ -35,12 +35,13 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
35
35
|
Requires-Python: >=3.10
|
|
36
36
|
Requires-Dist: bs4>=0.0.2
|
|
37
37
|
Requires-Dist: gitpython
|
|
38
|
+
Requires-Dist: markdown>=3.10
|
|
38
39
|
Requires-Dist: mdutils
|
|
39
40
|
Requires-Dist: packaging
|
|
40
41
|
Requires-Dist: prettytable
|
|
41
42
|
Requires-Dist: python-dotenv
|
|
42
43
|
Requires-Dist: requests
|
|
43
|
-
Requires-Dist: socketdev<4.0.0,>=3.0.
|
|
44
|
+
Requires-Dist: socketdev<4.0.0,>=3.0.25
|
|
44
45
|
Provides-Extra: dev
|
|
45
46
|
Requires-Dist: hatch; extra == 'dev'
|
|
46
47
|
Requires-Dist: pre-commit; extra == 'dev'
|
|
@@ -158,14 +159,14 @@ socketcli [-h] [--api-token API_TOKEN] [--repo REPO] [--repo-is-public] [--branc
|
|
|
158
159
|
[--only-facts-file] [--version]
|
|
159
160
|
````
|
|
160
161
|
|
|
161
|
-
If you don't want to provide the Socket API Token every time then you can use the environment variable `
|
|
162
|
+
If you don't want to provide the Socket API Token every time then you can use the environment variable `SOCKET_SECURITY_API_TOKEN`
|
|
162
163
|
|
|
163
164
|
### Parameters
|
|
164
165
|
|
|
165
166
|
#### Authentication
|
|
166
|
-
| Parameter | Required | Default | Description
|
|
167
|
-
|
|
168
|
-
| --api-token | False | | Socket Security API token (can also be set via
|
|
167
|
+
| Parameter | Required | Default | Description |
|
|
168
|
+
|:------------|:---------|:--------|:----------------------------------------------------------------------------------|
|
|
169
|
+
| --api-token | False | | Socket Security API token (can also be set via SOCKET_SECURITY_API_TOKEN env var) |
|
|
169
170
|
|
|
170
171
|
#### Repository
|
|
171
172
|
| Parameter | Required | Default | Description |
|
|
@@ -278,15 +279,43 @@ Example `SOCKET_JIRA_CONFIG_JSON` value
|
|
|
278
279
|
|
|
279
280
|
| Environment Variable | Required | Default | Description |
|
|
280
281
|
|:-------------------------|:---------|:--------|:-----------------------------------|
|
|
281
|
-
|
|
|
282
|
-
| SOCKET_SLACK_CONFIG_JSON | True | None | Required if the Plugin is enabled. |
|
|
282
|
+
| SOCKET_SLACK_CONFIG_JSON | False | None | Slack webhook configuration (enables plugin when set). Alternatively, use --slack-webhook CLI flag. |
|
|
283
283
|
|
|
284
|
-
Example `SOCKET_SLACK_CONFIG_JSON` value
|
|
284
|
+
Example `SOCKET_SLACK_CONFIG_JSON` value (simple webhook):
|
|
285
285
|
|
|
286
286
|
````json
|
|
287
287
|
{"url": "https://REPLACE_ME_WEBHOOK"}
|
|
288
288
|
````
|
|
289
289
|
|
|
290
|
+
Example with advanced filtering (reachability-only alerts):
|
|
291
|
+
|
|
292
|
+
````json
|
|
293
|
+
{
|
|
294
|
+
"url": [
|
|
295
|
+
{
|
|
296
|
+
"name": "prod_alerts",
|
|
297
|
+
"url": "https://hooks.slack.com/services/YOUR/WEBHOOK/URL"
|
|
298
|
+
}
|
|
299
|
+
],
|
|
300
|
+
"url_configs": {
|
|
301
|
+
"prod_alerts": {
|
|
302
|
+
"reachability_alerts_only": true,
|
|
303
|
+
"always_send_reachability": true
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
````
|
|
308
|
+
|
|
309
|
+
**Advanced Configuration Options:**
|
|
310
|
+
|
|
311
|
+
The `url_configs` object allows per-webhook filtering:
|
|
312
|
+
|
|
313
|
+
- `reachability_alerts_only` (boolean, default: false): When `--reach` is enabled, only send blocking alerts (error=true) from diff scans
|
|
314
|
+
- `always_send_reachability` (boolean, default: true): Send reachability alerts even on non-diff scans when `--reach` is enabled. Set to false to only send reachability alerts when there are diff alerts.
|
|
315
|
+
- `repos` (array): Only send alerts for specific repositories (e.g., `["owner/repo1", "owner/repo2"]`)
|
|
316
|
+
- `alert_types` (array): Only send specific alert types (e.g., `["malware", "typosquat"]`)
|
|
317
|
+
- `severities` (array): Only send alerts with specific severities (e.g., `["high", "critical"]`)
|
|
318
|
+
|
|
290
319
|
## Automatic Git Detection
|
|
291
320
|
|
|
292
321
|
The CLI now automatically detects repository information from your git environment, significantly simplifying usage in CI/CD pipelines:
|
|
@@ -547,7 +576,8 @@ Implementation targets:
|
|
|
547
576
|
### Environment Variables
|
|
548
577
|
|
|
549
578
|
#### Core Configuration
|
|
550
|
-
- `
|
|
579
|
+
- `SOCKET_SECURITY_API_TOKEN`: Socket Security API token (alternative to --api-token parameter)
|
|
580
|
+
- For backwards compatibility, also accepts: `SOCKET_SECURITY_API_KEY`, `SOCKET_API_KEY`, `SOCKET_API_TOKEN`
|
|
551
581
|
- `SOCKET_SDK_PATH`: Path to local socketdev repository (default: ../socketdev)
|
|
552
582
|
|
|
553
583
|
#### GitLab Integration
|
|
@@ -101,14 +101,14 @@ socketcli [-h] [--api-token API_TOKEN] [--repo REPO] [--repo-is-public] [--branc
|
|
|
101
101
|
[--only-facts-file] [--version]
|
|
102
102
|
````
|
|
103
103
|
|
|
104
|
-
If you don't want to provide the Socket API Token every time then you can use the environment variable `
|
|
104
|
+
If you don't want to provide the Socket API Token every time then you can use the environment variable `SOCKET_SECURITY_API_TOKEN`
|
|
105
105
|
|
|
106
106
|
### Parameters
|
|
107
107
|
|
|
108
108
|
#### Authentication
|
|
109
|
-
| Parameter | Required | Default | Description
|
|
110
|
-
|
|
111
|
-
| --api-token | False | | Socket Security API token (can also be set via
|
|
109
|
+
| Parameter | Required | Default | Description |
|
|
110
|
+
|:------------|:---------|:--------|:----------------------------------------------------------------------------------|
|
|
111
|
+
| --api-token | False | | Socket Security API token (can also be set via SOCKET_SECURITY_API_TOKEN env var) |
|
|
112
112
|
|
|
113
113
|
#### Repository
|
|
114
114
|
| Parameter | Required | Default | Description |
|
|
@@ -221,15 +221,43 @@ Example `SOCKET_JIRA_CONFIG_JSON` value
|
|
|
221
221
|
|
|
222
222
|
| Environment Variable | Required | Default | Description |
|
|
223
223
|
|:-------------------------|:---------|:--------|:-----------------------------------|
|
|
224
|
-
|
|
|
225
|
-
| SOCKET_SLACK_CONFIG_JSON | True | None | Required if the Plugin is enabled. |
|
|
224
|
+
| SOCKET_SLACK_CONFIG_JSON | False | None | Slack webhook configuration (enables plugin when set). Alternatively, use --slack-webhook CLI flag. |
|
|
226
225
|
|
|
227
|
-
Example `SOCKET_SLACK_CONFIG_JSON` value
|
|
226
|
+
Example `SOCKET_SLACK_CONFIG_JSON` value (simple webhook):
|
|
228
227
|
|
|
229
228
|
````json
|
|
230
229
|
{"url": "https://REPLACE_ME_WEBHOOK"}
|
|
231
230
|
````
|
|
232
231
|
|
|
232
|
+
Example with advanced filtering (reachability-only alerts):
|
|
233
|
+
|
|
234
|
+
````json
|
|
235
|
+
{
|
|
236
|
+
"url": [
|
|
237
|
+
{
|
|
238
|
+
"name": "prod_alerts",
|
|
239
|
+
"url": "https://hooks.slack.com/services/YOUR/WEBHOOK/URL"
|
|
240
|
+
}
|
|
241
|
+
],
|
|
242
|
+
"url_configs": {
|
|
243
|
+
"prod_alerts": {
|
|
244
|
+
"reachability_alerts_only": true,
|
|
245
|
+
"always_send_reachability": true
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
````
|
|
250
|
+
|
|
251
|
+
**Advanced Configuration Options:**
|
|
252
|
+
|
|
253
|
+
The `url_configs` object allows per-webhook filtering:
|
|
254
|
+
|
|
255
|
+
- `reachability_alerts_only` (boolean, default: false): When `--reach` is enabled, only send blocking alerts (error=true) from diff scans
|
|
256
|
+
- `always_send_reachability` (boolean, default: true): Send reachability alerts even on non-diff scans when `--reach` is enabled. Set to false to only send reachability alerts when there are diff alerts.
|
|
257
|
+
- `repos` (array): Only send alerts for specific repositories (e.g., `["owner/repo1", "owner/repo2"]`)
|
|
258
|
+
- `alert_types` (array): Only send specific alert types (e.g., `["malware", "typosquat"]`)
|
|
259
|
+
- `severities` (array): Only send alerts with specific severities (e.g., `["high", "critical"]`)
|
|
260
|
+
|
|
233
261
|
## Automatic Git Detection
|
|
234
262
|
|
|
235
263
|
The CLI now automatically detects repository information from your git environment, significantly simplifying usage in CI/CD pipelines:
|
|
@@ -490,7 +518,8 @@ Implementation targets:
|
|
|
490
518
|
### Environment Variables
|
|
491
519
|
|
|
492
520
|
#### Core Configuration
|
|
493
|
-
- `
|
|
521
|
+
- `SOCKET_SECURITY_API_TOKEN`: Socket Security API token (alternative to --api-token parameter)
|
|
522
|
+
- For backwards compatibility, also accepts: `SOCKET_SECURITY_API_KEY`, `SOCKET_API_KEY`, `SOCKET_API_TOKEN`
|
|
494
523
|
- `SOCKET_SDK_PATH`: Path to local socketdev repository (default: ../socketdev)
|
|
495
524
|
|
|
496
525
|
#### GitLab Integration
|
|
@@ -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.60"
|
|
10
10
|
requires-python = ">= 3.10"
|
|
11
11
|
license = {"file" = "LICENSE"}
|
|
12
12
|
dependencies = [
|
|
@@ -16,8 +16,9 @@ dependencies = [
|
|
|
16
16
|
'GitPython',
|
|
17
17
|
'packaging',
|
|
18
18
|
'python-dotenv',
|
|
19
|
-
|
|
19
|
+
"socketdev>=3.0.25,<4.0.0",
|
|
20
20
|
"bs4>=0.0.2",
|
|
21
|
+
"markdown>=3.10",
|
|
21
22
|
]
|
|
22
23
|
readme = "README.md"
|
|
23
24
|
description = "Socket Security CLI for CI/CD"
|
|
@@ -57,6 +57,7 @@ class CliConfig:
|
|
|
57
57
|
version: str = __version__
|
|
58
58
|
jira_plugin: PluginConfig = field(default_factory=PluginConfig)
|
|
59
59
|
slack_plugin: PluginConfig = field(default_factory=PluginConfig)
|
|
60
|
+
slack_webhook: Optional[str] = None
|
|
60
61
|
license_file_name: str = "license_output.json"
|
|
61
62
|
save_submitted_files_list: Optional[str] = None
|
|
62
63
|
save_manifest_tar: Optional[str] = None
|
|
@@ -85,8 +86,14 @@ class CliConfig:
|
|
|
85
86
|
parser = create_argument_parser()
|
|
86
87
|
args = parser.parse_args(args_list)
|
|
87
88
|
|
|
88
|
-
# Get API token from env or args
|
|
89
|
-
api_token =
|
|
89
|
+
# Get API token from env or args (check multiple env var names)
|
|
90
|
+
api_token = (
|
|
91
|
+
os.getenv("SOCKET_SECURITY_API_KEY") or
|
|
92
|
+
os.getenv("SOCKET_SECURITY_API_TOKEN") or
|
|
93
|
+
os.getenv("SOCKET_API_KEY") or
|
|
94
|
+
os.getenv("SOCKET_API_TOKEN") or
|
|
95
|
+
args.api_token
|
|
96
|
+
)
|
|
90
97
|
|
|
91
98
|
# Strip quotes from commit message if present
|
|
92
99
|
commit_message = args.commit_message
|
|
@@ -128,6 +135,7 @@ class CliConfig:
|
|
|
128
135
|
'save_manifest_tar': args.save_manifest_tar,
|
|
129
136
|
'sub_paths': args.sub_paths or [],
|
|
130
137
|
'workspace_name': args.workspace_name,
|
|
138
|
+
'slack_webhook': args.slack_webhook,
|
|
131
139
|
'reach': args.reach,
|
|
132
140
|
'reach_version': args.reach_version,
|
|
133
141
|
'reach_analysis_timeout': args.reach_analysis_timeout,
|
|
@@ -151,6 +159,11 @@ class CliConfig:
|
|
|
151
159
|
except json.JSONDecodeError:
|
|
152
160
|
logging.error(f"Unable to parse excluded_ecosystems: {config_args['excluded_ecosystems']}")
|
|
153
161
|
exit(1)
|
|
162
|
+
# Build Slack plugin config, merging CLI arg with env config
|
|
163
|
+
slack_config = get_plugin_config_from_env("SOCKET_SLACK")
|
|
164
|
+
if args.slack_webhook:
|
|
165
|
+
slack_config["url"] = args.slack_webhook
|
|
166
|
+
|
|
154
167
|
config_args.update({
|
|
155
168
|
"jira_plugin": PluginConfig(
|
|
156
169
|
enabled=os.getenv("SOCKET_JIRA_ENABLED", "false").lower() == "true",
|
|
@@ -158,9 +171,9 @@ class CliConfig:
|
|
|
158
171
|
config=get_plugin_config_from_env("SOCKET_JIRA")
|
|
159
172
|
),
|
|
160
173
|
"slack_plugin": PluginConfig(
|
|
161
|
-
enabled=
|
|
174
|
+
enabled=bool(slack_config) or bool(args.slack_webhook),
|
|
162
175
|
levels=os.getenv("SOCKET_SLACK_LEVELS", "block,warn").split(","),
|
|
163
|
-
config=
|
|
176
|
+
config=slack_config
|
|
164
177
|
)
|
|
165
178
|
})
|
|
166
179
|
|
|
@@ -212,7 +225,7 @@ def create_argument_parser() -> argparse.ArgumentParser:
|
|
|
212
225
|
"--api-token",
|
|
213
226
|
dest="api_token",
|
|
214
227
|
metavar="<token>",
|
|
215
|
-
help="Socket Security API token (can also be set via
|
|
228
|
+
help="Socket Security API token (can also be set via SOCKET_SECURITY_API_TOKEN env var)",
|
|
216
229
|
required=False
|
|
217
230
|
)
|
|
218
231
|
auth_group.add_argument(
|
|
@@ -475,6 +488,15 @@ def create_argument_parser() -> argparse.ArgumentParser:
|
|
|
475
488
|
help=argparse.SUPPRESS
|
|
476
489
|
)
|
|
477
490
|
|
|
491
|
+
# Plugin Configuration
|
|
492
|
+
plugin_group = parser.add_argument_group('Plugin Configuration')
|
|
493
|
+
plugin_group.add_argument(
|
|
494
|
+
"--slack-webhook",
|
|
495
|
+
dest="slack_webhook",
|
|
496
|
+
metavar="<url>",
|
|
497
|
+
help="Slack webhook URL for notifications (automatically enables Slack plugin)"
|
|
498
|
+
)
|
|
499
|
+
|
|
478
500
|
# Advanced Configuration
|
|
479
501
|
advanced_group = parser.add_argument_group('Advanced Configuration')
|
|
480
502
|
advanced_group.add_argument(
|
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
"""Helper module for loading and processing .socket.facts.json files."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Dict, Any, Optional, List
|
|
7
|
+
from copy import deepcopy
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def load_socket_facts(file_path: str = ".socket.facts.json") -> Optional[Dict[str, Any]]:
|
|
13
|
+
"""
|
|
14
|
+
Load a .socket.facts.json file into a dictionary.
|
|
15
|
+
|
|
16
|
+
The .socket.facts.json file is generated by the Socket CLI reachability analysis
|
|
17
|
+
and contains component dependency information, vulnerability data, and
|
|
18
|
+
reachability analysis results.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
file_path: Path to the .socket.facts.json file. Defaults to ".socket.facts.json"
|
|
22
|
+
in the current directory.
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
Dict containing the parsed JSON data with keys like:
|
|
26
|
+
- components: List of dependency components with vulnerabilities and reachability info
|
|
27
|
+
- tier1ReachabilityScanId: The scan ID for this reachability analysis
|
|
28
|
+
|
|
29
|
+
Returns None if the file doesn't exist or cannot be parsed.
|
|
30
|
+
|
|
31
|
+
Example structure:
|
|
32
|
+
{
|
|
33
|
+
"components": [
|
|
34
|
+
{
|
|
35
|
+
"id": "12345",
|
|
36
|
+
"type": "npm",
|
|
37
|
+
"name": "package-name",
|
|
38
|
+
"version": "1.0.0",
|
|
39
|
+
"namespace": "@scope",
|
|
40
|
+
"direct": false,
|
|
41
|
+
"dev": true,
|
|
42
|
+
"vulnerabilities": [...],
|
|
43
|
+
"reachability": [...],
|
|
44
|
+
...
|
|
45
|
+
}
|
|
46
|
+
],
|
|
47
|
+
"tier1ReachabilityScanId": "scan-id-here"
|
|
48
|
+
}
|
|
49
|
+
"""
|
|
50
|
+
facts_path = Path(file_path)
|
|
51
|
+
|
|
52
|
+
if not facts_path.exists():
|
|
53
|
+
logger.warning(f"Socket facts file not found: {file_path}")
|
|
54
|
+
return None
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
with facts_path.open('r', encoding='utf-8') as f:
|
|
58
|
+
data = json.load(f)
|
|
59
|
+
|
|
60
|
+
logger.debug(f"Successfully loaded socket facts from {file_path}")
|
|
61
|
+
|
|
62
|
+
# Validate expected structure
|
|
63
|
+
if not isinstance(data, dict):
|
|
64
|
+
logger.warning(f"Socket facts file has unexpected format: expected dict, got {type(data)}")
|
|
65
|
+
return None
|
|
66
|
+
|
|
67
|
+
if 'components' not in data:
|
|
68
|
+
logger.warning(f"Socket facts file missing 'components' key")
|
|
69
|
+
|
|
70
|
+
return data
|
|
71
|
+
|
|
72
|
+
except json.JSONDecodeError as e:
|
|
73
|
+
logger.error(f"Failed to parse JSON from {file_path}: {e}")
|
|
74
|
+
return None
|
|
75
|
+
except IOError as e:
|
|
76
|
+
logger.error(f"Failed to read {file_path}: {e}")
|
|
77
|
+
return None
|
|
78
|
+
except Exception as e:
|
|
79
|
+
logger.error(f"Unexpected error loading socket facts from {file_path}: {e}")
|
|
80
|
+
return None
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def get_components_with_vulnerabilities(facts_data: Dict[str, Any]) -> list:
|
|
84
|
+
"""
|
|
85
|
+
Extract components that have vulnerabilities from socket facts data.
|
|
86
|
+
|
|
87
|
+
Note: The .socket.facts.json file contains 'vulnerabilities' and 'reachability'
|
|
88
|
+
data separately. This function returns components that have vulnerabilities defined.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
facts_data: Dictionary loaded from .socket.facts.json
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
List of component dictionaries that have vulnerabilities
|
|
95
|
+
"""
|
|
96
|
+
if not facts_data or 'components' not in facts_data:
|
|
97
|
+
return []
|
|
98
|
+
|
|
99
|
+
components = facts_data.get('components', [])
|
|
100
|
+
components_with_vulns = [
|
|
101
|
+
comp for comp in components
|
|
102
|
+
if comp.get('vulnerabilities') and len(comp.get('vulnerabilities', [])) > 0
|
|
103
|
+
]
|
|
104
|
+
|
|
105
|
+
return components_with_vulns
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def get_scan_id(facts_data: Dict[str, Any]) -> Optional[str]:
|
|
109
|
+
"""
|
|
110
|
+
Extract the tier1ReachabilityScanId from socket facts data.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
facts_data: Dictionary loaded from .socket.facts.json
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
The scan ID string if present, None otherwise
|
|
117
|
+
"""
|
|
118
|
+
if not facts_data:
|
|
119
|
+
return None
|
|
120
|
+
|
|
121
|
+
scan_id = facts_data.get('tier1ReachabilityScanId')
|
|
122
|
+
return scan_id.strip() if scan_id else None
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _make_purl(component: Dict[str, Any]) -> str:
|
|
126
|
+
"""Construct a package URL (purl) from a component entry."""
|
|
127
|
+
pkg_type = component.get('type', '')
|
|
128
|
+
namespace = component.get('namespace', '')
|
|
129
|
+
name = component.get('name') or component.get('id', '')
|
|
130
|
+
version = component.get('version', '')
|
|
131
|
+
|
|
132
|
+
if not name:
|
|
133
|
+
return ''
|
|
134
|
+
|
|
135
|
+
if namespace:
|
|
136
|
+
# Percent-encode @ in namespace for purl spec compliance
|
|
137
|
+
ns_encoded = namespace.replace('@', '%40')
|
|
138
|
+
purl = f"pkg:{pkg_type}/{ns_encoded}/{name}"
|
|
139
|
+
else:
|
|
140
|
+
purl = f"pkg:{pkg_type}/{name}"
|
|
141
|
+
|
|
142
|
+
if version:
|
|
143
|
+
purl = f"{purl}@{version}"
|
|
144
|
+
|
|
145
|
+
return purl
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _determine_reachability(vulnerability: Dict[str, Any], component: Dict[str, Any]) -> Dict[str, Any]:
|
|
149
|
+
"""
|
|
150
|
+
Determine the reachability state for a vulnerability on a component.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
vulnerability: Vulnerability dict from component's vulnerabilities array
|
|
154
|
+
component: Component dict containing reachability data
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
Dict with keys:
|
|
158
|
+
- type: 'reachable', 'unreachable', 'unknown', 'error', or 'not_applicable'
|
|
159
|
+
- undeterminableReachability: bool
|
|
160
|
+
- trace: list of formatted trace strings
|
|
161
|
+
"""
|
|
162
|
+
result = {
|
|
163
|
+
'type': 'unknown',
|
|
164
|
+
'undeterminableReachability': False,
|
|
165
|
+
'trace': []
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
vuln_id = vulnerability.get('ghsaId') or vulnerability.get('cveId')
|
|
169
|
+
if not vuln_id:
|
|
170
|
+
return result
|
|
171
|
+
|
|
172
|
+
# Check for undeterminable reachability in the vulnerability data
|
|
173
|
+
reach_data = vulnerability.get('reachabilityData') or {}
|
|
174
|
+
if reach_data.get('undeterminableReachability'):
|
|
175
|
+
result['undeterminableReachability'] = True
|
|
176
|
+
result['type'] = 'unknown'
|
|
177
|
+
|
|
178
|
+
# Find matching reachability entry in component
|
|
179
|
+
reachability_list = component.get('reachability', [])
|
|
180
|
+
matched_reach = None
|
|
181
|
+
|
|
182
|
+
for reach_entry in reachability_list:
|
|
183
|
+
if reach_entry.get('ghsa_id') == vuln_id:
|
|
184
|
+
matched_reach = reach_entry
|
|
185
|
+
break
|
|
186
|
+
|
|
187
|
+
if not matched_reach:
|
|
188
|
+
# No reachability data found for this vulnerability
|
|
189
|
+
if result['undeterminableReachability']:
|
|
190
|
+
return result
|
|
191
|
+
# Check if this vulnerability applies to this component version
|
|
192
|
+
if 'reachabilityData' in vulnerability:
|
|
193
|
+
# Has reachability data structure but no match - might not apply
|
|
194
|
+
result['type'] = 'not_applicable'
|
|
195
|
+
return result
|
|
196
|
+
|
|
197
|
+
# Process reachability matches
|
|
198
|
+
reach_items = matched_reach.get('reachability', [])
|
|
199
|
+
if not reach_items:
|
|
200
|
+
return result
|
|
201
|
+
|
|
202
|
+
# Take the first reachability entry (usually most relevant)
|
|
203
|
+
reach_info = reach_items[0]
|
|
204
|
+
reach_type = reach_info.get('type', 'unknown')
|
|
205
|
+
result['type'] = reach_type
|
|
206
|
+
|
|
207
|
+
# Build trace for reachable vulnerabilities
|
|
208
|
+
if reach_type == 'reachable':
|
|
209
|
+
matches = reach_info.get('matches', [])
|
|
210
|
+
for match_group in matches:
|
|
211
|
+
if not match_group:
|
|
212
|
+
continue
|
|
213
|
+
|
|
214
|
+
for i, frame in enumerate(match_group):
|
|
215
|
+
pkg = frame.get('package', '')
|
|
216
|
+
src_loc = frame.get('sourceLocation', {})
|
|
217
|
+
filename = src_loc.get('filename', '')
|
|
218
|
+
start = src_loc.get('start', {})
|
|
219
|
+
line = start.get('line')
|
|
220
|
+
col = start.get('column')
|
|
221
|
+
end = src_loc.get('end', {})
|
|
222
|
+
end_line = end.get('line')
|
|
223
|
+
end_col = end.get('column')
|
|
224
|
+
|
|
225
|
+
if i == 0:
|
|
226
|
+
# First frame - use filename as primary
|
|
227
|
+
if filename:
|
|
228
|
+
loc = filename
|
|
229
|
+
if line is not None:
|
|
230
|
+
if end_line is not None and end_line != line:
|
|
231
|
+
loc = f"{filename} {line}:{col if col else ''}-{end_line}:{end_col if end_col else ''}"
|
|
232
|
+
else:
|
|
233
|
+
loc = f"{filename} {line}:{col if col else ''}"
|
|
234
|
+
result['trace'].append(loc)
|
|
235
|
+
else:
|
|
236
|
+
# Subsequent frames - show package/module reference
|
|
237
|
+
if pkg or filename:
|
|
238
|
+
entry = pkg if pkg else filename
|
|
239
|
+
if line is not None:
|
|
240
|
+
entry = f" -> {entry} {line}:{col if col else ''}"
|
|
241
|
+
else:
|
|
242
|
+
entry = f" -> {entry}"
|
|
243
|
+
result['trace'].append(entry)
|
|
244
|
+
|
|
245
|
+
# Add final line showing the vulnerable component
|
|
246
|
+
comp_name = component.get('name') or component.get('id')
|
|
247
|
+
comp_ver = component.get('version')
|
|
248
|
+
if comp_name:
|
|
249
|
+
final_line = f" -> {comp_name}@{comp_ver}" if comp_ver else f" -> {comp_name}"
|
|
250
|
+
result['trace'].append(final_line)
|
|
251
|
+
|
|
252
|
+
return result
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def convert_to_alerts(components: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
|
256
|
+
"""
|
|
257
|
+
Convert components with vulnerabilities into components with alerts.
|
|
258
|
+
|
|
259
|
+
This function processes the raw .socket.facts.json format (with 'vulnerabilities'
|
|
260
|
+
and 'reachability' arrays) and converts them into an 'alerts' format suitable
|
|
261
|
+
for formatters and notifications.
|
|
262
|
+
|
|
263
|
+
Args:
|
|
264
|
+
components: List of component dicts from .socket.facts.json
|
|
265
|
+
|
|
266
|
+
Returns:
|
|
267
|
+
List of component dicts with 'alerts' field added (original components unchanged)
|
|
268
|
+
"""
|
|
269
|
+
components_with_alerts = []
|
|
270
|
+
|
|
271
|
+
for comp in components:
|
|
272
|
+
vulns = comp.get('vulnerabilities', [])
|
|
273
|
+
if not vulns:
|
|
274
|
+
continue
|
|
275
|
+
|
|
276
|
+
alerts = []
|
|
277
|
+
for vuln in vulns:
|
|
278
|
+
vuln_id = vuln.get('ghsaId') or vuln.get('cveId') or 'Unknown'
|
|
279
|
+
|
|
280
|
+
# Extract severity
|
|
281
|
+
sev_val = vuln.get('severity', '')
|
|
282
|
+
severity = 'unknown'
|
|
283
|
+
|
|
284
|
+
# Handle both numeric and string severities
|
|
285
|
+
try:
|
|
286
|
+
if isinstance(sev_val, (int, float)):
|
|
287
|
+
score = float(sev_val)
|
|
288
|
+
if score >= 9.0:
|
|
289
|
+
severity = 'critical'
|
|
290
|
+
elif score >= 7.0:
|
|
291
|
+
severity = 'high'
|
|
292
|
+
elif score >= 4.0:
|
|
293
|
+
severity = 'medium'
|
|
294
|
+
else:
|
|
295
|
+
severity = 'low'
|
|
296
|
+
elif isinstance(sev_val, str):
|
|
297
|
+
# Try to parse as number first
|
|
298
|
+
if sev_val.replace('.', '', 1).isdigit():
|
|
299
|
+
score = float(sev_val)
|
|
300
|
+
if score >= 9.0:
|
|
301
|
+
severity = 'critical'
|
|
302
|
+
elif score >= 7.0:
|
|
303
|
+
severity = 'high'
|
|
304
|
+
elif score >= 4.0:
|
|
305
|
+
severity = 'medium'
|
|
306
|
+
else:
|
|
307
|
+
severity = 'low'
|
|
308
|
+
else:
|
|
309
|
+
# Use as-is if it's a string severity
|
|
310
|
+
severity = sev_val.lower()
|
|
311
|
+
except (ValueError, TypeError):
|
|
312
|
+
severity = 'unknown'
|
|
313
|
+
|
|
314
|
+
# Determine reachability
|
|
315
|
+
reach_info = _determine_reachability(vuln, comp)
|
|
316
|
+
|
|
317
|
+
# Skip vulnerabilities that don't apply to this component version
|
|
318
|
+
if reach_info.get('type') == 'not_applicable':
|
|
319
|
+
continue
|
|
320
|
+
|
|
321
|
+
# Build alert
|
|
322
|
+
purl = _make_purl(comp)
|
|
323
|
+
trace_str = '\n'.join(reach_info.get('trace', []))
|
|
324
|
+
reach_type = reach_info.get('type', 'unknown')
|
|
325
|
+
|
|
326
|
+
# Map reachability to severity (reachable = critical, unknown/error = high, unreachable = low)
|
|
327
|
+
final_severity = severity
|
|
328
|
+
if reach_type == 'reachable':
|
|
329
|
+
final_severity = 'critical'
|
|
330
|
+
elif reach_type in ('unknown', 'error') or reach_info.get('undeterminableReachability'):
|
|
331
|
+
final_severity = 'high'
|
|
332
|
+
elif reach_type == 'unreachable':
|
|
333
|
+
final_severity = 'low'
|
|
334
|
+
|
|
335
|
+
alert = {
|
|
336
|
+
'title': vuln_id,
|
|
337
|
+
'severity': final_severity,
|
|
338
|
+
'type': 'vulnerability',
|
|
339
|
+
'category': 'vulnerability',
|
|
340
|
+
'props': {
|
|
341
|
+
'cveId': vuln.get('cveId'),
|
|
342
|
+
'ghsaId': vuln.get('ghsaId'),
|
|
343
|
+
'range': vuln.get('range'),
|
|
344
|
+
'purl': purl,
|
|
345
|
+
'reachability': reach_type,
|
|
346
|
+
'undeterminableReachability': reach_info.get('undeterminableReachability', False),
|
|
347
|
+
'trace': trace_str,
|
|
348
|
+
'severity': final_severity,
|
|
349
|
+
'original_severity': severity,
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
alerts.append(alert)
|
|
353
|
+
|
|
354
|
+
if alerts:
|
|
355
|
+
# Create a copy with alerts added
|
|
356
|
+
comp_with_alerts = deepcopy(comp)
|
|
357
|
+
comp_with_alerts['alerts'] = alerts
|
|
358
|
+
components_with_alerts.append(comp_with_alerts)
|
|
359
|
+
|
|
360
|
+
return components_with_alerts
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def get_component_count(facts_data: Dict[str, Any]) -> Dict[str, int]:
|
|
364
|
+
"""
|
|
365
|
+
Get statistics about components in the socket facts data.
|
|
366
|
+
|
|
367
|
+
Args:
|
|
368
|
+
facts_data: Dictionary loaded from .socket.facts.json
|
|
369
|
+
|
|
370
|
+
Returns:
|
|
371
|
+
Dictionary with counts:
|
|
372
|
+
- total: Total number of components
|
|
373
|
+
- with_vulnerabilities: Components with vulnerabilities
|
|
374
|
+
- direct: Direct dependencies
|
|
375
|
+
- dev: Development dependencies
|
|
376
|
+
"""
|
|
377
|
+
if not facts_data or 'components' not in facts_data:
|
|
378
|
+
return {'total': 0, 'with_vulnerabilities': 0, 'direct': 0, 'dev': 0}
|
|
379
|
+
|
|
380
|
+
components = facts_data.get('components', [])
|
|
381
|
+
|
|
382
|
+
return {
|
|
383
|
+
'total': len(components),
|
|
384
|
+
'with_vulnerabilities': len([c for c in components if c.get('vulnerabilities')]),
|
|
385
|
+
'direct': len([c for c in components if c.get('direct')]),
|
|
386
|
+
'dev': len([c for c in components if c.get('dev')])
|
|
387
|
+
}
|