Skip to content
Merged
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
44 changes: 43 additions & 1 deletion crates/socket-patch-cli/CLI_CONTRACT.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,49 @@ Rows are in `--help` order (v5.0): the hosted/vendored workflow (`scan` → `vex

**Bare-UUID fallback.** `socket-patch <UUID>` is rewritten to `socket-patch get <UUID>`, also when root-position flags come first (`socket-patch --json <UUID>`); a UUID after a subcommand name is that subcommand's operand. The UUID shape checked is the standard 8-4-4-4-12 hex pattern (case-insensitive), the target grammar's.

**Target grammar (v5.0).** `get`, `remove`, `rollback` and the bare-UUID fallback classify their package/patch token with one parser (core `utils::target`): a UUID; `CVE-…` / `GHSA-…` (case-insensitive); a `pkg:` purl (with a version: that release, a base purl covering every release variant and a `?qualified` one exactly one; without a version: every version of that package, compared by purl identity — case-sensitive for npm, Go, Maven, cargo and gem, folded for PyPI (PEP 503), NuGet and Composer, so `pkg:npm/jsonstream` never selects `JSONStream`); otherwise an **exact** package name — full name or last segment, case-insensitive, PEP 503 for PyPI — the same matcher as `scan --package` and `socket.yml`. A name never matches by prefix or substring, and a Go major-version suffix (`v2`) is never a name. **Ambiguous names**: `get`, `remove` and `rollback` act on one package per name, so a name whose last-segment rule reaches several packages (`core` → `@angular/core` and `@babel/core`; the same name in two ecosystems), or whose case-insensitive rule reaches case-distinct packages (`jsonstream` → npm's `JSONStream` and `jsonstream`, Go's `Sirupsen` and `sirupsen`), is refused with exit 1 before anything is searched or changed — `"core" is ambiguous: it names pkg:npm/@angular/core, pkg:npm/@babel/core; use the full name or a purl` (`remove --json`: `errorCode: "ambiguous_target"`; `get` / `rollback --json`: `{status: "error", error}`). Several versions of one package are not ambiguous, and a name typed with an uppercase letter settles on its exact-case package among case-distinct ones (`JSONStream`). `scan --package` and `socket.yml` keep selecting every package the name reaches. `get <name>` searches every installed version of the matched name (within `--ecosystems`) and prints `Matched: …` on stderr; with no exact match it is `no_match` (exit 0, no API call) and, in human mode, suggests up to five near names (`Did you mean: …?`) without acting on them. `remove` / `rollback` accept the same names and versionless purls against manifest records, vendor-ledger entries and hosted pins; CVE/GHSA ids match no record there. A name containing `/` (composer `vendor/pkg`, a go module path) is path-shaped, so `rollback` first tries it as a name: when it selects a recorded or hosted patch it is a target, otherwise a path glob.
**Target grammar (v5.0).** `get`, `remove`, `rollback` and the bare-UUID
fallback classify their package/patch token with one parser (core `utils::target`):
a UUID; `CVE-…` / `GHSA-…` (case-insensitive); a `pkg:` purl (with a version:
that release, a base purl covering every release variant and a `?qualified` one
exactly one; without a version: every version of that package, compared by purl identity —
case-sensitive for npm, Go, Maven, cargo and gem, folded for PyPI (PEP 503),
NuGet and Composer, so `pkg:npm/jsonstream` never selects `JSONStream`); otherwise an **exact** package
name — full name or last segment, case-insensitive, PEP 503 for PyPI — the same
matcher as `scan --package` and `socket.yml`. A name never matches by prefix or
substring, and a Go major-version suffix (`v2`) alone never selects a module.

A full-name match takes precedence over last-segment matches: `lodash` selects
only `lodash` when `@types/lodash` is also present. **Ambiguous names**: `get`,
`remove` and `rollback` act on one package per name, so a name that still reaches
several packages (`core` → `@angular/core` and `@babel/core`; the same name in two
ecosystems), or whose case-insensitive rule reaches case-distinct packages
(`jsonstream` → npm's `JSONStream` and `jsonstream`, Go's `Sirupsen` and
`sirupsen`), is refused with exit 1 before any patch search or mutation. The
message lists versionless purls and a remedy: `"core" is ambiguous: it names
pkg:npm/@angular/core, pkg:npm/@babel/core; use the full name or a purl`. All three
commands report `status: "error"` and `error.code: "ambiguous_target"` under
`--json`. Several versions of one package are not ambiguous, and a name typed with an
uppercase letter settles on its exact-case package among case-distinct ones
(`JSONStream`). `scan --package` and
`socket.yml` keep selecting every package the name reaches.

`get <name>` searches every installed version of the matched name (within
`--ecosystems`) and prints `Matched: …` on stderr. With no exact match in a
nonempty inventory it is `no_match` (exit 0, no patch-search API call) and, in
human mode, suggests up to five near names (`Did you mean: …?`) without acting on
them. An empty inventory is `no_packages` (exit 0). `--ecosystems` also filters
advisory/purl search results and UUID selection before any patch is written; a
UUID outside the selected ecosystems is `not_found` (exit 0).

`remove` / `rollback` accept the same names and versionless purls against manifest
records, vendor-ledger entries and hosted pins; CVE/GHSA ids match no record
there. An npm scoped name (`@babel/core`) is always a package target. A relative
name containing `/` (composer `vendor/pkg`, a Go module path), without glob
metacharacters or `.` / `..` path segments, is path-shaped, so `rollback` first
tries it as a name: when it selects a recorded or hosted patch it is a target,
otherwise a path glob. Use `./vendor/pkg` or `'vendor/pkg/**'` to explicitly select
installed copies by path. See the [migration guide](../../docs/migrating-to-v5.md#package-targeting)
for examples and script migration guidance.

**Root `--update` flag.** `socket-patch --update [VERSION]` updates the binary itself from GitHub Releases. It is a root flag, not a subcommand: argv is rewritten (the same mechanism as the bare-UUID fallback) onto an internal hidden subcommand whose name carries no stability guarantee — script the flag, never the internal name. Combining the flag with a subcommand (`socket-patch --update scan`) is a usage error (exit 2). Full contract: [Self-update contract](#self-update-contract-socket-patch---update).

Expand Down
51 changes: 51 additions & 0 deletions docs/migrating-to-v5.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,57 @@ existing scripts against the new CLI; the [changelog](../CHANGELOG.md) and
`SOCKET_ORG_SLUG` to get org patches. The org is resolved once per run, so an
embedded `--vex` no longer resolves it again.

## Package targeting

`get <name>` now selects an exact package name instead of automatically choosing
the nearest fuzzy match. For example, `get yaml` never selects `yaml-ast-parser`;
near names are suggestions only. It queries every installed version of the
selected package, including older nested copies. If a script should select only
one release, use a versioned PURL such as `pkg:npm/lodash@4.17.20`.

`get`, `remove`, and `rollback` share these package/patch target forms:

| Target | Meaning |
| --- | --- |
| Package name, such as `lodash` or `@babel/core` | Every installed version for `get`; every recorded version for `remove` / `rollback`, including vendor records and hosted pins |
| Versionless PURL, such as `pkg:npm/lodash` | Select the package across versions, with an explicit ecosystem |
| Versioned PURL, such as `pkg:npm/lodash@4.17.20` | Select that release; an unqualified PURL covers its release variants, while a qualified PURL selects one variant |
| Patch UUID | Select one patch; `socket-patch <UUID>` is shorthand for `get <UUID>`, including after root flags such as `--json` |
| CVE or GHSA ID | Search by advisory with `get`; these IDs do not select records for `remove` / `rollback`, so use the package PURL or patch UUID there |

Name matching uses the same case-insensitive rules as `scan --package`, including
PyPI normalization of `.`, `_`, and `-`. A full-name match takes precedence:
`get lodash` selects `lodash` when both it and `@types/lodash` are installed. A
last-segment name such as `core` is accepted only when it identifies one package.
If it reaches both `@angular/core` and `@babel/core`, `get`, `remove`, and `rollback`
refuse it with exit 1 and `error.code: "ambiguous_target"` under `--json`, before
changing any patches. The message lists exact versionless PURLs to use instead.
The same name in two ecosystems also needs a PURL to disambiguate it. Multiple
versions of one package are not ambiguous. `scan --package` and `socket.yml`
continue to select every package a name matches. Go major-version suffixes such
as `v2` alone do not select modules; use the full module path or PURL.

`get --ecosystems` now scopes package-name discovery and filters every search
result and UUID selection before any patch is written, in every mode. A UUID
outside the selected ecosystems returns `status: "not_found"` with exit 0. A
name absent from a nonempty package inventory returns `status: "no_match"` with
exit 0, without searching for or applying a suggested package. An empty inventory
returns `status: "no_packages"`. Scripts should inspect these statuses instead of
assuming exit 0 means a patch was selected.

`rollback` also accepts path globs. An npm scoped name such as `@babel/core` is
always a package target. A slash-containing name such as `monolog/monolog` is a
package target when it selects a recorded patch or hosted pin; otherwise it is
treated as a path glob. Use an explicit path such as `./monolog/monolog` or a
quoted glob such as `'node_modules/**'` to select installed copies by location.

```sh
socket-patch get yaml --ecosystems npm --dry-run
socket-patch get pkg:npm/lodash@4.17.20 --dry-run
socket-patch remove pkg:npm/@babel/core --dry-run
socket-patch rollback pkg:composer/monolog/monolog --dry-run
```

## Service-only vendoring

v4 could build patched artifacts locally, with `auto` as the default acquisition
Expand Down
13 changes: 8 additions & 5 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,12 @@ Replace the example identifiers with the advisory or package you need. `get` als
accepts a patch UUID or an exact package name, which covers every installed version
of that package. `remove` and `rollback` take the same names. A short name that
names several packages (`core` for `@angular/core` and `@babel/core`) is refused:
use the full name or a purl. `get` defaults to hosted mode; `--save-only` and
global targeting default to agent mode instead. Hosted and vendored `get` do not
prompt. Agent-mode searches can offer an interactive choice.
use the full name or a purl. Near names are only suggestions, and `--ecosystems`
scopes both `get` searches and UUID selection. See the
[v5 targeting changes](migrating-to-v5.md#package-targeting) for target forms,
ambiguity rules, and no-match statuses. `get` defaults to hosted mode; `--save-only`
and global targeting default to agent mode instead. Hosted and vendored `get` do
not prompt. Agent-mode searches can offer an interactive choice.

For each package version, automatic selection prefers the highest severity among
downloadable patches, then the most distinct advisories fixed, then the newest
Expand Down Expand Up @@ -228,8 +231,8 @@ The removed `setup` command is covered in the [migration guide](migrating-to-v5.
| Command | Effect |
| --- | --- |
| `list` | Show agent records, vendor records, and hosted pins; an empty project succeeds |
| `rollback [PURL\|UUID\|PATH]...` | Restore selected patches, or all patches when no target is given, and remove their local state |
| `remove <PURL\|UUID>` | Restore and remove one patch |
| `rollback [NAME\|PURL\|UUID\|PATH]...` | Restore selected patches, or all patches when no target is given, and remove their local state |
| `remove <NAME\|PURL\|UUID>` | Restore and remove matching patches; a name or versionless PURL selects every recorded version |
| `vendor --revert` | Undo vendoring from its recorded edits and remove the vendored artifacts |
| `repair` | Restore missing agent patch data or missing/corrupt vendored artifacts using existing records, and clean unused data |
| `scan --mode agent --prune` | Patch discovered packages and remove records for dependencies that left the project |
Expand Down
Loading