socketsecurity 2.2.93__tar.gz → 2.3.1__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 (137) hide show
  1. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/CHANGELOG.md +73 -0
  2. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/PKG-INFO +45 -1
  3. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/README.md +42 -0
  4. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/pyproject.toml +3 -1
  5. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/socketsecurity/__init__.py +1 -1
  6. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/socketsecurity/config.py +30 -0
  7. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/socketsecurity/core/__init__.py +139 -6
  8. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/socketsecurity/socketcli.py +59 -7
  9. socketsecurity-2.3.1/tests/core/test_facts_compression.py +137 -0
  10. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/tests/core/test_sdk_methods.py +1 -0
  11. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/tests/unit/test_cli_config.py +39 -0
  12. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/tests/unit/test_socketcli.py +96 -0
  13. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/uv.lock +108 -2
  14. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/.github/CODEOWNERS +0 -0
  15. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/.github/PULL_REQUEST_TEMPLATE/bug-fix.md +0 -0
  16. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/.github/PULL_REQUEST_TEMPLATE/feature.md +0 -0
  17. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/.github/PULL_REQUEST_TEMPLATE/improvement.md +0 -0
  18. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  19. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/.github/dependabot.yml +0 -0
  20. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/.github/workflows/dependabot-review.yml +0 -0
  21. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/.github/workflows/docker-stable.yml +0 -0
  22. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/.github/workflows/e2e-test.yml +0 -0
  23. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/.github/workflows/pr-preview.yml +0 -0
  24. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/.github/workflows/python-tests.yml +0 -0
  25. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/.github/workflows/release.yml +0 -0
  26. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/.github/workflows/version-check.yml +0 -0
  27. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/.github/zizmor.yml +0 -0
  28. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/.gitignore +0 -0
  29. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/.hooks/sync_version.py +0 -0
  30. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/.pre-commit-config.yaml +0 -0
  31. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/.python-version +0 -0
  32. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/Dockerfile +0 -0
  33. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/LICENSE +0 -0
  34. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/Makefile +0 -0
  35. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/docs/ci-cd.md +0 -0
  36. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/docs/cli-reference.md +0 -0
  37. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/docs/development.md +0 -0
  38. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/docs/troubleshooting.md +0 -0
  39. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/examples/config/sarif-dashboard-parity.json +0 -0
  40. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/examples/config/sarif-dashboard-parity.toml +0 -0
  41. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/examples/config/sarif-diff-ci-cd.json +0 -0
  42. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/examples/config/sarif-diff-ci-cd.toml +0 -0
  43. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/examples/config/sarif-instance-detail.json +0 -0
  44. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/examples/config/sarif-instance-detail.toml +0 -0
  45. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/instructions/gitlab-commit-status/uat.md +0 -0
  46. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/pytest.ini +0 -0
  47. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/scripts/build_container.sh +0 -0
  48. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/scripts/build_container_flexible.sh +0 -0
  49. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/scripts/deploy-test-docker.sh +0 -0
  50. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/scripts/deploy-test-pypi.sh +0 -0
  51. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/scripts/docker-entrypoint.sh +0 -0
  52. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/scripts/run.sh +0 -0
  53. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/session.md +0 -0
  54. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/socket.yml +0 -0
  55. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/socketsecurity/core/alert_selection.py +0 -0
  56. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/socketsecurity/core/classes.py +0 -0
  57. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/socketsecurity/core/cli_client.py +0 -0
  58. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/socketsecurity/core/exceptions.py +0 -0
  59. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/socketsecurity/core/git_interface.py +0 -0
  60. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/socketsecurity/core/helper/__init__.py +0 -0
  61. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/socketsecurity/core/helper/socket_facts_loader.py +0 -0
  62. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/socketsecurity/core/lazy_file_loader.py +0 -0
  63. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/socketsecurity/core/logging.py +0 -0
  64. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/socketsecurity/core/messages.py +0 -0
  65. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/socketsecurity/core/resource_utils.py +0 -0
  66. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/socketsecurity/core/scm/__init__.py +0 -0
  67. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/socketsecurity/core/scm/base.py +0 -0
  68. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/socketsecurity/core/scm/client.py +0 -0
  69. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/socketsecurity/core/scm/github.py +0 -0
  70. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/socketsecurity/core/scm/gitlab.py +0 -0
  71. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/socketsecurity/core/scm_comments.py +0 -0
  72. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/socketsecurity/core/socket_config.py +0 -0
  73. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/socketsecurity/core/tools/reachability.py +0 -0
  74. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/socketsecurity/core/utils.py +0 -0
  75. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/socketsecurity/fossa_compat.py +0 -0
  76. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/socketsecurity/output.py +0 -0
  77. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/socketsecurity/plugins/__init__.py +0 -0
  78. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/socketsecurity/plugins/base.py +0 -0
  79. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/socketsecurity/plugins/formatters/__init__.py +0 -0
  80. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/socketsecurity/plugins/formatters/slack.py +0 -0
  81. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/socketsecurity/plugins/jira.py +0 -0
  82. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/socketsecurity/plugins/manager.py +0 -0
  83. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/socketsecurity/plugins/slack.py +0 -0
  84. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/socketsecurity/plugins/teams.py +0 -0
  85. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/socketsecurity/plugins/webhook.py +0 -0
  86. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/tests/__init__.py +0 -0
  87. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/tests/core/conftest.py +0 -0
  88. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/tests/core/create_diff_input.json +0 -0
  89. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/tests/core/test_diff_alerts.py +0 -0
  90. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/tests/core/test_diff_generation.py +0 -0
  91. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/tests/core/test_has_manifest_files.py +0 -0
  92. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/tests/core/test_package_and_alerts.py +0 -0
  93. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/tests/core/test_supporting_methods.py +0 -0
  94. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/tests/data/fullscans/create_response.json +0 -0
  95. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/tests/data/fullscans/diff/stream_diff.json +0 -0
  96. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/tests/data/fullscans/diff/stream_diff_full.json +0 -0
  97. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/tests/data/fullscans/head_scan/metadata.json +0 -0
  98. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/tests/data/fullscans/head_scan/stream_scan.json +0 -0
  99. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/tests/data/fullscans/head_scan/stream_scan_full.json +0 -0
  100. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/tests/data/fullscans/new_scan/metadata.json +0 -0
  101. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/tests/data/fullscans/new_scan/stream_scan.json +0 -0
  102. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/tests/data/repos/repo_info_error.json +0 -0
  103. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/tests/data/repos/repo_info_no_head.json +0 -0
  104. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/tests/data/repos/repo_info_success.json +0 -0
  105. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/tests/data/settings/security-policy.json +0 -0
  106. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/tests/e2e/fixtures/simple-npm/index.js +0 -0
  107. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/tests/e2e/fixtures/simple-npm/package.json +0 -0
  108. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/tests/e2e/fixtures/simple-pypi/requirements.txt +0 -0
  109. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/tests/e2e/validate-gitlab.sh +0 -0
  110. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/tests/e2e/validate-json.sh +0 -0
  111. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/tests/e2e/validate-reachability.sh +0 -0
  112. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/tests/e2e/validate-sarif.sh +0 -0
  113. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/tests/e2e/validate-scan.sh +0 -0
  114. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/tests/fixtures/fossa/README.md +0 -0
  115. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/tests/fixtures/fossa/fossa-analyze-empty.json +0 -0
  116. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/tests/fixtures/fossa/fossa-analyze-populated.json +0 -0
  117. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/tests/fixtures/fossa/fossa-sbom-empty-deep.json +0 -0
  118. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/tests/fixtures/fossa/fossa-sbom-populated.json +0 -0
  119. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/tests/unit/__init__.py +0 -0
  120. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/tests/unit/test_alert_selection.py +0 -0
  121. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/tests/unit/test_client.py +0 -0
  122. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/tests/unit/test_config.py +0 -0
  123. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/tests/unit/test_dependency_overview.py +0 -0
  124. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/tests/unit/test_disable_ignore.py +0 -0
  125. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/tests/unit/test_fossa_compat.py +0 -0
  126. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/tests/unit/test_fossa_parity.py +0 -0
  127. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/tests/unit/test_gitlab_auth.py +0 -0
  128. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/tests/unit/test_gitlab_auth_fallback.py +0 -0
  129. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/tests/unit/test_gitlab_commit_status.py +0 -0
  130. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/tests/unit/test_gitlab_format.py +0 -0
  131. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/tests/unit/test_ignore_telemetry_filtering.py +0 -0
  132. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/tests/unit/test_output.py +0 -0
  133. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/tests/unit/test_slack_plugin.py +0 -0
  134. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/workflows/bitbucket-pipelines.yml +0 -0
  135. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/workflows/buildkite.yml +0 -0
  136. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/workflows/github-actions.yml +0 -0
  137. {socketsecurity-2.2.93 → socketsecurity-2.3.1}/workflows/gitlab-ci.yml +0 -0
@@ -1,5 +1,78 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.3.1
4
+
5
+ ### New: brotli-compressed `.socket.facts.json` upload
6
+
7
+ The reachability facts file (`.socket.facts.json`) is now brotli-compressed before it is
8
+ uploaded as part of a full scan. The Socket API transparently decompresses any multipart
9
+ part named exactly `.socket.facts.json.br` and stores it as plain `.socket.facts.json`, so
10
+ the stored result is unchanged — but the on-the-wire payload shrinks dramatically (a
11
+ ~262 MB facts file compresses to roughly 15–30 MB).
12
+
13
+ This fixes large tier‑1 reachability scans that previously failed when the uncompressed
14
+ facts file exceeded the API's per‑file upload size cap (surfaced to the CLI as an HTTP
15
+ 4xx/“502”, leaving the scan stuck with no report).
16
+
17
+ Details:
18
+
19
+ - Compression happens at the upload boundary (`Core.create_full_scan`); the file on disk is
20
+ left untouched, so local consumers (SARIF/JSON output, tier‑1 finalize, alert selection)
21
+ continue to read the plain `.socket.facts.json`.
22
+ - Only a file whose basename is exactly `.socket.facts.json` is compressed (the API matches
23
+ that exact name). A custom `--reach-output-file` name is uploaded uncompressed, as before.
24
+ - Empty baseline-scan placeholder files are not compressed.
25
+ - Compression never blocks an upload: if it fails for any reason it falls back to uploading
26
+ the plain file, and a partially-written `.socket.facts.json.br` is removed rather than
27
+ left behind in the target directory.
28
+ - Adds a `brotli` (CPython) / `brotlicffi` (PyPy) dependency.
29
+
30
+ ## 2.3.0
31
+
32
+ ### New: `--exit-code-on-api-error`
33
+
34
+ Adds a configurable exit code for API / infrastructure failures (timeouts,
35
+ network errors, unexpected exceptions), so CI pipelines can distinguish them
36
+ from blocking security findings (exit `1`):
37
+
38
+ ```
39
+ socketcli --exit-code-on-api-error 100 ...
40
+ ```
41
+
42
+ Default is `3` (the code the CLI already used for these errors), so **default
43
+ behavior is unchanged** — the exit code only changes when you pass the flag.
44
+ Set it to a Buildkite `soft_fail` code, or to `0` to swallow infra errors.
45
+
46
+ **Interaction to be aware of:** `--disable-blocking` forces exit `0` for *all*
47
+ outcomes and therefore overrides `--exit-code-on-api-error`. Use the new flag
48
+ *without* `--disable-blocking` if you want a custom infra-error code to take
49
+ effect. See the exit-code reference in the README.
50
+
51
+ > A future `3.0` release is planned to make infrastructure errors exit non-zero
52
+ > even under `--disable-blocking` (so outages stop being silently swallowed).
53
+ > That is a breaking change and is intentionally **not** in this release.
54
+
55
+ ### New: commit message auto-truncation
56
+
57
+ `--commit-message` values longer than 200 characters are now automatically
58
+ truncated before being sent to the API, preventing HTTP 413 errors from
59
+ oversized URL query parameters (common with AI-generated commit messages or
60
+ `$BUILDKITE_MESSAGE`).
61
+
62
+ ### Improved: Buildkite log formatting
63
+
64
+ When running inside a Buildkite job (`BUILDKITE=true`), infrastructure errors
65
+ emit Buildkite log section markers (`^^^ +++` / `--- :warning:`) so the error
66
+ section auto-expands in the BK UI, plus a `soft_fail` hint. No effect on other
67
+ CI platforms.
68
+
69
+ ### Fixed
70
+
71
+ - `--timeout` is now honored end-to-end: it was only applied to the local
72
+ `CliClient`, but the full-scan diff comparison uses the Socket SDK instance,
73
+ which was constructed without the CLI timeout and defaulted to 1200s.
74
+ - `--exclude-license-details` now propagates to the full-scan diff comparison
75
+ request (it was only applied to full-scan params / report URLs before).
3
76
  ## 2.2.93
4
77
 
5
78
  - Bundled twelve Dependabot dependency updates: `urllib3`, `gitpython`, `python-dotenv`, `pytest`, `uv`, `cryptography`, `pygments`, `requests`, and `idna` (main app), plus `axios`, `requests`, and `flask` (e2e fixtures). `idna` 3.11 → 3.15 includes the fix for CVE-2026-45409.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: socketsecurity
3
- Version: 2.2.93
3
+ Version: 2.3.1
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>
@@ -33,6 +33,8 @@ Classifier: Intended Audience :: Developers
33
33
  Classifier: Programming Language :: Python :: 3.11
34
34
  Classifier: Programming Language :: Python :: 3.12
35
35
  Requires-Python: >=3.11
36
+ Requires-Dist: brotli>=1.0.9; platform_python_implementation == 'CPython'
37
+ Requires-Dist: brotlicffi>=1.0.9; platform_python_implementation != 'CPython'
36
38
  Requires-Dist: bs4>=0.0.2
37
39
  Requires-Dist: gitpython
38
40
  Requires-Dist: markdown>=3.10
@@ -252,6 +254,48 @@ Minimal pattern:
252
254
  SOCKET_SECURITY_API_TOKEN: ${{ secrets.SOCKET_SECURITY_API_TOKEN }}
253
255
  ```
254
256
 
257
+ ## Exit codes
258
+
259
+ | Code | Meaning |
260
+ |------|---------|
261
+ | `0` | Clean scan — no blocking issues (or `--disable-blocking` set) |
262
+ | `1` | Blocking security finding(s) detected |
263
+ | `2` | Scan interrupted (SIGINT / Ctrl+C) |
264
+ | `3` | Infrastructure or API error (timeout, network failure, unexpected error) |
265
+
266
+ `--exit-code-on-api-error <N>` remaps the infrastructure-error code (`3`) to any
267
+ value — e.g. a Buildkite `soft_fail` code, or `0` to swallow infra errors. Exit
268
+ `3` is a Socket convention, not an industry standard.
269
+
270
+ ### How these options interact
271
+
272
+ The two flags that affect exit codes can cancel each other out, so the order of
273
+ precedence matters:
274
+
275
+ - **`--disable-blocking` wins over everything.** It forces exit `0` for *all*
276
+ outcomes — security findings *and* infrastructure errors. If you set it,
277
+ `--exit-code-on-api-error` has no effect (you'll always get `0`).
278
+ - **`--exit-code-on-api-error` only applies when `--disable-blocking` is *not*
279
+ set.** It changes the infra-error code (and the generic-error code); it never
280
+ touches the security-finding code (`1`).
281
+
282
+ So for the common "don't let Socket outages block my pipeline, but still fail on
283
+ real findings" goal, use `--exit-code-on-api-error` **without** `--disable-blocking`:
284
+
285
+ ```yaml
286
+ # Buildkite: soft-fail only on infrastructure errors, still block on findings
287
+ steps:
288
+ - label: ":lock: Socket Security Scan"
289
+ command: "socketcli --exit-code-on-api-error 100 ..." # NOT --disable-blocking
290
+ soft_fail:
291
+ - exit_status: 100
292
+ ```
293
+
294
+ Combining `--disable-blocking` with `--exit-code-on-api-error 100` would make the
295
+ scan exit `0` on *both* findings and outages — the `soft_fail: 100` rule would
296
+ never match, and real findings would stop blocking. That's usually not what you
297
+ want.
298
+
255
299
  ## Common gotchas
256
300
 
257
301
  See [`docs/troubleshooting.md`](https://github.com/SocketDev/socket-python-cli/blob/main/docs/troubleshooting.md#common-gotchas).
@@ -194,6 +194,48 @@ Minimal pattern:
194
194
  SOCKET_SECURITY_API_TOKEN: ${{ secrets.SOCKET_SECURITY_API_TOKEN }}
195
195
  ```
196
196
 
197
+ ## Exit codes
198
+
199
+ | Code | Meaning |
200
+ |------|---------|
201
+ | `0` | Clean scan — no blocking issues (or `--disable-blocking` set) |
202
+ | `1` | Blocking security finding(s) detected |
203
+ | `2` | Scan interrupted (SIGINT / Ctrl+C) |
204
+ | `3` | Infrastructure or API error (timeout, network failure, unexpected error) |
205
+
206
+ `--exit-code-on-api-error <N>` remaps the infrastructure-error code (`3`) to any
207
+ value — e.g. a Buildkite `soft_fail` code, or `0` to swallow infra errors. Exit
208
+ `3` is a Socket convention, not an industry standard.
209
+
210
+ ### How these options interact
211
+
212
+ The two flags that affect exit codes can cancel each other out, so the order of
213
+ precedence matters:
214
+
215
+ - **`--disable-blocking` wins over everything.** It forces exit `0` for *all*
216
+ outcomes — security findings *and* infrastructure errors. If you set it,
217
+ `--exit-code-on-api-error` has no effect (you'll always get `0`).
218
+ - **`--exit-code-on-api-error` only applies when `--disable-blocking` is *not*
219
+ set.** It changes the infra-error code (and the generic-error code); it never
220
+ touches the security-finding code (`1`).
221
+
222
+ So for the common "don't let Socket outages block my pipeline, but still fail on
223
+ real findings" goal, use `--exit-code-on-api-error` **without** `--disable-blocking`:
224
+
225
+ ```yaml
226
+ # Buildkite: soft-fail only on infrastructure errors, still block on findings
227
+ steps:
228
+ - label: ":lock: Socket Security Scan"
229
+ command: "socketcli --exit-code-on-api-error 100 ..." # NOT --disable-blocking
230
+ soft_fail:
231
+ - exit_status: 100
232
+ ```
233
+
234
+ Combining `--disable-blocking` with `--exit-code-on-api-error 100` would make the
235
+ scan exit `0` on *both* findings and outages — the `soft_fail: 100` rule would
236
+ never match, and real findings would stop blocking. That's usually not what you
237
+ want.
238
+
197
239
  ## Common gotchas
198
240
 
199
241
  See [`docs/troubleshooting.md`](https://github.com/SocketDev/socket-python-cli/blob/main/docs/troubleshooting.md#common-gotchas).
@@ -6,7 +6,7 @@ build-backend = "hatchling.build"
6
6
 
7
7
  [project]
8
8
  name = "socketsecurity"
9
- version = "2.2.93"
9
+ version = "2.3.1"
10
10
  requires-python = ">= 3.11"
11
11
  license = {"file" = "LICENSE"}
12
12
  dependencies = [
@@ -19,6 +19,8 @@ dependencies = [
19
19
  "socketdev>=3.0.33,<4.0.0",
20
20
  "bs4>=0.0.2",
21
21
  "markdown>=3.10",
22
+ "brotli>=1.0.9; platform_python_implementation == 'CPython'",
23
+ "brotlicffi>=1.0.9; platform_python_implementation != 'CPython'",
22
24
  ]
23
25
  readme = "README.md"
24
26
  description = "Socket Security CLI for CI/CD"
@@ -1,3 +1,3 @@
1
1
  __author__ = 'socket.dev'
2
- __version__ = '2.2.93'
2
+ __version__ = '2.3.1'
3
3
  USER_AGENT = f'SocketPythonCLI/{__version__}'
@@ -101,6 +101,7 @@ class CliConfig:
101
101
  pending_head: bool = False
102
102
  enable_diff: bool = False
103
103
  timeout: Optional[int] = 1200
104
+ exit_code_on_api_error: int = 3
104
105
  exclude_license_details: bool = False
105
106
  include_module_folders: bool = False
106
107
  repo_is_public: bool = False
@@ -182,6 +183,19 @@ class CliConfig:
182
183
  if commit_message and commit_message.startswith('"') and commit_message.endswith('"'):
183
184
  commit_message = commit_message[1:-1]
184
185
 
186
+ # Truncate to avoid 413s from oversized URL query parameters.
187
+ # The API has no application-layer length validation on commit_message;
188
+ # the 413 originates from an infrastructure-layer URL length limit
189
+ # (nginx/Cloudflare). 200 chars chosen as a conservative ceiling given
190
+ # URL encoding can 2-3x raw character count.
191
+ MAX_COMMIT_MESSAGE_LENGTH = 200
192
+ if commit_message and len(commit_message) > MAX_COMMIT_MESSAGE_LENGTH:
193
+ logging.debug(
194
+ f"commit_message truncated from {len(commit_message)} to "
195
+ f"{MAX_COMMIT_MESSAGE_LENGTH} characters to avoid API request size limits"
196
+ )
197
+ commit_message = commit_message[:MAX_COMMIT_MESSAGE_LENGTH]
198
+
185
199
  config_args = {
186
200
  'api_token': api_token,
187
201
  'repo': args.repo,
@@ -219,6 +233,7 @@ class CliConfig:
219
233
  'integration_type': args.integration,
220
234
  'pending_head': args.pending_head,
221
235
  'timeout': args.timeout,
236
+ 'exit_code_on_api_error': args.exit_code_on_api_error,
222
237
  'exclude_license_details': args.exclude_license_details,
223
238
  'include_module_folders': args.include_module_folders,
224
239
  'repo_is_public': args.repo_is_public,
@@ -802,6 +817,21 @@ def create_argument_parser() -> argparse.ArgumentParser:
802
817
  help="Timeout in seconds for API requests",
803
818
  required=False
804
819
  )
820
+ advanced_group.add_argument(
821
+ "--exit-code-on-api-error",
822
+ dest="exit_code_on_api_error",
823
+ type=int,
824
+ default=3,
825
+ metavar="<int>",
826
+ help=(
827
+ "Exit code to use when the CLI fails on an API or infrastructure error "
828
+ "(timeout, network failure, unexpected exception). Default: 3. Useful for "
829
+ "distinguishing infrastructure failures from security findings (exit 1) in "
830
+ "CI -- e.g. set to a Buildkite soft_fail code. NOTE: --disable-blocking "
831
+ "forces exit 0 for ALL outcomes and therefore overrides this flag; do not "
832
+ "combine the two if you want the custom code to take effect."
833
+ )
834
+ )
805
835
  advanced_group.add_argument(
806
836
  "--allow-unverified",
807
837
  action="store_true",
@@ -51,6 +51,26 @@ _ALERT_TYPE_TITLE_OVERRIDES = {
51
51
 
52
52
  _HUMANIZE_BOUNDARY = re.compile(r"(?<=[a-z0-9])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])")
53
53
 
54
+ # Reachability facts-file upload compression.
55
+ #
56
+ # The Socket full-scan endpoint transparently brotli-decompresses any multipart part
57
+ # whose basename is exactly ``.socket.facts.json.br`` and stores it as plain
58
+ # ``.socket.facts.json``. Compressing the facts file on upload keeps it well under the
59
+ # server's per-file size cap (a ~262 MB facts file compresses to roughly 15-30 MB),
60
+ # which is required for large reachability (tier 1) scans to succeed.
61
+ #
62
+ # The server matches the *exact* name ``.socket.facts.json.br``, so we only compress
63
+ # files whose basename is exactly ``.socket.facts.json`` (a custom ``--reach-output-file``
64
+ # name would not be decompressed server-side, so it is left as a plain upload).
65
+ SOCKET_FACTS_FILENAME = ".socket.facts.json"
66
+ SOCKET_FACTS_BROTLI_FILENAME = ".socket.facts.json.br"
67
+ # Brotli quality (0-11); 5 is a good speed/ratio tradeoff for large JSON payloads.
68
+ SOCKET_FACTS_BROTLI_QUALITY = 5
69
+ # Largest brotli window (2**24 bytes); improves the ratio on large facts files.
70
+ SOCKET_FACTS_BROTLI_LGWIN = 24
71
+ # Stream the facts file in 1 MiB chunks so large files aren't held fully in memory.
72
+ SOCKET_FACTS_BROTLI_CHUNK_SIZE = 1024 * 1024
73
+
54
74
 
55
75
  def _humanize_alert_type(alert_type: str) -> str:
56
76
  """Convert a camelCase/PascalCase alert type into a Title-Cased label.
@@ -544,6 +564,102 @@ class Core:
544
564
  log.debug(f"Unable to finalize tier 1 scan: {e}")
545
565
  return False
546
566
 
567
+ @staticmethod
568
+ def _compress_facts_file(source_path: str) -> str:
569
+ """Brotli-compress a ``.socket.facts.json`` file to a sibling ``.socket.facts.json.br``.
570
+
571
+ The source is streamed in chunks so a large facts file (hundreds of MB) never has
572
+ to be held in memory at once. The compressed file is written next to the source so
573
+ that the multipart key the SDK derives keeps the same directory prefix, only with a
574
+ ``.br`` basename. Any existing ``.socket.facts.json.br`` sibling is overwritten, and a
575
+ partially-written output is removed if compression fails part-way through (e.g. the
576
+ disk fills up mid-stream) so no orphaned ``.br`` is left in the target directory.
577
+
578
+ Args:
579
+ source_path: Path to the plain ``.socket.facts.json`` file.
580
+
581
+ Returns:
582
+ Path to the compressed sibling file.
583
+ """
584
+ # Imported lazily so the dependency is only needed when actually uploading a facts
585
+ # file. brotlicffi is the API-compatible fallback used on PyPy / non-CPython runtimes.
586
+ try:
587
+ import brotli
588
+ except ImportError:
589
+ import brotlicffi as brotli
590
+
591
+ target_path = os.path.join(os.path.dirname(source_path), SOCKET_FACTS_BROTLI_FILENAME)
592
+ compressor = brotli.Compressor(
593
+ quality=SOCKET_FACTS_BROTLI_QUALITY,
594
+ lgwin=SOCKET_FACTS_BROTLI_LGWIN,
595
+ )
596
+ try:
597
+ with open(source_path, "rb") as src, open(target_path, "wb") as dst:
598
+ while True:
599
+ chunk = src.read(SOCKET_FACTS_BROTLI_CHUNK_SIZE)
600
+ if not chunk:
601
+ break
602
+ compressed = compressor.process(chunk)
603
+ if compressed:
604
+ dst.write(compressed)
605
+ dst.write(compressor.finish())
606
+ except BaseException:
607
+ # Don't leave a half-written .br behind for the caller to miss (it only tracks
608
+ # the path for cleanup once this returns). Remove it, then re-raise so the caller
609
+ # falls back to uploading the plain file.
610
+ try:
611
+ os.unlink(target_path)
612
+ except OSError:
613
+ pass
614
+ raise
615
+ return target_path
616
+
617
+ def _compress_facts_files_for_upload(self, files: List[str]) -> Tuple[List[str], List[str]]:
618
+ """Replace any ``.socket.facts.json`` upload entry with a brotli-compressed ``.br`` sibling.
619
+
620
+ The Socket full-scan endpoint transparently decompresses a multipart part named
621
+ exactly ``.socket.facts.json.br``, so compressing here keeps a large facts file under
622
+ the server's per-file size cap without changing the stored result. Files whose
623
+ basename is not exactly ``.socket.facts.json`` are left untouched (the server only
624
+ matches that exact name), as are empty placeholder files (e.g. baseline scans).
625
+
626
+ Compression never blocks an upload: if it fails for any reason (missing optional
627
+ ``brotli`` dependency, unwritable directory, etc.) the original plain file is used.
628
+
629
+ Args:
630
+ files: The list of file paths about to be uploaded.
631
+
632
+ Returns:
633
+ ``(upload_files, temp_paths)`` where ``upload_files`` is the possibly-rewritten
634
+ list to upload and ``temp_paths`` are compressed files the caller must delete
635
+ once the upload completes.
636
+ """
637
+ upload_files: List[str] = []
638
+ temp_paths: List[str] = []
639
+ for file_path in files:
640
+ try:
641
+ if (
642
+ os.path.basename(file_path) == SOCKET_FACTS_FILENAME
643
+ and os.path.isfile(file_path)
644
+ and os.path.getsize(file_path) > 0
645
+ ):
646
+ compressed_path = self._compress_facts_file(file_path)
647
+ log.debug(
648
+ f"Brotli-compressed {file_path} for upload: "
649
+ f"{os.path.getsize(file_path)} -> {os.path.getsize(compressed_path)} bytes "
650
+ f"(uploading as {SOCKET_FACTS_BROTLI_FILENAME})"
651
+ )
652
+ upload_files.append(compressed_path)
653
+ temp_paths.append(compressed_path)
654
+ continue
655
+ except Exception as e:
656
+ # Never let compression break an upload: fall back to the plain file.
657
+ log.warning(
658
+ f"Failed to brotli-compress facts file {file_path}, uploading uncompressed: {e}"
659
+ )
660
+ upload_files.append(file_path)
661
+ return upload_files, temp_paths
662
+
547
663
  def create_full_scan(self, files: List[str], params: FullScanParams, base_paths: Optional[List[str]] = None) -> FullScan:
548
664
  """
549
665
  Creates a new full scan via the Socket API.
@@ -559,7 +675,19 @@ class Core:
559
675
  log.info("Creating new full scan")
560
676
  create_full_start = time.time()
561
677
 
562
- res = self.sdk.fullscans.post(files, params, use_types=True, use_lazy_loading=True, max_open_files=50, base_paths=base_paths)
678
+ # Brotli-compress the reachability facts file (if present) so it is uploaded as a
679
+ # `.socket.facts.json.br` part. The API decompresses it server-side, keeping a large
680
+ # facts file under the per-file upload size cap. See _compress_facts_files_for_upload.
681
+ upload_files, compressed_temp_files = self._compress_facts_files_for_upload(files)
682
+ try:
683
+ res = self.sdk.fullscans.post(upload_files, params, use_types=True, use_lazy_loading=True, max_open_files=50, base_paths=base_paths)
684
+ finally:
685
+ for temp_file in compressed_temp_files:
686
+ try:
687
+ os.unlink(temp_file)
688
+ log.debug(f"Cleaned up temporary compressed facts file: {temp_file}")
689
+ except OSError as cleanup_error:
690
+ log.debug(f"Failed to clean up temporary compressed facts file {temp_file}: {cleanup_error}")
563
691
  if not res.success:
564
692
  log.error(f"Error creating full scan: {res.message}, status: {res.status}")
565
693
  raise Exception(f"Error creating full scan: {res.message}, status: {res.status}")
@@ -941,7 +1069,8 @@ class Core:
941
1069
  def get_added_and_removed_packages(
942
1070
  self,
943
1071
  head_full_scan_id: str,
944
- new_full_scan_id: str
1072
+ new_full_scan_id: str,
1073
+ include_license_details: bool = True
945
1074
  ) -> Tuple[Dict[str, Package], Dict[str, Package], Dict[str, Package]]:
946
1075
  """
947
1076
  Get packages that were added and removed between scans.
@@ -958,12 +1087,12 @@ class Core:
958
1087
  diff_start = time.time()
959
1088
  try:
960
1089
  diff_report = (
961
- self.sdk.fullscans.stream_diff
962
- (
1090
+ self.sdk.fullscans.stream_diff(
963
1091
  self.config.org_slug,
964
1092
  head_full_scan_id,
965
1093
  new_full_scan_id,
966
- use_types=True
1094
+ use_types=True,
1095
+ include_license_details=str(include_license_details).lower()
967
1096
  ).data
968
1097
  )
969
1098
  except APIFailure as e:
@@ -1175,7 +1304,11 @@ class Core:
1175
1304
  added_packages,
1176
1305
  removed_packages,
1177
1306
  packages
1178
- ) = self.get_added_and_removed_packages(head_full_scan_id, new_full_scan.id)
1307
+ ) = self.get_added_and_removed_packages(
1308
+ head_full_scan_id,
1309
+ new_full_scan.id,
1310
+ include_license_details=getattr(params, "include_license_details", True)
1311
+ )
1179
1312
 
1180
1313
  # Separate unchanged packages from added/removed for --strict-blocking support
1181
1314
  unchanged_packages = {
@@ -27,6 +27,37 @@ socket_logger, log = initialize_logging()
27
27
 
28
28
  load_dotenv()
29
29
 
30
+ # Buildkite sets BUILDKITE=true in every job environment. Used to gate log
31
+ # section markers that would render as literal text on other CI platforms.
32
+ IS_BUILDKITE = os.getenv("BUILDKITE") == "true"
33
+
34
+
35
+ def _emit_infrastructure_error(message: str, include_traceback: bool = False) -> None:
36
+ """Emit a structured error for infrastructure/API failures.
37
+
38
+ When running in Buildkite, wraps the error in log-section markers
39
+ (`^^^ +++` expands the section in the BK UI) and prints a soft_fail hint.
40
+ On every other platform it's a plain log.error so the markers don't leak
41
+ as literal text. This is presentation only -- it does not decide the exit
42
+ code (the caller does that, honoring --disable-blocking and
43
+ --exit-code-on-api-error).
44
+ """
45
+ if IS_BUILDKITE:
46
+ print("^^^ +++", flush=True)
47
+ print("--- :warning: Socket infrastructure error", flush=True)
48
+
49
+ log.error(message)
50
+
51
+ if IS_BUILDKITE:
52
+ log.error(
53
+ "Tip: this is an infrastructure error, not a security finding. To keep it "
54
+ "from blocking the build, add a soft_fail rule for the CLI's API-error exit "
55
+ "code (default 3, or whatever you pass to --exit-code-on-api-error)."
56
+ )
57
+
58
+ if include_traceback:
59
+ traceback.print_exc()
60
+
30
61
 
31
62
  def build_license_artifact_payload(
32
63
  diff: Diff,
@@ -62,6 +93,23 @@ def _write_attribution_file(config, payload: dict) -> None:
62
93
  Core.save_file(config.license_file_name, json.dumps(payload, indent=2))
63
94
 
64
95
 
96
+ DEFAULT_API_TIMEOUT = 1200
97
+
98
+
99
+ def get_api_request_timeout(config: CliConfig) -> int:
100
+ return config.timeout if config.timeout is not None else DEFAULT_API_TIMEOUT
101
+
102
+
103
+ def build_socket_sdk(config: CliConfig) -> socketdev:
104
+ cli_user_agent_string = f"SocketPythonCLI/{config.version}"
105
+ return socketdev(
106
+ token=config.api_token,
107
+ timeout=get_api_request_timeout(config),
108
+ allow_unverified=config.allow_unverified,
109
+ user_agent=cli_user_agent_string
110
+ )
111
+
112
+
65
113
  def cli():
66
114
  try:
67
115
  main_code()
@@ -73,12 +121,17 @@ def cli():
73
121
  else:
74
122
  sys.exit(0)
75
123
  except Exception as error:
76
- log.error("Unexpected error when running the cli")
77
- log.error(error)
78
- traceback.print_exc()
79
124
  config = CliConfig.from_args() # Get current config
125
+ _emit_infrastructure_error(
126
+ f"Unexpected error when running the CLI: {error}",
127
+ include_traceback=True,
128
+ )
129
+ # --disable-blocking forces a clean exit for ALL outcomes (it takes
130
+ # precedence over --exit-code-on-api-error); otherwise infra/API errors
131
+ # exit with the configurable code (default 3), keeping them distinct
132
+ # from blocking security findings (exit 1).
80
133
  if not config.disable_blocking:
81
- sys.exit(3)
134
+ sys.exit(config.exit_code_on_api_error)
82
135
  else:
83
136
  sys.exit(0)
84
137
 
@@ -99,8 +152,7 @@ def main_code():
99
152
  "1. Command line: --api-token YOUR_TOKEN\n"
100
153
  "2. Environment variable: SOCKET_SECURITY_API_TOKEN")
101
154
  sys.exit(3)
102
- cli_user_agent_string = f"SocketPythonCLI/{config.version}"
103
- sdk = socketdev(token=config.api_token, allow_unverified=config.allow_unverified, user_agent=cli_user_agent_string)
155
+ sdk = build_socket_sdk(config)
104
156
 
105
157
  # Suppress urllib3 InsecureRequestWarning when using --allow-unverified
106
158
  if config.allow_unverified:
@@ -119,7 +171,7 @@ def main_code():
119
171
  socket_config = SocketConfig(
120
172
  api_key=config.api_token,
121
173
  allow_unverified_ssl=config.allow_unverified,
122
- timeout=config.timeout if config.timeout is not None else 1200 # Use CLI timeout if provided
174
+ timeout=get_api_request_timeout(config)
123
175
  )
124
176
  log.debug("loaded socket_config")
125
177
  client = CliClient(socket_config)