diff --git a/CHANGELOG.md b/CHANGELOG.md index 810aabd..ce0dc7d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog +## 2.12.1 + +### Documentation + +- 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 ### Added 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 cb6ee62..e4934f3 100644 --- a/docs/ci-cd.md +++ b/docs/ci-cd.md @@ -623,3 +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:*` 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 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/cli-reference.md b/docs/cli-reference.md index c52450e..c69e6fa 100644 --- a/docs/cli-reference.md +++ b/docs/cli-reference.md @@ -232,6 +232,22 @@ 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` + +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. +> 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 +923,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 4161dc3..02eeadc 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -1,27 +1,120 @@ # 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. 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. -- `diff-scans:create` -- `diff-scans:list` -- `full-scans:list` +### Scopes to grant -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: +For a least-privilege CI token, grant these: + +| Scope | Needed for | +|:---|:---| +| `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 | +| `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) +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` + +**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. + +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. + +### API calls by run type + +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. 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: + +| 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 | +| `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:*` 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 ``` -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:** + +| 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 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 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 +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 +122,63 @@ 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 + +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 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. +``` + +Grant `repo:create`, or create the repository in Socket before the first pipeline run. + +## Baseline scans that do not appear in the dashboard + +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 +Baseline selected: source=empty scan_id="" +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, 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 + +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 b62bd60..3ee6508 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 203f24c..01e2757 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 dc276bb..6077c13 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" },