From a2beacb4301fdf9fc7c35e0314b619f57a41cb7a 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/9] 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) --- README.md | 10 +++- docs/ci-cd.md | 6 ++ docs/cli-reference.md | 17 +++++- docs/troubleshooting.md | 119 +++++++++++++++++++++++++++++++++++----- 4 files changed, 136 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 5221daf1..0b47fd46 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 cb6ee629..61ce9d40 100644 --- a/docs/ci-cd.md +++ b/docs/ci-cd.md @@ -623,3 +623,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 c52450ec..582f60d3 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 | |:-----------------|:---------|:--------|:------------------------------------------------------------------------------------------------------------------| @@ -907,7 +922,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 4161dc3f..84568a3f 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 1f75a8f12563c1dc0c4648964626b0f6719c51aa 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/9] 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) --- docs/troubleshooting.md | 41 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 36 insertions(+), 5 deletions(-) diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 84568a3f..cfcfc768 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 From 6cc80a619c3a9f16b589344d112a7ed15fcd4596 Mon Sep 17 00:00:00 2001 From: lelia <2418071+lelia@users.noreply.github.com> Date: Wed, 23 Sep 2026 17:51:49 -0400 Subject: [PATCH 3/9] Correct which runs compare, after the 2.9.0 flow change now sends only a "diff" event type to create_new_diff and everything else, default-branch pushes included, to create_full_scan_with_report_url. The permission docs asserted the opposite -- that plain pushes also reach the diff-scans path -- which was true when they were written against 2.8.1 and is not true now. Replace the claim with a table of which runs compare, and keep the old behavior as a note, since the warning still shows up in logs from pre-2.9.0 runs with no pull request in sight. Re-verified the rest against 2.9.7: SDK call surface, the four sys.exit(2) sites, the temporary empty baseline, and the reachability scan ID are all unchanged. --- docs/troubleshooting.md | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index cfcfc768..a37687f5 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -76,10 +76,8 @@ identifiers for them are not published. ## `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: +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 @@ -87,8 +85,18 @@ Diff scan comparison failed with APIAccessDenied(Insufficient permissions), fall 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 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 From db69ae4174342f2995ec54fae9b6c1bb39a84db3 Mon Sep 17 00:00:00 2001 From: lelia <2418071+lelia@users.noreply.github.com> Date: Fri, 13 Nov 2026 20:51:34 -0500 Subject: [PATCH 4/9] Map each API call to the scope the API checks The scope tables were inferred from scope names. The API reference publishes the scope each endpoint checks, so the tables now use those and the open question about unpublished scopes is resolved: GET organizations, GET report/supported and the ignore-comment telemetry post check none. Rechecking the call surface against 2.12.0 also changed the guidance: - repo:update is never exercised. POST full-scans checks only full-scans:create, including when it sets the repository head. - --sbom-file makes no API call; the CycloneDX export it was mapped to has no caller. License-metadata and full-scan metadata reads are likewise unreached outside tests. - diff-scans/from-ids checks full-scans:list as well as diff-scans:create. - --reach's manifest upload and tier-1 finalize check full-scans:create, as do the Coana CLI's reachability calls. - The exit-2 section now quotes the repo:create hint the CLI logs on a 403. --- docs/cli-reference.md | 5 +- docs/troubleshooting.md | 142 ++++++++++++++++++++++------------------ 2 files changed, 80 insertions(+), 67 deletions(-) diff --git a/docs/cli-reference.md b/docs/cli-reference.md index 582f60d3..c69e6fa2 100644 --- a/docs/cli-reference.md +++ b/docs/cli-reference.md @@ -239,8 +239,9 @@ The token is read from the first of these that is set, then from `--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. +This repository's examples use `SOCKET_SECURITY_API_TOKEN`, and the CI/CD guides on +docs.socket.dev use `SOCKET_SECURITY_API_KEY`. Either works. If both are set, +`SOCKET_SECURITY_API_KEY` wins. > **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. diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index a37687f5..5fcc92da 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -2,82 +2,86 @@ ## API token permissions -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. +The CLI authenticates with a single Socket organization API token. A token missing a +permission usually does **not** fail the run. Several paths fall back or skip work and +leave only a warning in the log. -### What every run calls +### Scopes to grant -These are exercised on any invocation, regardless of flags: +For a least-privilege CI token, grant these: -| API call | Purpose | +| Scope | Needed for | |:---|:---| -| `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 +| `repo:list` | Looking up the repository and its head scan, on every run | +| `repo:create` | Creating the repository on its first scan. Without it, a scan of a repository Socket has not seen yet exits `2`; see [below](#a-missing-repository-permission-can-exit-2) | +| `full-scans:create` | Creating the scan, on every run. `--reach` also uploads manifests and finalizes the reachability scan under this scope | +| `full-scans:list` | Every comparison, the `--base-commit-sha` lookup, and fetching scan contents for `--enable-json`, `--enable-sarif`, `--enable-gitlab-security` and `--generate-license` on a full scan | +| `diff-scans:create` | Creating the comparison on a diff-producing run | +| `diff-scans:list` | Polling for the comparison, and finding an existing one | +| `packages:list` | `--generate-license` only | + +The scopes each endpoint checks are listed in the +[API reference](https://docs.socket.dev/reference). A few calls check no scope and work +with any valid token for the organization: `GET organizations`, `GET report/supported`, +and the telemetry event posted when someone replies to a Socket comment with an ignore +command. + +### The CI/CD setup guide's list is incomplete for this CLI The [CI/CD token setup guide](https://docs.socket.dev/docs/create-socket-api-key-for-cicd) -tells you to select nine scopes: +lists 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. +**It does not include `diff-scans:create` or `diff-scans:list`**, and every +diff-producing run needs both. A token set up exactly as that guide describes always +falls back to the older comparison path; see +[the next section](#apiaccessdenied-on-the-scan-comparison). Add the two scopes. -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. +The guide's list also includes four scopes this CLI never uses: `repo:update`, +`security-policy:read`, `triage:alerts-list` and `triage:alerts-update`. Other Socket +tooling uses them, so leave them on a shared token. A token used only by `socketcli` +can drop them. -Mapping the remaining scopes to the calls above (inferred from the names; the setup -guide does not publish a per-endpoint mapping): +### API calls by run type -| 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}` | +Every run: + +| API call | Scope | Purpose | +|:---|:---|:---| +| `GET organizations` | none | Resolve the org ID and slug from the token | +| `GET report/supported` | none | Fetch supported manifest patterns. On failure the CLI uses a bundled list that covers fewer ecosystems | +| `GET orgs/{org}/repos/{repo}` | `repo:list` | Look up the repository and its head scan | +| `POST orgs/{org}/repos` | `repo:create` | Create the repository, **only attempted if the lookup above fails** | +| `POST orgs/{org}/full-scans` | `full-scans:create` | Create the new scan, and the temporary empty baseline on a first scan | + +Diff-producing runs (see [which runs compare](#apiaccessdenied-on-the-scan-comparison)) add: + +| API call | Scope | Purpose | +|:---|:---|:---| +| `POST orgs/{org}/diff-scans/from-ids` | `diff-scans:create` and `full-scans:list` | Create the comparison (preferred path) | +| `GET orgs/{org}/diff-scans` | `diff-scans:list` | Find the existing comparison when creation returns 409 | +| `GET orgs/{org}/diff-scans/{id}` | `diff-scans:list` | Poll for the computed comparison | +| `GET orgs/{org}/full-scans/diff` | `full-scans:list` | Streaming comparison (fallback path) | + +Flags add: + +| Flag | API calls | Scope | +|:---|:---|:---| +| `--reach` | `GET organizations` (plan check), `POST orgs/{org}/upload-manifest-files`, `POST tier1-reachability-scan/finalize` | none, `full-scans:create`, `full-scans:create` | +| `--base-commit-sha` | `GET orgs/{org}/full-scans` (find the scan for that commit or a scanned ancestor) | `full-scans:list` | +| `--generate-license` | `POST orgs/{org}/purl` | `packages:list` | +| `--enable-json`, `--enable-sarif`, `--enable-gitlab-security` or `--generate-license` on a full scan | `GET orgs/{org}/full-scans/{id}` (scan contents) | `full-scans:list` | + +`--reach` also runs the Coana CLI with the same token, and its reachability calls +check `full-scans:create`. ## `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-scans:*` scopes logs a warning and continues on the older path: ``` Diff scan comparison failed with APIAccessDenied(Insufficient permissions), falling back to the streaming scan comparison @@ -98,10 +102,10 @@ Before 2.9.0 an SCM-integrated branch push also compared, so a pipeline scanning 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. +**Which scope is missing.** The fallback calls `GET orgs/{org}/full-scans/diff`, which +checks `full-scans:list`. If the fallback produces results, the token already has +`full-scans:list`, so the missing grant is `diff-scans:create`, `diff-scans:list`, or +both. If the fallback also fails, `full-scans:list` is missing too. **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 @@ -124,9 +128,16 @@ interrupt. That exit also bypasses `--disable-blocking` and `--exit-code-on-api- 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. +This usually happens on the first scan of a repository Socket has not seen, when the +token lacks `repo:create`. A token without `repo:list` can end up here even for +a repository that exists, because its lookup fails and the CLI then tries to create it. The log shows `API failure while creating repository`, and on a +403 adds: + +``` +A 403 here usually means the API token is missing the repo:create scope. +``` + +Grant `repo:create`, or create the repository in Socket before the first pipeline run. ## Baseline scans that do not appear in the dashboard @@ -135,6 +146,7 @@ the CLI creates a **temporary empty baseline** and compares to that: ``` No previous scan found - creating empty baseline scan +Baseline selected: source=empty scan_id="" Comparing scans - Head scan ID: , New scan ID: ``` From 39323c4b3423a170ad40d26466b10fff5a94546f Mon Sep 17 00:00:00 2001 From: lelia <2418071+lelia@users.noreply.github.com> Date: Fri, 13 Nov 2026 20:52:07 -0500 Subject: [PATCH 5/9] Describe which runs compare without pinning it to a release --- docs/ci-cd.md | 10 ++++++---- docs/troubleshooting.md | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/docs/ci-cd.md b/docs/ci-cd.md index 61ce9d40..e4934f3e 100644 --- a/docs/ci-cd.md +++ b/docs/ci-cd.md @@ -623,9 +623,11 @@ 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 +- A token missing the `diff-scans:*` scopes 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 + back to a less resilient comparison path. This affects every run that compares + scans: PR/MR events, and runs without an SCM integration. An SCM-integrated branch + push creates a full scan and never compares. The + [CI/CD token setup guide](https://docs.socket.dev/docs/create-socket-api-key-for-cicd) + leaves both scopes out; see [`troubleshooting.md#api-token-permissions`](troubleshooting.md#api-token-permissions). diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 5fcc92da..abf89797 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -89,7 +89,7 @@ Diff scan comparison failed with APIAccessDenied(Insufficient permissions), fall 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: +**Which runs produce a diff:** | Run | Compares? | |:---|:---| From a1c1ae4222d85b7006afbf1aeafaf4f90979f6f7 Mon Sep 17 00:00:00 2001 From: lelia <2418071+lelia@users.noreply.github.com> Date: Fri, 13 Nov 2026 20:51:43 -0500 Subject: [PATCH 6/9] Bump to 2.12.1 --- CHANGELOG.md | 25 +++++++++++++++++++++++++ pyproject.toml | 2 +- socketsecurity/__init__.py | 2 +- uv.lock | 2 +- 4 files changed, 28 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 810aabd4..319336b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,30 @@ # Changelog +## 2.12.1 + +### Documentation + +- Documented the API token scopes `socketcli` needs, and which API call each one + covers, in + [Troubleshooting](docs/troubleshooting.md#api-token-permissions). Each mapping + comes from the scope the API checks for that endpoint. +- The [CI/CD token setup guide](https://docs.socket.dev/docs/create-socket-api-key-for-cicd) + does not list `diff-scans:create` or `diff-scans:list`. A token set up from it + falls back to the older scan comparison on every run that compares scans, with only a + warning in the log. The troubleshooting page now says to add both, and notes that + the CLI never uses four of the guide's scopes: `repo:update`, + `security-policy:read`, `triage:alerts-list` and `triage:alerts-update`. +- Corrected the `APIAccessDenied` comparison guidance. It applies to every run that + compares scans, not only pull requests, and the missing scopes are the + `diff-scans:*` pair, not `full-scans:list`. +- Documented three results that look like failures but aren't: the temporary empty + baseline on a repository's first scan, reachability scan IDs that aren't full-scan + IDs, and exit `2` when the token can't create the repository. Exit `2` is not + remapped by `--disable-blocking` or `--exit-code-on-api-error`. +- Listed all four accepted token environment variables in priority order, and fixed + the GitLab section, which named `$SOCKET_API_TOKEN` instead of + `SOCKET_SECURITY_API_TOKEN`. + ## 2.12.0 ### Added diff --git a/pyproject.toml b/pyproject.toml index b62bd60f..3ee6508e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" [project] name = "socketsecurity" -version = "2.12.0" +version = "2.12.1" requires-python = ">= 3.11" license = {"file" = "LICENSE"} dependencies = [ diff --git a/socketsecurity/__init__.py b/socketsecurity/__init__.py index 203f24cf..01e27579 100644 --- a/socketsecurity/__init__.py +++ b/socketsecurity/__init__.py @@ -1,4 +1,4 @@ __author__ = 'socket.dev' -__version__ = '2.12.0' +__version__ = '2.12.1' USER_AGENT_NAME = 'socket-python-cli' USER_AGENT = f'{USER_AGENT_NAME}/{__version__}' diff --git a/uv.lock b/uv.lock index dc276bb5..6077c132 100644 --- a/uv.lock +++ b/uv.lock @@ -1334,7 +1334,7 @@ wheels = [ [[package]] name = "socketsecurity" -version = "2.12.0" +version = "2.12.1" source = { editable = "." } dependencies = [ { name = "beautifulsoup4" }, From 1d0fcd9d2e406d7ca294eb6ec25b5b4b1373043f Mon Sep 17 00:00:00 2001 From: lelia <2418071+lelia@users.noreply.github.com> Date: Fri, 13 Nov 2026 22:30:54 -0500 Subject: [PATCH 7/9] Trim the 2.12.1 changelog to what a release user sees --- CHANGELOG.md | 26 ++++++-------------------- 1 file changed, 6 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 319336b4..ce0dc7d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,26 +4,12 @@ ### Documentation -- Documented the API token scopes `socketcli` needs, and which API call each one - covers, in - [Troubleshooting](docs/troubleshooting.md#api-token-permissions). Each mapping - comes from the scope the API checks for that endpoint. -- The [CI/CD token setup guide](https://docs.socket.dev/docs/create-socket-api-key-for-cicd) - does not list `diff-scans:create` or `diff-scans:list`. A token set up from it - falls back to the older scan comparison on every run that compares scans, with only a - warning in the log. The troubleshooting page now says to add both, and notes that - the CLI never uses four of the guide's scopes: `repo:update`, - `security-policy:read`, `triage:alerts-list` and `triage:alerts-update`. -- Corrected the `APIAccessDenied` comparison guidance. It applies to every run that - compares scans, not only pull requests, and the missing scopes are the - `diff-scans:*` pair, not `full-scans:list`. -- Documented three results that look like failures but aren't: the temporary empty - baseline on a repository's first scan, reachability scan IDs that aren't full-scan - IDs, and exit `2` when the token can't create the repository. Exit `2` is not - remapped by `--disable-blocking` or `--exit-code-on-api-error`. -- Listed all four accepted token environment variables in priority order, and fixed - the GitLab section, which named `$SOCKET_API_TOKEN` instead of - `SOCKET_SECURITY_API_TOKEN`. +- New [API token permissions](docs/troubleshooting.md#api-token-permissions) guide: + the scopes `socketcli` needs and the API calls each one covers. +- Add `diff-scans:create` and `diff-scans:list` to CI tokens. Without them, scan + comparisons fall back to a less resilient path with only a warning. +- Documented why exit code `2` can mean the token can't create the repository. +- Listed all four accepted token environment variables in priority order. ## 2.12.0 From 06869b42b1fa423be922dfb36f0849e80abda791 Mon Sep 17 00:00:00 2001 From: lelia <2418071+lelia@users.noreply.github.com> Date: Fri, 13 Nov 2026 22:43:24 -0500 Subject: [PATCH 8/9] Scope the repository lookup to diff-producing runs Only a diff-producing run that takes its baseline from the repository's head scan looks the repository up, and so only that run can create it or exit 2. A full-scan run uploads straight to full-scans, which creates a missing repository for an organization-wide token. A failed streaming fallback also only points at full-scans:list when it fails with Insufficient permissions. --- docs/troubleshooting.md | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index abf89797..88777be4 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -12,9 +12,9 @@ For a least-privilege CI token, grant these: | Scope | Needed for | |:---|:---| -| `repo:list` | Looking up the repository and its head scan, on every run | -| `repo:create` | Creating the repository on its first scan. Without it, a scan of a repository Socket has not seen yet exits `2`; see [below](#a-missing-repository-permission-can-exit-2) | -| `full-scans:create` | Creating the scan, on every run. `--reach` also uploads manifests and finalizes the reachability scan under this scope | +| `repo:list` | Looking up the repository's head scan to compare against, on diff-producing runs without `--base-scan-id` or `--base-commit-sha` | +| `repo:create` | Creating the repository when that lookup finds none. Without it, the first comparison for a repository Socket has not seen yet exits `2`; see [below](#a-missing-repository-permission-can-exit-2) | +| `full-scans:create` | Creating the scan, on every run. On a full-scan run with an organization-wide token, the upload also creates a missing repository under this scope. `--reach` also uploads manifests and finalizes the reachability scan under it | | `full-scans:list` | Every comparison, the `--base-commit-sha` lookup, and fetching scan contents for `--enable-json`, `--enable-sarif`, `--enable-gitlab-security` and `--generate-license` on a full scan | | `diff-scans:create` | Creating the comparison on a diff-producing run | | `diff-scans:list` | Polling for the comparison, and finding an existing one | @@ -53,14 +53,15 @@ Every run: |:---|:---|:---| | `GET organizations` | none | Resolve the org ID and slug from the token | | `GET report/supported` | none | Fetch supported manifest patterns. On failure the CLI uses a bundled list that covers fewer ecosystems | -| `GET orgs/{org}/repos/{repo}` | `repo:list` | Look up the repository and its head scan | -| `POST orgs/{org}/repos` | `repo:create` | Create the repository, **only attempted if the lookup above fails** | -| `POST orgs/{org}/full-scans` | `full-scans:create` | Create the new scan, and the temporary empty baseline on a first scan | +| `POST orgs/{org}/full-scans` | `full-scans:create` | Create the new scan. With an organization-wide token, the server creates the repository if it does not exist yet | Diff-producing runs (see [which runs compare](#apiaccessdenied-on-the-scan-comparison)) add: | API call | Scope | Purpose | |:---|:---|:---| +| `GET orgs/{org}/repos/{repo}` | `repo:list` | Look up the repository's head scan as the baseline. Skipped with `--base-scan-id` or `--base-commit-sha` | +| `POST orgs/{org}/repos` | `repo:create` | Create the repository, **only attempted if the lookup above fails** | +| `POST orgs/{org}/full-scans` | `full-scans:create` | Create a temporary empty baseline when the repository has no head scan yet | | `POST orgs/{org}/diff-scans/from-ids` | `diff-scans:create` and `full-scans:list` | Create the comparison (preferred path) | | `GET orgs/{org}/diff-scans` | `diff-scans:list` | Find the existing comparison when creation returns 409 | | `GET orgs/{org}/diff-scans/{id}` | `diff-scans:list` | Poll for the computed comparison | @@ -105,7 +106,9 @@ run, that is why the warning can appear in a log with no pull request in sight. **Which scope is missing.** The fallback calls `GET orgs/{org}/full-scans/diff`, which checks `full-scans:list`. If the fallback produces results, the token already has `full-scans:list`, so the missing grant is `diff-scans:create`, `diff-scans:list`, or -both. If the fallback also fails, `full-scans:list` is missing too. +both. If the fallback also fails with `Insufficient permissions`, `full-scans:list` +is missing too. A fallback that fails another way, such as a timeout or a dropped +connection, says nothing about scopes. **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 @@ -121,17 +124,20 @@ 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 +A diff-producing run looks up the repository to find the head scan it compares +against. If that 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. -This usually happens on the first scan of a repository Socket has not seen, when the -token lacks `repo:create`. A token without `repo:list` can end up here even for -a repository that exists, because its lookup fails and the CLI then tries to create it. The log shows `API failure while creating repository`, and on a -403 adds: +This usually happens on the first comparison for a repository Socket has not seen, +when the token lacks `repo:create`. A token without `repo:list` can end up here even +for a repository that exists, because its lookup fails and the CLI then tries to +create it. Full-scan runs, such as SCM-integrated branch pushes, and runs with +`--base-scan-id` or `--base-commit-sha` skip the lookup, so they never take this +path. The log shows `API failure while creating repository`, and on a 403 adds: ``` A 403 here usually means the API token is missing the repo:create scope. From 5fc3b67fc20b49c3d70572b479c52078dfa66f1b Mon Sep 17 00:00:00 2001 From: lelia <2418071+lelia@users.noreply.github.com> Date: Fri, 13 Nov 2026 22:52:24 -0500 Subject: [PATCH 9/9] Correct the supported-patterns failure and the empty-baseline trigger A failed GET report/supported raises from the SDK and stops the run; the bundled pattern list only covers a non-error response with no body. An empty baseline is created when the repository has no head scan, not on every new branch, since a branch compares against the repository's head scan. --- docs/troubleshooting.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 88777be4..02eeadc0 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -52,7 +52,7 @@ Every run: | API call | Scope | Purpose | |:---|:---|:---| | `GET organizations` | none | Resolve the org ID and slug from the token | -| `GET report/supported` | none | Fetch supported manifest patterns. On failure the CLI uses a bundled list that covers fewer ecosystems | +| `GET report/supported` | none | Fetch supported manifest patterns. A failed request, such as a timeout or an invalid token, stops the run | | `POST orgs/{org}/full-scans` | `full-scans:create` | Create the new scan. With an organization-wide token, the server creates the repository if it does not exist yet | Diff-producing runs (see [which runs compare](#apiaccessdenied-on-the-scan-comparison)) add: @@ -147,8 +147,9 @@ Grant `repo:create`, or create the repository in Socket before the first pipelin ## 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: +A diff-producing run compares against the repository's head scan. When the repository +has none yet, usually on its first comparison, the CLI creates a **temporary empty +baseline** and compares to that: ``` No previous scan found - creating empty baseline scan @@ -161,8 +162,9 @@ repository head or default branch, so it does not show up as a repository scan i 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. +Because everything is new relative to an empty baseline, that comparison reports +every package as added and zero as unchanged. A new branch of a repository that +already has a head scan compares against that head scan, not an empty baseline. ## Reachability scan IDs are not full-scan IDs