From 07b6ed11a2bb1c04b39319b975f6c9036dccd99f Mon Sep 17 00:00:00 2001 From: lelia <2418071+lelia@users.noreply.github.com> Date: Fri, 11 Sep 2026 18:05:02 -0400 Subject: [PATCH 1/2] Document the API token permissions a run actually needs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The only scope guidance in the repo covered the diff-scans comparison path, described it as PR/MR-only, and listed `full-scans:list` alongside the two `diff-scans:*` scopes. The comparison also runs on plain pushes, and the streaming fallback is itself a full-scans read, so a reader hitting the `APIAccessDenied` warning was pointed at the wrong permission and told the condition did not apply to them. Replace it with the full picture: what every run calls, what diff-producing runs add, and what each flag adds. Scope names are only published for the diff-scans path, so the rest are described by endpoint rather than guessed at. Also document three outcomes that look like failures and are not: the temporary empty baseline on a first scan, the reachability scan ID being a `tier1ReachabilityScanId` rather than a full scan ID, and a repository permission gap exiting 2 — a code the exit table documents as a keyboard interrupt, and which bypasses `--disable-blocking` and `--exit-code-on-api-error`. Co-Authored-By: Claude Opus 5 (1M context) --- CHANGELOG.md | 23 ++++++++ README.md | 10 +++- docs/ci-cd.md | 5 ++ docs/cli-reference.md | 17 +++++- docs/troubleshooting.md | 119 +++++++++++++++++++++++++++++++++++----- 5 files changed, 158 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b6eebb26..9b8cdb55 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,28 @@ # Changelog +## Unreleased + +### 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. Only the diff-scans scope names are published, so the remaining calls are + described by endpoint, with a pointer to support for exact scope identifiers. +- Corrected the scan-comparison guidance. The `APIAccessDenied` fallback was documented + as a PR/MR-only condition, but it applies to any run that produces a diff, including + plain pushes on the default branch. 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.8.1 ### Changed: bump pinned @coana-tech/cli to 15.10.40 diff --git a/README.md b/README.md index 5f978d59..559b8f4e 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 @@ -220,7 +224,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 @@ -245,6 +249,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 968799b5..ca3f7ab5 100644 --- a/docs/ci-cd.md +++ b/docs/ci-cd.md @@ -411,3 +411,8 @@ Prebuilt examples in this repo: - `--sarif-grouping alert` currently applies to `--sarif-scope full`. - Diff-based SARIF can validly be empty when there are no matching net-new alerts. - Keep API tokens in secret stores (`SOCKET_SECURITY_API_TOKEN`), not in config files. +- 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 applies to plain pushes, not just + PR/MR runs. See + [`troubleshooting.md#api-token-permissions`](troubleshooting.md#api-token-permissions). diff --git a/docs/cli-reference.md b/docs/cli-reference.md index f64de267..85fd7622 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 | |:-----------------|:---------|:--------|:------------------------------------------------------------------------------------------------------------------| @@ -868,7 +883,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 9d6d06df..8d4c930e 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -1,27 +1,74 @@ # 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` | + +The diff-scans path is the one with published scope names: `diff-scans:create`, +`diff-scans:list` and `full-scans:list`. For the rest of the calls above, grant the +token access to the corresponding resource; if you need the exact scope identifiers to +provision a least-privilege token, ask Socket support rather than inferring them from +the endpoint paths. + +### 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 — PR/MR events, **plain pushes on the default branch**, +and `--enable-diff` / `--ignore-commit-files` runs without an SCM integration — 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. + +Note that this is *not* limited to PR/MR runs. A pipeline that only ever scans pushes +(`--pr-number 0 --default-branch`) still hits it. + +**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 +76,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. From 72c2f29344c1cb39b105fbb9fd02332022fd01ac Mon Sep 17 00:00:00 2001 From: lelia <2418071+lelia@users.noreply.github.com> Date: Fri, 11 Sep 2026 18:37:23 -0400 Subject: [PATCH 2/2] Name the token scopes and flag the two the setup guide omits The permission tables described endpoints but deferred on scope names, since nothing in this repo or the SDK publishes them. The public CI/CD token setup guide does: nine scopes, which the docs now name and map to the calls they cover. That guide omits `diff-scans:create` and `diff-scans:list`. Every diff-producing run tries the diff-scans endpoints first, so a token provisioned exactly as documented always fails that call and falls back to the legacy streaming comparison, with a warning as the only signal. Anyone following the documented setup hits this. The converse is also worth stating: `socketcli` makes no triage or security-policy calls, so three of the nine scopes the guide lists are not exercised by this CLI. Co-Authored-By: Claude Opus 5 (1M context) --- CHANGELOG.md | 8 ++++++-- docs/troubleshooting.md | 41 ++++++++++++++++++++++++++++++++++++----- 2 files changed, 42 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b8cdb55..f0868fe8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,8 +6,12 @@ - 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. Only the diff-scans scope names are published, so the remaining calls are - described by endpoint, with a pointer to support for exact scope identifiers. + 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, but it applies to any run that produces a diff, including plain pushes on the default branch. The guidance also listed `full-scans:list` diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 8d4c930e..5056df8b 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -28,11 +28,42 @@ These are exercised on any invocation, regardless of flags: | `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` | -The diff-scans path is the one with published scope names: `diff-scans:create`, -`diff-scans:list` and `full-scans:list`. For the rest of the calls above, grant the -token access to the corresponding resource; if you need the exact scope identifiers to -provision a least-privilege token, ask Socket support rather than inferring them from -the endpoint paths. +### 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