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.
Files changed (92) hide show
  1. {socketsecurity-2.2.59 → socketsecurity-2.2.60}/PKG-INFO +40 -10
  2. {socketsecurity-2.2.59 → socketsecurity-2.2.60}/README.md +37 -8
  3. {socketsecurity-2.2.59 → socketsecurity-2.2.60}/pyproject.toml +3 -2
  4. {socketsecurity-2.2.59 → socketsecurity-2.2.60}/socketsecurity/__init__.py +1 -1
  5. {socketsecurity-2.2.59 → socketsecurity-2.2.60}/socketsecurity/config.py +27 -5
  6. socketsecurity-2.2.60/socketsecurity/core/helper/socket_facts_loader.py +387 -0
  7. socketsecurity-2.2.60/socketsecurity/plugins/formatters/__init__.py +5 -0
  8. socketsecurity-2.2.60/socketsecurity/plugins/formatters/slack.py +272 -0
  9. socketsecurity-2.2.60/socketsecurity/plugins/slack.py +491 -0
  10. {socketsecurity-2.2.59 → socketsecurity-2.2.60}/socketsecurity/socketcli.py +3 -3
  11. {socketsecurity-2.2.59 → socketsecurity-2.2.60}/uv.lock +285 -274
  12. socketsecurity-2.2.59/socketsecurity/plugins/slack.py +0 -95
  13. {socketsecurity-2.2.59 → socketsecurity-2.2.60}/.github/CODEOWNERS +0 -0
  14. {socketsecurity-2.2.59 → socketsecurity-2.2.60}/.github/PULL_REQUEST_TEMPLATE/bug-fix.md +0 -0
  15. {socketsecurity-2.2.59 → socketsecurity-2.2.60}/.github/PULL_REQUEST_TEMPLATE/feature.md +0 -0
  16. {socketsecurity-2.2.59 → socketsecurity-2.2.60}/.github/PULL_REQUEST_TEMPLATE/improvement.md +0 -0
  17. {socketsecurity-2.2.59 → socketsecurity-2.2.60}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  18. {socketsecurity-2.2.59 → socketsecurity-2.2.60}/.github/workflows/docker-stable.yml +0 -0
  19. {socketsecurity-2.2.59 → socketsecurity-2.2.60}/.github/workflows/pr-preview.yml +0 -0
  20. {socketsecurity-2.2.59 → socketsecurity-2.2.60}/.github/workflows/release.yml +0 -0
  21. {socketsecurity-2.2.59 → socketsecurity-2.2.60}/.github/workflows/version-check.yml +0 -0
  22. {socketsecurity-2.2.59 → socketsecurity-2.2.60}/.gitignore +0 -0
  23. {socketsecurity-2.2.59 → socketsecurity-2.2.60}/.hooks/sync_version.py +0 -0
  24. {socketsecurity-2.2.59 → socketsecurity-2.2.60}/.pre-commit-config.yaml +0 -0
  25. {socketsecurity-2.2.59 → socketsecurity-2.2.60}/.python-version +0 -0
  26. {socketsecurity-2.2.59 → socketsecurity-2.2.60}/CHANGELOG.md +0 -0
  27. {socketsecurity-2.2.59 → socketsecurity-2.2.60}/Dockerfile +0 -0
  28. {socketsecurity-2.2.59 → socketsecurity-2.2.60}/LICENSE +0 -0
  29. {socketsecurity-2.2.59 → socketsecurity-2.2.60}/Makefile +0 -0
  30. {socketsecurity-2.2.59 → socketsecurity-2.2.60}/docs/README.md +0 -0
  31. {socketsecurity-2.2.59 → socketsecurity-2.2.60}/pytest.ini +0 -0
  32. {socketsecurity-2.2.59 → socketsecurity-2.2.60}/scripts/build_container.sh +0 -0
  33. {socketsecurity-2.2.59 → socketsecurity-2.2.60}/scripts/build_container_flexible.sh +0 -0
  34. {socketsecurity-2.2.59 → socketsecurity-2.2.60}/scripts/deploy-test-docker.sh +0 -0
  35. {socketsecurity-2.2.59 → socketsecurity-2.2.60}/scripts/deploy-test-pypi.sh +0 -0
  36. {socketsecurity-2.2.59 → socketsecurity-2.2.60}/scripts/docker-entrypoint.sh +0 -0
  37. {socketsecurity-2.2.59 → socketsecurity-2.2.60}/scripts/run.sh +0 -0
  38. {socketsecurity-2.2.59 → socketsecurity-2.2.60}/socketsecurity/core/__init__.py +0 -0
  39. {socketsecurity-2.2.59 → socketsecurity-2.2.60}/socketsecurity/core/classes.py +0 -0
  40. {socketsecurity-2.2.59 → socketsecurity-2.2.60}/socketsecurity/core/cli_client.py +0 -0
  41. {socketsecurity-2.2.59 → socketsecurity-2.2.60}/socketsecurity/core/exceptions.py +0 -0
  42. {socketsecurity-2.2.59 → socketsecurity-2.2.60}/socketsecurity/core/git_interface.py +0 -0
  43. {socketsecurity-2.2.59 → socketsecurity-2.2.60}/socketsecurity/core/helper/__init__.py +0 -0
  44. {socketsecurity-2.2.59 → socketsecurity-2.2.60}/socketsecurity/core/lazy_file_loader.py +0 -0
  45. {socketsecurity-2.2.59 → socketsecurity-2.2.60}/socketsecurity/core/logging.py +0 -0
  46. {socketsecurity-2.2.59 → socketsecurity-2.2.60}/socketsecurity/core/messages.py +0 -0
  47. {socketsecurity-2.2.59 → socketsecurity-2.2.60}/socketsecurity/core/resource_utils.py +0 -0
  48. {socketsecurity-2.2.59 → socketsecurity-2.2.60}/socketsecurity/core/scm/__init__.py +0 -0
  49. {socketsecurity-2.2.59 → socketsecurity-2.2.60}/socketsecurity/core/scm/base.py +0 -0
  50. {socketsecurity-2.2.59 → socketsecurity-2.2.60}/socketsecurity/core/scm/client.py +0 -0
  51. {socketsecurity-2.2.59 → socketsecurity-2.2.60}/socketsecurity/core/scm/github.py +0 -0
  52. {socketsecurity-2.2.59 → socketsecurity-2.2.60}/socketsecurity/core/scm/gitlab.py +0 -0
  53. {socketsecurity-2.2.59 → socketsecurity-2.2.60}/socketsecurity/core/scm_comments.py +0 -0
  54. {socketsecurity-2.2.59 → socketsecurity-2.2.60}/socketsecurity/core/socket_config.py +0 -0
  55. {socketsecurity-2.2.59 → socketsecurity-2.2.60}/socketsecurity/core/tools/reachability.py +0 -0
  56. {socketsecurity-2.2.59 → socketsecurity-2.2.60}/socketsecurity/core/utils.py +0 -0
  57. {socketsecurity-2.2.59 → socketsecurity-2.2.60}/socketsecurity/output.py +0 -0
  58. {socketsecurity-2.2.59 → socketsecurity-2.2.60}/socketsecurity/plugins/__init__.py +0 -0
  59. {socketsecurity-2.2.59 → socketsecurity-2.2.60}/socketsecurity/plugins/base.py +0 -0
  60. {socketsecurity-2.2.59 → socketsecurity-2.2.60}/socketsecurity/plugins/jira.py +0 -0
  61. {socketsecurity-2.2.59 → socketsecurity-2.2.60}/socketsecurity/plugins/manager.py +0 -0
  62. {socketsecurity-2.2.59 → socketsecurity-2.2.60}/socketsecurity/plugins/teams.py +0 -0
  63. {socketsecurity-2.2.59 → socketsecurity-2.2.60}/socketsecurity/plugins/webhook.py +0 -0
  64. {socketsecurity-2.2.59 → socketsecurity-2.2.60}/tests/__init__.py +0 -0
  65. {socketsecurity-2.2.59 → socketsecurity-2.2.60}/tests/core/conftest.py +0 -0
  66. {socketsecurity-2.2.59 → socketsecurity-2.2.60}/tests/core/create_diff_input.json +0 -0
  67. {socketsecurity-2.2.59 → socketsecurity-2.2.60}/tests/core/test_diff_generation.py +0 -0
  68. {socketsecurity-2.2.59 → socketsecurity-2.2.60}/tests/core/test_package_and_alerts.py +0 -0
  69. {socketsecurity-2.2.59 → socketsecurity-2.2.60}/tests/core/test_sdk_methods.py +0 -0
  70. {socketsecurity-2.2.59 → socketsecurity-2.2.60}/tests/core/test_supporting_methods.py +0 -0
  71. {socketsecurity-2.2.59 → socketsecurity-2.2.60}/tests/data/fullscans/create_response.json +0 -0
  72. {socketsecurity-2.2.59 → socketsecurity-2.2.60}/tests/data/fullscans/diff/stream_diff.json +0 -0
  73. {socketsecurity-2.2.59 → socketsecurity-2.2.60}/tests/data/fullscans/diff/stream_diff_full.json +0 -0
  74. {socketsecurity-2.2.59 → socketsecurity-2.2.60}/tests/data/fullscans/head_scan/metadata.json +0 -0
  75. {socketsecurity-2.2.59 → socketsecurity-2.2.60}/tests/data/fullscans/head_scan/stream_scan.json +0 -0
  76. {socketsecurity-2.2.59 → socketsecurity-2.2.60}/tests/data/fullscans/head_scan/stream_scan_full.json +0 -0
  77. {socketsecurity-2.2.59 → socketsecurity-2.2.60}/tests/data/fullscans/new_scan/metadata.json +0 -0
  78. {socketsecurity-2.2.59 → socketsecurity-2.2.60}/tests/data/fullscans/new_scan/stream_scan.json +0 -0
  79. {socketsecurity-2.2.59 → socketsecurity-2.2.60}/tests/data/repos/repo_info_error.json +0 -0
  80. {socketsecurity-2.2.59 → socketsecurity-2.2.60}/tests/data/repos/repo_info_no_head.json +0 -0
  81. {socketsecurity-2.2.59 → socketsecurity-2.2.60}/tests/data/repos/repo_info_success.json +0 -0
  82. {socketsecurity-2.2.59 → socketsecurity-2.2.60}/tests/data/settings/security-policy.json +0 -0
  83. {socketsecurity-2.2.59 → socketsecurity-2.2.60}/tests/unit/__init__.py +0 -0
  84. {socketsecurity-2.2.59 → socketsecurity-2.2.60}/tests/unit/test_cli_config.py +0 -0
  85. {socketsecurity-2.2.59 → socketsecurity-2.2.60}/tests/unit/test_client.py +0 -0
  86. {socketsecurity-2.2.59 → socketsecurity-2.2.60}/tests/unit/test_config.py +0 -0
  87. {socketsecurity-2.2.59 → socketsecurity-2.2.60}/tests/unit/test_gitlab_auth.py +0 -0
  88. {socketsecurity-2.2.59 → socketsecurity-2.2.60}/tests/unit/test_gitlab_auth_fallback.py +0 -0
  89. {socketsecurity-2.2.59 → socketsecurity-2.2.60}/tests/unit/test_output.py +0 -0
  90. {socketsecurity-2.2.59 → socketsecurity-2.2.60}/workflows/bitbucket-pipelines.yml +0 -0
  91. {socketsecurity-2.2.59 → socketsecurity-2.2.60}/workflows/github-actions.yml +0 -0
  92. {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.59
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.22
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 `SOCKET_SECURITY_API_KEY`
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 SOCKET_SECURITY_API_KEY env var) |
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
- | SOCKET_SLACK_ENABLED | False | false | Enables/Disables the Slack Plugin |
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
- - `SOCKET_SECURITY_API_KEY`: Socket Security API token (alternative to --api-token parameter)
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 `SOCKET_SECURITY_API_KEY`
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 SOCKET_SECURITY_API_KEY env var) |
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
- | SOCKET_SLACK_ENABLED | False | false | Enables/Disables the Slack Plugin |
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
- - `SOCKET_SECURITY_API_KEY`: Socket Security API token (alternative to --api-token parameter)
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.59"
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
- 'socketdev>=3.0.22,<4.0.0',
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"
@@ -1,3 +1,3 @@
1
1
  __author__ = 'socket.dev'
2
- __version__ = '2.2.59'
2
+ __version__ = '2.2.60'
3
3
  USER_AGENT = f'SocketPythonCLI/{__version__}'
@@ -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 = os.getenv("SOCKET_SECURITY_API_KEY") or args.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=os.getenv("SOCKET_SLACK_ENABLED", "false").lower() == "true",
174
+ enabled=bool(slack_config) or bool(args.slack_webhook),
162
175
  levels=os.getenv("SOCKET_SLACK_LEVELS", "block,warn").split(","),
163
- config=get_plugin_config_from_env("SOCKET_SLACK")
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 SOCKET_SECURITY_API_KEY env var)",
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
+ }
@@ -0,0 +1,5 @@
1
+ """Formatters for Socket security data output."""
2
+
3
+ from .slack import format_socket_facts_for_slack
4
+
5
+ __all__ = ['format_socket_facts_for_slack']