diff --git a/CHANGELOG.md b/CHANGELOG.md index b6eebb2..f0868fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,32 @@ # 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, 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` + 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 5f978d5..559b8f4 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 968799b..ca3f7ab 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 f64de26..85fd762 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 9d6d06d..5056df8 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -1,27 +1,105 @@ # Troubleshooting -## API token scopes for scan comparisons - -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: - -- `diff-scans:create` -- `diff-scans:list` -- `full-scans:list` - -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: +## 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. + +### What every run calls + +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 — 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 +107,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.