Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ pip install socketsecurity
export SOCKET_SECURITY_API_TOKEN="<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
Expand Down Expand Up @@ -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 <N>` remaps the infrastructure-error code (`3`) to any
Expand All @@ -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`:

Expand Down
8 changes: 8 additions & 0 deletions docs/ci-cd.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
18 changes: 17 additions & 1 deletion docs/cli-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
|:-----------------|:---------|:--------|:------------------------------------------------------------------------------------------------------------------|
Expand Down Expand Up @@ -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
Expand Down
178 changes: 164 additions & 14 deletions docs/troubleshooting.md
Original file line number Diff line number Diff line change
@@ -1,34 +1,184 @@
# 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
streaming comparison requests a lean payload. On a large dependency tree, compare the
`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="<baseline>"
Comparing scans - Head scan ID: <baseline>, New scan ID: <real scan>
```

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: <tier-1 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.
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down
2 changes: 1 addition & 1 deletion socketsecurity/__init__.py
Original file line number Diff line number Diff line change
@@ -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__}'
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading