Skip to content

Commit b9e106d

Browse files
v5: socket.yml patch rollout config and filtering (#293)
* Plan staged patch rollout for v5 Adds the design for rolling Socket patches out gradually: a `patches:` block in socket.yml that narrows what scan may patch (paths, ecosystems, packages, severity floor, on/off), and a severity-ordered per-run cap on new patches so each scan lands the next few most critical fixes. The plan splits the work into two parallel items with a frozen interface, lists every hard-coded filter and where it belongs, and covers the depscan autopatch follow-up. configuration.md now records that socket-patch reads socket.yml for selection policy only, with the trust boundary unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Revise rollout plan after adversarial review Three independent reviews (ambiguity, churn, trust boundary) found gaps that would have let the two implementations disagree or let a repo file widen or stall the rollout. The plan now: - matches paths against marker files with the backend's top-down gitignore rules, so projectIgnorePaths means the same everywhere - uses one data source for severity, supersession and ordering - spends the budget only on patches the planning pass proves can land, and admits nothing new when a lookup failed - uses the merged recorded view in every mode and engine - has depscan read the policy from the base commit for PR jobs - hardens file handling (regular files, aliases, size, encoding, trusted repo root) and reports what a policy hides Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Close interface gaps in the rollout plan A final consistency pass found places where the two work items would have produced incompatible code: base purls admitted in one directory being charged again in the next, no defined hand-off of the unfiltered offers from the severity filter to classification, no shared repo-relative path helper, and override sources the JSON must report but the interface could not carry. The shared contract now defines each of these, and the parity tests match each engine's budget scope. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Clarify who shapes scan's selection output The plan said both that the selector returns the shared offers struct (work item A) and that it returns admitted/deferred rows (work item B). The selector now returns the offers, and B adds the rollout stage after it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Add the socket.yml selection policy to core New socket_patch_core::policy module: the SelectionPolicy, Offers and repo-root helpers that the staged-rollout plan freezes as the shared contract between the socket.yml work and the --max-new-patches work. It reads the repo root's socket.yml/socket.yaml strictly and fails closed: bad YAML, a misspelled `patches` block, unknown keys (with a did-you-mean hint), wrong types, empty allowlists, bad globs and anchors or aliases inside the keys we read are all errors that name the key path. Path lists match marker files with npm `ignore` semantics, walked top-down; a golden fixture generated from the npm package pins that. The built-in test/fixture ignores become overridable defaults for discovered roots. --package matching moves to core so scan and the policy share it. Claude-Session: https://claude.ai/code/session_01BKsyzefGhAnPkYmXCwq3H3 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Honor socket.yml patch policy in disk scans scan now reads the repo root's socket.yml before any write and applies its patches block in hosted, vendored and agent mode, --dry-run included: path filters on project roots (PATH-glob matches also get the built-in test/fixture ignores), ecosystem and package filters on crawled packages, and a severity floor on the patches a package may receive. An invalid or ambiguous file fails the run with exit 1 and an errorCode before any request. Packages that already carry a patch are never removed, upgraded or replaced by the policy: they are held and reported under policy.retained. A recorded patch below a new floor stays in place. patches.enabled: false reports what would be patched and writes nothing. New flags: --no-socket-yml / SOCKET_NO_SOCKET_YML and --min-severity / SOCKET_MIN_SEVERITY. Every successful scan --json result gains a top-level policy block. A PATH outside the repository root is a usage error. Claude-Session: https://claude.ai/code/session_01BKsyzefGhAnPkYmXCwq3H3 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Apply socket.yml policy in the in-memory engine selectHostedScanPaths now streams the root socket.yml/socket.yaml and returns them as policyPaths; it drops test and fixture trees through the policy's built-in default ignores instead of a hard-coded segment list (structural excludes like node_modules and vendor stay fixed). The session reads the policy before any root is processed: path filters run before the project limit, ecosystem and package filters on each root's packages, and the severity floor before selection. A listed policy file that arrives without content, or an invalid one, yields policyError with no root processed and no file changed. New options noSocketYml, minSeverity and policyPaths; the result gains a policy block. hosted-bundle and index.d.ts carry the new fields. get now warns policy_bypassed when the repo's socket.yml would have skipped the package it patches. Claude-Session: https://claude.ai/code/session_01BKsyzefGhAnPkYmXCwq3H3 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Document the socket.yml patch policy CLI_CONTRACT.md gains a "socket.yml patch policy" section (grammar, precedence, paths, lookup, validation, commands, error codes and the policy JSON block), the flag and env rows, and the "narrow or pace" half of the trust-boundary rule. README adds a "Roll out gradually" section with copyable socket.yml recipes, CHANGELOG lists the new policy and the breaking scan changes, and the design docs record the decisions made while building it. Claude-Session: https://claude.ai/code/session_01BKsyzefGhAnPkYmXCwq3H3 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Harden the socket.yml policy after review Fixes from an adversarial self-review of the policy work: - A non-ASCII package spec no longer panics validation, and error text, warnings and verbose lines drop terminal escapes and bidi or zero-width characters. - Ignore lists that fail to compile together are an error instead of silently matching nothing, and the policy file is opened by its resolved path without following a swapped-in symlink. - A top-level key that looks like a misspelled `patches` (`patchs`), a top-level merge key or an aliased key now fails closed. - Repo-root lookup follows a `.git` symlink and trusts the checkout owner under root and sudo, so CI containers keep the policy. - The severity floor always reports the patch it held back, report- only --json runs report what a floor or `enabled: false` hides, a root skipped as a whole is always one entry and no longer prints "No packages found", warnings print once per invocation, and the policy line is omitted when there is nothing to say. - policy entries are sorted and use canonical purls on disk and in memory; the in-memory engine applies a socket.yml negation of a built-in ignore to roots it was given, and policyError carries no CLI-only remedy. - A lockfile-less disk root matches path filters by its manifests. Tests cover each fix, plus the prune universe, agent path filters, a vendored package held byte-identical, and tighter oracles; docs are corrected where they overstated what the human output names. Claude-Session: https://claude.ai/code/session_01BKsyzefGhAnPkYmXCwq3H3 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Give the release test job more time The optimized test job now has to build two more crates and one more test binary for the socket.yml policy. It ran out of time on its last 40 minutes, about a minute short, and a docs-only change already takes 38. Raise the limit to 50 minutes so it can finish. Claude-Session: https://claude.ai/code/session_01BKsyzefGhAnPkYmXCwq3H3 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Apply socket.yml paths during memory selection The merged plan makes in-memory path selection two-phase: the host fetches the root socket.yml first and passes its text to selectHostedScanPaths. Selection now applies the full path policy, so a negation such as `!/e2e/tests/` brings a test tree back in memory exactly as on disk. A listed policy file that is missing, symlinked or invalid returns policyError with nothing selected. Selection returns policySha256, and the session fails closed when the policy it reads differs. Roots the policy excludes keep their marker files presence-only, so the session still lists them as filtered. Claude-Session: https://claude.ai/code/session_01BKsyzefGhAnPkYmXCwq3H3 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Keep excluded roots out of the memory stream Review follow-ups to two-phase selection: - Roots the policy excludes are reported in ignoredSample instead of streamed presence-only, so a repo with many fixture lockfiles no longer runs into the session's file limit. - A session that bypasses socket.yml while selection applied it now fails closed instead of processing roots it never fetched. - The docs say policy text must decode losslessly (TextDecoder drops a BOM) and when policySha256 is null. Claude-Session: https://claude.ai/code/session_01BKsyzefGhAnPkYmXCwq3H3 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Keep scan -h short with the policy flags The v5 help rules cap each command's short help at about eight options. `--no-socket-yml` and `--min-severity` pushed `scan -h` to ten, so they now appear only in `scan --help`, like the other advanced scan flags. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BKsyzefGhAnPkYmXCwq3H3 * Fail report-only JSON when detail queries fail A report-only `scan --json` (`--prune` with no mode) with a severity floor or `enabled: false` fetches patch details to fill `policy.filtered[]`. If every query failed it still printed a success envelope and exited 0. It now reports the error and exits 1, like the agent and vendored runs. Human-mode policy warnings now print `Warning: <detail>` like the rest of `scan`; the code stays in the JSON `warnings[]` entry. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BKsyzefGhAnPkYmXCwq3H3 --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent b97a1c2 commit b9e106d

49 files changed

Lines changed: 7891 additions & 204 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎CHANGELOG.md‎

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -652,8 +652,57 @@ into the new version's section — see docs/releasing.md.
652652
batch is reported as failed (warning, or the all-failed error when it was
653653
the only batch) — instead of that one package being skipped silently.
654654

655+
- **scan honors the repo's socket.yml.** `projectIgnorePaths` (the
656+
scanner's key) now also keeps `scan` from patching the matching projects,
657+
in every mode and in the in-memory engine, whether or not the file has a
658+
`patches` block (malformed values there only warn
659+
`socket_yml_ignored_value`). See "socket.yml patch policy" in
660+
CLI_CONTRACT.md.
661+
- **Test and fixture trees are skipped by default when scan discovers
662+
projects.** `test/ tests/ fixtures/ __fixtures__/ testdata/` (any case)
663+
are built-in `ignorePaths` for discovered roots: hosted/vendored
664+
PATH-glob matches (`scan 'services/*'`) and the in-memory engine's
665+
detected roots (which used to skip them through a hard-coded, case-
666+
sensitive segment list). A directory you name (`--cwd`, a literal PATH,
667+
`projectRoots`) is not affected; `ignorePaths: ["!/e2e/tests/"]`
668+
re-includes one, in memory too.
669+
- **An invalid socket.yml fails scan.** An unparseable file, a misspelled
670+
top-level `patches` key (`Patches`, `patchs`), a top-level merge or
671+
aliased key, an invalid `patches` block (unknown key, wrong type, bad glob, `patches`
672+
without `version: 2`), or `socket.yml` and `socket.yaml` that disagree
673+
now fail `scan` before any request or write: exit 1, `errorCode:
674+
socket_yml_invalid` / `socket_yml_ambiguous`, the key path and the fix
675+
in the message. `--no-socket-yml` ignores the file for one run.
676+
- **scan rejects a PATH outside the repository root** (exit 2): one socket.yml
677+
policy per invocation.
678+
655679
### Added
656680

681+
- **socket.yml patch policy (staged rollout).** A `patches` block in the
682+
repo-root socket.yml narrows what `scan` patches: `enabled` (false =
683+
report only), `includePaths` / `ignorePaths` (gitignore patterns matched
684+
against each project's lockfiles, npm `ignore` semantics),
685+
`ecosystems`, `packages` / `ignorePackages` (`--package` specs),
686+
`minSeverity` (critical|high|medium|moderate|low, judged by the worst
687+
advisory a patch fixes) and `maxNewPatches` (validated; the per-run cap
688+
lands with `--max-new-patches`). List flags (`--ecosystems`,
689+
`--package`, PATHs) only narrow further; `--min-severity` beats the
690+
file's floor and `--no-socket-yml` ignores the file. A package
691+
that already carries a patch is never removed, upgraded or replaced by
692+
the policy: it is held and reported under `policy.retained[]`. New flags
693+
`--min-severity` / `SOCKET_MIN_SEVERITY` and `--no-socket-yml` /
694+
`SOCKET_NO_SOCKET_YML`; every successful `scan --json` result gains a
695+
top-level `policy` block (`source`, `sha256`, `minSeverity`, `filtered[]`,
696+
`retained[]`) and the human output a `Policy (socket.yml): …` line that
697+
names every skipped project and every critical/high patch the severity
698+
floor held back. In memory, selection is two-phase:
699+
`selectHostedScanPaths` takes the root policy files' text
700+
(`policyFiles`) and `noSocketYml`, applies the full path policy and
701+
returns `policyPaths`, `policySha256` and `policyError`; the session
702+
takes `noSocketYml` / `minSeverity` / `policyPaths` / `policySha256` and
703+
its result carries `policy` or `policyError`.
704+
`get` ignores the policy and warns `policy_bypassed`.
705+
657706
- **`scan --package <name|purl>`** (repeatable or comma-separated, env
658707
`SOCKET_SCAN_PACKAGES`) scopes a scan to the named packages: a name
659708
(`lodash`, `@scope/pkg`, `group:artifact`) or a purl with or without its

‎Cargo.lock‎

Lines changed: 125 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎Cargo.toml‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ serial_test = "=3.4.0"
5959
napi = { version = "=3.13.0", features = ["napi8", "tokio_rt"] }
6060
napi-derive = "=3.6.9"
6161
napi-build = "=2.5.0"
62+
serde-saphyr = { version = "=1.3.0", default-features = false, features = ["deserialize"] }
63+
ignore = "=0.4.33"
6264

6365
[profile.release]
6466
strip = true

‎README.md‎

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -413,6 +413,10 @@ socket-patch scan 'services/*' # directory glo
413413
hosted and vendored mode each PATH is a project directory, scanned as if it were
414414
`--cwd` under an `== <dir> ==` header; the worst exit code wins.
415415
416+
To make the choice stick for everyone who runs `scan` in the repo (CI and the Socket
417+
autopatch bot included), put it in `socket.yml` instead — see
418+
[Roll out gradually](#roll-out-gradually-with-socketyml).
419+
416420
### Patch one specific CVE or advisory
417421
418422
```bash
@@ -647,6 +651,8 @@ socket-patch scan [PATHS]... [options]
647651
| `--prune` | — | Agent-mode garbage collection after the scan: remove manifest entries for packages no longer present in the crawl (installed trees + lockfiles — a wiped `node_modules` alone doesn't prune lockfile-listed entries) and delete orphan blob/diff-archive files (plus any legacy package archives). [Vendored](#vendor) packages are exempt from the crawl-based prune, but a vendored entry whose dependency has left the lockfile is reverted. Ignored, with a `redirect_prune_ignored` warning, in hosted mode; without a mode the scan is report-only. |
648652
| `--sync` | — | Shorthand for `--mode agent --prune`: the one-flag agent-mode auto-update run. |
649653
| `--batch-size <n>` | `SOCKET_BATCH_SIZE` | Packages per API request (default: `500` on the authenticated API, `100` on the public proxy). A request whose body would exceed 256 KiB is split into smaller ones. |
654+
| `--min-severity <level>` | `SOCKET_MIN_SEVERITY` | Only patch packages whose patch fixes an advisory of at least `critical`, `high`, `medium` (or `moderate`) or `low`; `none` lifts the floor. Overrides `patches.minSeverity` in socket.yml. Patches of unknown severity are skipped whenever a floor is set. |
655+
| `--no-socket-yml` | `SOCKET_NO_SOCKET_YML` | Ignore the repo's socket.yml patch policy for this run (the built-in test/fixture directory ignores still apply). |
650656
| `--all-releases` | `SOCKET_ALL_RELEASES` | Store patches for every release/distribution variant, not just the installed one — PyPI wheel/sdist, RubyGems platform, Maven classifier. Makes the manifest portable across environments (e.g. cross-platform CI caches). |
651657
| `--vex <path>` | `SOCKET_VEX` | On a successful scan, also write an OpenVEX 0.2.0 document to this path. See [Inline VEX](#inline-vex-on-apply--scan--vendor). |
652658
| `--vex-product`, `--vex-no-verify`, `--vex-doc-id`, `--vex-compact` | `SOCKET_VEX_*` | Passthrough to the embedded VEX builder; mirror the standalone [`vex`](#vex) knobs. Inert unless `--vex` is set. |
@@ -692,6 +698,72 @@ socket-patch scan --vex socket.vex.json
692698
> artifact is the patch); a newer available patch still appears in `updates[]` — re-run
693699
> `scan --mode vendored` to take it.
694700
701+
#### Roll out gradually with socket.yml
702+
703+
A `patches` block in the repo-root `socket.yml` (the file the Socket scanner already
704+
reads; keep `version: 2`) narrows what `scan` may patch, for every mode and for the
705+
in-memory engine behind the Socket autopatch bot. It can only narrow: nothing in it can
706+
name an endpoint or token, pick a mode, or turn off a safety check.
707+
708+
```yaml
709+
# Critical first: widen by editing one line
710+
version: 2
711+
patches:
712+
minSeverity: critical # later: high, then low, then remove the key
713+
# (low still skips patches whose severity is unknown)
714+
```
715+
716+
```yaml
717+
# One directory first (monorepo)
718+
version: 2
719+
patches:
720+
includePaths:
721+
- "/services/payments/"
722+
# add "/services/checkout/" next sprint
723+
```
724+
725+
```yaml
726+
# One ecosystem, hold one package
727+
version: 2
728+
patches:
729+
ecosystems: [npm]
730+
ignorePackages: ["pkg:npm/left-pad"]
731+
```
732+
733+
```yaml
734+
# Pause: report only; existing patches stay in place
735+
version: 2
736+
patches:
737+
enabled: false
738+
```
739+
740+
- Paths are gitignore patterns (the same rules as `projectIgnorePaths`, which scan now
741+
honors too), matched against each project's lockfiles: `"/services/payments/"`,
742+
`"**/yarn.lock"`, `"examples/**"`. `test/`, `tests/`, `fixtures/`, `__fixtures__/`
743+
and `testdata/` directories are skipped by default when scan discovers projects
744+
(a directory glob such as `scan 'services/*'`); re-include one with a negation
745+
(`ignorePaths: ["!/e2e/tests/"]`). A directory you name yourself is always scanned.
746+
(The autopatch bot's tree listing skips those directories before it reads
747+
socket.yml, so there a negation cannot bring one back.)
748+
- `packages` / `ignorePackages` take `--package` specs; prefer purls (`pkg:npm/core`),
749+
because a bare name also matches other ecosystems and scoped packages (`core`
750+
matches `@babel/core`).
751+
- Narrowing never removes a patch: a package that already carries one and is now
752+
filtered out is left exactly as it is (reported under `policy.retained[]`). Use
753+
`rollback` or `remove` to take a patch out.
754+
- A broken file fails the scan (exit 1, `errorCode: socket_yml_invalid`) before anything
755+
is written, with the key and the fix in the message — a typo never widens the
756+
rollout. `--no-socket-yml` ignores the file for one run.
757+
- `scan --json` reports what the policy did in a top-level `policy` block
758+
(`jq '.policy.counts'`); the human output adds a `Policy (socket.yml): …` line that
759+
names every skipped project and every critical/high patch the severity floor held
760+
back (`--verbose` lists everything). `get` ignores the policy (explicit
761+
intent) and warns `policy_bypassed`.
762+
763+
Every key: `enabled`, `includePaths`, `ignorePaths`, `ecosystems`, `packages`,
764+
`ignorePackages`, `minSeverity`, `maxNewPatches` — see CLI_CONTRACT.md "socket.yml patch
765+
policy" for the full grammar, precedence and validation rules.
766+
695767
### `vex`
696768
697769
Generate an [OpenVEX](https://github.com/openvex) 0.2.0 attestation describing the

0 commit comments

Comments
 (0)