diff --git a/CHANGELOG.md b/CHANGELOG.md index 2fa5ad6..2860d82 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,32 @@ # Changelog +## 2.9.8 + +### Documentation: API token permissions + +- Documented the API calls a run actually makes, so a token can be provisioned without + trial and error: what every run calls, what diff-producing runs add, and what each + flag adds, with the scope names mapped to them. +- Recorded that the published CI/CD token setup guide does not list `diff-scans:create` + or `diff-scans:list`. Every diff-producing run needs both, so a token provisioned + exactly as that guide describes always falls back to the legacy comparison path. + Recorded the converse too: `socketcli` makes no triage or security-policy calls, so + three of the nine scopes that guide lists are not exercised by this CLI. +- Corrected the scan-comparison guidance. The `APIAccessDenied` fallback was documented + as a PR/MR-only condition; it applies to any run that produces a diff, which since + 2.9.0 means PR/MR events and runs without an SCM integration. Added a table of which + runs compare, and a note that before 2.9.0 SCM-integrated branch pushes compared too. + The guidance also listed `full-scans:list` alongside the two `diff-scans:*` scopes, + which points readers at a permission the fallback path demonstrably already has. +- Documented three outcomes that are expected but read as failures: the temporary empty + baseline created on a repository's first scan, which does not appear in the dashboard; + the reachability scan ID being a `tier1ReachabilityScanId` rather than a full scan ID, + so it does not resolve in the dashboard's scan views; and a repository permission gap + exiting `2`, a code the exit table otherwise documents as a keyboard interrupt and + which bypasses both `--disable-blocking` and `--exit-code-on-api-error`. +- Documented all four accepted API token environment variables in priority order, and + corrected a reference to `$SOCKET_API_TOKEN` in the GitLab requirements section that + did not match the documented `SOCKET_SECURITY_API_TOKEN`. ## 2.9.7 ### Changed: bump pinned @coana-tech/cli to 15.10.51 diff --git a/README.md b/README.md index 5221daf..0b47fd4 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,10 @@ pip install socketsecurity export SOCKET_SECURITY_API_TOKEN="" ``` +The token needs more than scan-creation access, and some paths fall back with only a +warning when a permission is missing. See +[`docs/troubleshooting.md#api-token-permissions`](https://github.com/SocketDev/socket-python-cli/blob/main/docs/troubleshooting.md#api-token-permissions). + ### 3) Run a basic scan ```bash @@ -221,7 +225,7 @@ Minimal pattern: |------|---------| | `0` | Clean scan — no blocking issues (or `--disable-blocking` set) | | `1` | Blocking security finding(s) detected | -| `2` | Scan interrupted (SIGINT / Ctrl+C) | +| `2` | Scan interrupted (SIGINT / Ctrl+C), or a repository lookup/creation failure | | `3` | Infrastructure or API error (timeout, network failure, unexpected error) | `--exit-code-on-api-error ` remaps the infrastructure-error code (`3`) to any @@ -246,6 +250,10 @@ precedence matters: set.** It changes the infra-error code (and the generic-error code); it never touches the security-finding code (`1`). +- **Neither flag covers exit `2`.** A repository lookup/creation failure (typically a + token permission gap) exits `2` directly and is not remapped by either flag. See + [`docs/troubleshooting.md#a-missing-repository-permission-can-exit-2`](https://github.com/SocketDev/socket-python-cli/blob/main/docs/troubleshooting.md#a-missing-repository-permission-can-exit-2). + So for the common "don't let Socket outages block my pipeline, but still fail on real findings" goal, use `--exit-code-on-api-error` **without** `--disable-blocking`: diff --git a/docs/ci-cd.md b/docs/ci-cd.md index 253c27c..7ccc6a0 100644 --- a/docs/ci-cd.md +++ b/docs/ci-cd.md @@ -546,3 +546,9 @@ Prebuilt examples in this repo: the console table, including native Buildkite job logs, and in GitHub/GitLab security comments when that SCM adapter is configured. Findings without a known patched release leave the console cell blank and omit the comment field. +- A token missing the `diff-scans:*` permissions does not fail the build — it logs + `Diff scan comparison failed with APIAccessDenied(Insufficient permissions)` and falls + back to a less resilient comparison path. This affects PR/MR events and the + `--scm api` / `--enable-diff` paths; since 2.9.0 an SCM-integrated branch push + creates a full scan instead and never reaches it. See + [`troubleshooting.md#api-token-permissions`](troubleshooting.md#api-token-permissions). diff --git a/docs/cli-reference.md b/docs/cli-reference.md index d0d85e2..7695c79 100644 --- a/docs/cli-reference.md +++ b/docs/cli-reference.md @@ -232,6 +232,21 @@ If you don't want to provide the Socket API Token every time then you can use th |:------------|:---------|:--------|:----------------------------------------------------------------------------------| | `--api-token` | False | | Socket Security API token (can also be set via SOCKET_SECURITY_API_TOKEN env var) | +The token is read from the first of these that is set, then from `--api-token`: + +1. `SOCKET_SECURITY_API_KEY` +2. `SOCKET_SECURITY_API_TOKEN` +3. `SOCKET_API_KEY` +4. `SOCKET_API_TOKEN` + +`SOCKET_SECURITY_API_TOKEN` is the documented name; the others are accepted for +compatibility. Prefer the documented name in new pipelines. + +> **Token permissions:** the CLI needs more than scan-creation access, and several +> paths degrade with only a warning when a permission is missing rather than failing. +> See [`troubleshooting.md#api-token-permissions`](troubleshooting.md#api-token-permissions) +> for the full list of API calls a run makes and which flags add to it. + #### Repository | Parameter | Required | Default | Description | |:-----------------|:---------|:--------|:------------------------------------------------------------------------------------------------------------------| @@ -898,7 +913,7 @@ When `--enable-gitlab-security` (or `--enable-json` / `--enable-sarif`) is used ### Requirements - **GitLab Version**: GitLab 12.0 or later (for Security Dashboard support) -- **Socket API Token**: Set via `$SOCKET_API_TOKEN` environment variable or `--api-token` parameter +- **Socket API Token**: Set via `SOCKET_SECURITY_API_TOKEN` environment variable or `--api-token` parameter (see [Authentication](#authentication) for the other accepted variable names) - **CI/CD Artifacts**: Reports must be uploaded as `dependency_scanning` artifacts ### Troubleshooting diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 9d6d06d..ac5a55b 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -1,27 +1,113 @@ # Troubleshooting -## API token scopes for scan comparisons +## API token permissions -PR/MR runs compare the new scan against the repository's head scan. That comparison -first uses the diff-scans endpoints, which require an organization token with these -scopes in addition to whatever the scan itself needs: +The CLI authenticates with a single Socket organization API token. Every run needs +more than "create a scan" access, and a token that is missing a permission usually +does **not** fail loudly — several paths degrade or fall back with only a warning. -- `diff-scans:create` -- `diff-scans:list` -- `full-scans:list` +### What every run calls -If the token is missing them the scan still succeeds, so this is easy to miss. The only -signal is a warning, after which the CLI falls back to the older streaming comparison: +These are exercised on any invocation, regardless of flags: + +| API call | Purpose | +|:---|:---| +| `GET organizations` | Resolve the org ID and slug from the token | +| `GET report/supported` | Fetch supported manifest patterns (falls back to a bundled list on failure) | +| `GET orgs/{org}/repos/{repo}` | Look up the repository | +| `POST orgs/{org}/repos` | Create the repository — **only attempted if the lookup above fails** | +| `POST orgs/{org}/full-scans` | Create the new scan | + +### What diff-producing runs add + +| API call | Purpose | +|:---|:---| +| `POST orgs/{org}/diff-scans/from-ids` | Create the comparison (preferred path) | +| `GET orgs/{org}/diff-scans` | Resolve an existing comparison on a 409 | +| `GET orgs/{org}/diff-scans/{id}?cached=true` | Poll for the computed comparison | +| `GET orgs/{org}/full-scans/diff` | Legacy streaming comparison (fallback path) | +| `GET orgs/{org}/full-scans/{id}` | Resolve a baseline for `--base-scan-id` / `--base-commit-sha` | + +### Which scopes to grant + +The [CI/CD token setup guide](https://docs.socket.dev/docs/create-socket-api-key-for-cicd) +tells you to select nine scopes: + +`repo:list`, `repo:create`, `repo:update`, `security-policy:read`, +`triage:alerts-list`, `triage:alerts-update`, `full-scans:list`, `full-scans:create`, +`packages:list` + +**That list is not sufficient for this CLI.** It does not include `diff-scans:create` +or `diff-scans:list`, which every diff-producing run needs. A token provisioned exactly +as that guide describes will always fall back to the legacy comparison path — see the +next section. Grant those two in addition. + +Going the other way, `socketcli` makes no triage or security-policy API calls at all, +so `security-policy:read`, `triage:alerts-list` and `triage:alerts-update` are not +exercised by this CLI. They are on the guide's list for other Socket tooling. + +Mapping the remaining scopes to the calls above (inferred from the names; the setup +guide does not publish a per-endpoint mapping): + +| Scope | Covers | +|:---|:---| +| `repo:list` | Repository lookup | +| `repo:create` | Repository creation on lookup failure | +| `repo:update` | Setting the scan as repository head / default branch | +| `full-scans:create` | Creating the new scan | +| `full-scans:list` | Reading scans, metadata, streams, and the legacy `full-scans/diff` comparison | +| `diff-scans:create` | Creating the comparison | +| `diff-scans:list` | Resolving and polling the comparison | +| `packages:list` | `POST purl` for license text | + +`GET organizations` and `GET report/supported` are not covered by any scope on the +guide's list and appear to be available to any valid org token. If you are provisioning +a least-privilege token and one of these fails, ask Socket support — the scope +identifiers for them are not published. + +### What individual flags add + +| Flag | Additional API calls | +|:---|:---| +| `--reach` | Manifest upload, `GET organizations` (plan check), full-scan tier-1 finalize | +| `--generate-license` | `POST purl` | +| `--enable-json` / `--enable-sarif` / `--enable-gitlab-security` on a full scan | `GET orgs/{org}/full-scans/{id}` (metadata), full-scan stream, and `POST license-metadata` for every package that carries a license | +| `--sbom-file` | `GET orgs/{org}/export/cdx/{id}` | + +## `APIAccessDenied` on the scan comparison + +Any run that produces a diff first tries the diff-scans endpoints. A token without the +`diff-scans:*` permissions logs a warning and silently continues on the older path: ``` Diff scan comparison failed with APIAccessDenied(Insufficient permissions), falling back to the streaming scan comparison ``` -Grant the scopes to use the diff-scans path. It polls with short, bounded requests -rather than holding one connection open while the backend computes, which is what lets -large comparisons survive network idle timeouts — notably Azure NAT gateways, which -reap idle connections after four minutes and surface as an intermittent -`ConnectionResetError`. +The scan still succeeds and the diff results are the same, so this is easy to miss. + +**Which runs produce a diff.** As of 2.9.0: + +| Run | Compares? | +|:---|:---| +| PR/MR event with `--scm github` / `--scm gitlab` | Yes | +| Any other event with `--scm github` / `--scm gitlab` (branch and default-branch pushes) | No — creates a full scan | +| No SCM integration (`--scm api`, the default), including `--enable-diff` and `--ignore-commit-files` | Yes | +| No supported manifest in the changed-file set | No — falls back to a full scan | + +Before 2.9.0 an SCM-integrated branch push also compared, so a pipeline scanning only +pushes (`--pr-number 0 --default-branch`) hit this too. If you are diagnosing an older +run, that is why the warning can appear in a log with no pull request in sight. + +**Which permission is missing.** The fallback path is `GET orgs/{org}/full-scans/diff`, +a full-scans read. If you see the fallback produce results, your token already has +full-scans read, and the missing grants are the two `diff-scans:*` permissions — not +`full-scans:list`. If the fallback *also* fails, the gap is broader. + +**Why the diff-scans path is preferred.** It polls with short, bounded requests rather +than holding one connection open while the backend computes, which is what lets large +comparisons survive network idle timeouts — notably Azure NAT gateways, which reap idle +connections after four minutes and surface as an intermittent `ConnectionResetError`. +Falling back costs resilience, not correctness. The two paths can take noticeably different amounts of time on the same repository, because cached diff-scan responses always embed per-package license details while the @@ -29,6 +115,50 @@ streaming comparison requests a lean payload. On a large dependency tree, compar `Diff scan comparison ready in ...` timing against the `Diff Report Gathered in ...` total before assuming either path is at fault. +## A missing repository permission can exit 2 + +If the repository lookup fails, the CLI assumes the repo does not exist yet and tries +to create it. When the token cannot do either, the run exits with code **2** — which +[the exit code table](../README.md#exit-codes) otherwise documents as a keyboard +interrupt. That exit also bypasses `--disable-blocking` and `--exit-code-on-api-error`, +so it fails the pipeline even when you have asked for infrastructure errors to be +non-blocking. + +If a job dies with exit 2 and `Failed to create repository` or `API failure while +creating repository` in the log, check the token's repository permissions before +looking anywhere else. + +## Baseline scans that do not appear in the dashboard + +On the first scan of a repository or branch there is no head scan to diff against, so +the CLI creates a **temporary empty baseline** and compares to that: + +``` +No previous scan found - creating empty baseline scan +Comparing scans - Head scan ID: , New scan ID: +``` + +The baseline is created as a temporary scan and is deliberately not set as the +repository head or default branch, so it does not show up as a repository scan in the +dashboard. Looking it up by ID will come up empty. This is expected — the scan to look +at is the "New scan ID". + +Because everything is new relative to an empty baseline, the first run also reports +every package as added and zero as unchanged. + +## Reachability scan IDs are not full-scan IDs + +With `--reach`, the CLI logs an extra identifier before the normal scan flow starts: + +``` +Reachability scan ID: +``` + +That value is `tier1ReachabilityScanId`, read out of `.socket.facts.json`. It +identifies the reachability analysis run, not a Socket full scan, and will not resolve +in the dashboard's scan views. The full scan to look at is the "New scan ID" reported +by the comparison step that follows. + ## Common gotchas - In diff scope, `--strict-blocking` uses a stricter alert set (`new + unchanged`) for blocking checks and diff-based output selection. diff --git a/pyproject.toml b/pyproject.toml index 3b73053..db09130 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" [project] name = "socketsecurity" -version = "2.9.7" +version = "2.9.8" requires-python = ">= 3.11" license = {"file" = "LICENSE"} dependencies = [ diff --git a/socketsecurity/__init__.py b/socketsecurity/__init__.py index de72965..5730d9b 100644 --- a/socketsecurity/__init__.py +++ b/socketsecurity/__init__.py @@ -1,3 +1,3 @@ __author__ = 'socket.dev' -__version__ = '2.9.7' +__version__ = '2.9.8' USER_AGENT = f'SocketPythonCLI/{__version__}' diff --git a/uv.lock b/uv.lock index 96f0318..030af62 100644 --- a/uv.lock +++ b/uv.lock @@ -1293,7 +1293,7 @@ wheels = [ [[package]] name = "socketsecurity" -version = "2.9.7" +version = "2.9.8" source = { editable = "." } dependencies = [ { name = "beautifulsoup4" },