Skip to content

audit: version-aware verdicts + install gate (fixes the CVE-remediation catch-22) - #29

Open
superbiche wants to merge 14 commits into
mainfrom
feat/version-aware-verdict-core
Open

audit: version-aware verdicts + install gate (fixes the CVE-remediation catch-22)#29
superbiche wants to merge 14 commits into
mainfrom
feat/version-aware-verdict-core

Conversation

@superbiche

Copy link
Copy Markdown
Owner

Problem

Third occurrence (inbox 2026-07-31-reveille-safe-blocks-security-bumps-cve-catch22.md): safe WARN-blocks the dependency bump that fixes the very CVE it warns about, and suggests the forbidden host-allow add <pkg>@latest shape. Root cause: wrappers fabricate @latest for unversioned specs; safe audit check posts version:"latest" to OSV, which cannot parse it and returns every advisory ever filed; the verdict is a version-blind count. The fixed-version ranges were fetched, persisted, and never consulted. The host-allow matcher hard-rejects latest, so the operator ruling "never host-allow @latest" made pinned allows unmatchable on npm update paths.

Change

  • Target-version resolution before verdicting (in safe-audit, one implementation for the zsh wrappers and the bin/safe mirror): exact specs as-is; npm dist-tag for installs; in-range max from package.json + lockfile ranges for --op update (nested brace-expansion resolves to 2.1.4, not 5.x); registry latest for pip/uv, cargo, composer, go. Resolution failure degrades to a package-level audit with a WARN floor (version_unresolved) — never a silent GO.
  • Version-aware OSV verdicts: advisories classified per resolved version (affecting / remediated / unfixed / ambiguous) via a jq semver library; only affecting advisories drive the verdict; install.block_severities (default ["critical"]) escalates to BLOCK. An OSV outage now fails closed (previously it counted as zero CVEs — a fail-open hole). Exit codes and plain check semantics unchanged.
  • --gate install mode used by wrappers and safe install: GO proceeds with no operator terminal and records the pinned resolved version in machine-written ~/.config/safe/run/install-known.json (evidence → check receipt; offline fallback when the audit times out, within install.auto_allow_ttl_days). WARN proceeds only on a pinned host-allow entry matching the resolved version (fixing the unmatchable-pin dead end) or on opt-in install.auto_allow_tolerate causes. Refusal hints are always pinned; nothing ever renders @latest.
  • Socket failures are infrastructure alarms, not CVEs (operator ruling 2026-07-31): auth/rate-limit/network failures still refuse, with an explicit "infrastructure failure, NOT a package finding" message and recovery path; safe doctor now reports Socket CLI/token wiring.
  • Install decisions finally leave a persistent record: wrappers and safe install append install:<ecosystem> | … | GATE | … | PROCEED/AUTO-…/REFUSED_* lines to the safe-run audit log (they were stderr-only).

Verification

  • New offline suite tests/audit/check_version_aware.sh — 29 cases, all green: in-range update resolution, remediated-pass, critical-block, resolved host-allow match, socket-outage refusal + tolerate knob, ||-range degradation, registry/OSV outage fail-closed, and a no-@latest-in-hints guard.
  • tests/install/run.sh extended and green (72 cases); tests/audit/smoke.sh, tests/audit/external_binary.sh, tests/integration/dispatcher.sh, tests/run/safe_audit_integration.sh all green.
  • Live run of the reveille scenario (project pinning brace-expansion@^2.0.0, --op update --gate install): resolves 2.1.4 via registry.npmjs.org, and the hint is safe run host-allow add brace-expansion@2.1.4 --reason "...". Note: current upstream OSV data says GHSA-mh99-v99m-4gvg affects [0, 5.0.8) (advisory revised after an incomplete fix), so 2.1.4 correctly still WARNs today — with an honest, pinned, actionable message instead of a version-blind count.

Part 1 of the safe overhaul (verdict core). Next: PATH-executable wrappers + mise coverage, bulk audit, agent contract surface.


@superbiche · maintainer · drafted with Claude Fable 5.

https://claude.ai/code/session_01Rk1wZEXbFB23TamMZYuRi6

…t, stop blocking remediations

The check pipeline previously posted version:"latest" to OSV, which returns
every advisory ever filed, and verdicted on a version-blind count — blocking
the very bumps that fix CVEs (third occurrence, inbox 2026-07-31) while the
fabricated @latest spec made pinned host-allow entries unmatchable.

- Resolve the actual install target before verdicting: exact specs as-is;
  npm dist-tag for installs; in-range max from package.json + lockfile
  ranges for updates (nested brace-expansion -> 2.1.4, not 5.x); registry
  latest for pip/uv, cargo, composer, go. Failure degrades to package-level
  audit with a WARN floor (version_unresolved) — never silent GO.
- Classify advisories per resolved version (affecting/remediated/unfixed/
  ambiguous) with a jq semver library; only affecting ones drive the
  verdict; install.block_severities (default critical) escalates to BLOCK.
  OSV outage now fails closed (previously counted as zero CVEs).
- New --gate install mode: GO proceeds and records the pinned version in
  machine-written install-known.json (evidence -> check receipt; offline
  fallback within auto_allow_ttl_days). WARN proceeds only on a pinned
  host-allow entry matching the RESOLVED version or opt-in tolerated
  causes. Hints always pinned; nothing suggests @latest.
- Socket failures refuse with an explicit infrastructure-failure message +
  recovery path (socket login / safe doctor, which now reports Socket
  wiring) — never conflatable with a package finding.
- Wrappers and safe install stop fabricating @latest, pass --op/--gate,
  and log every gate decision to the safe-run audit log.
- New offline suite tests/audit/check_version_aware.sh (29 cases: in-range
  update resolution, remediated pass, critical block, resolved host-allow
  match, socket-outage refusal + tolerate knob, || degradation, registry/
  OSV outage fail-closed, no-@latest guard); install suite extended (72).

Claude-Session: https://claude.ai/code/session_01Rk1wZEXbFB23TamMZYuRi6
…osed)

Review: GPT-5.6-sol xhigh, tmp/pr29-verdict-core-FINDINGS.md (DO-NOT-SHIP, 5
blockers + 2 majors). All CORRECTIVE, all closed:

- F1 server-authoritative exact queries: a version-scoped OSV hit can never
  be downgraded to GO by the local comparator (defect class: local
  re-derivation overriding authoritative upstream data; swept the
  classification path — the degraded package-only path keeps local matching
  but its floor is WARN, never GO).
- F2 OSV pagination: follow next_page_token (bounded 10), validate vulns
  type, fail closed on repeated token/malformed pages (defect class:
  partial-response-as-complete; the packument/registry fetches are single
  documents, not paginated — checked).
- F3 target selectors threaded: npm --tag/--registry/--prefix and composer
  --working-dir reach resolution (--dist-tag/--registry/--project-dir check
  flags); custom pip/uv/cargo/composer indexes degrade to the unresolved
  refusal (defect class: audited context != install context; swept all four
  wrapper families via safe_install_scan_target_flags, per-family tables).
- F4 npm selection semantics: latest-tag preference (npm-pick-manifest),
  read -a instead of globbing word-split, root-overrides mention degrades
  (residual: engines/peer/dedupe constraints — architectural, proposed
  below). Defect class: reimplementing a resolver the ecosystem owns.
- F5 install-known lifecycle: adverse WARN/BLOCK revokes the entry for that
  version; both timeout readers (zsh + bash mirror — sibling surfaces swept)
  require verdict == "GO"; WARN_TOLERATED records stay distinguishable.
- F6 severity ladder: qualitative labels + computed CVSS v3 base score
  (most severe wins), CVSS v4-only floors at high; standard-severity
  critical now blocks without database_specific.
- F7 ecosystem normalization: cargo->rust, composer->php, pip->python at
  check entry; cargo install --version threads the pin into the spec.

Suites: tests/audit/check_version_aware.sh rewritten with a version-aware
OSV mock + pagination state (50 cases), tests/install/run.sh 72 cases, both
green; dispatcher integration 15/15.

ARCHITECTURAL residual for operator ruling: full npm parity (engines,
peerDeps, workspaces, lockfileVersion interplay) needs npm's own resolver as
the oracle or a post-install lockfile verification sweep (proposed for the
bulk-audit PR).

Claude-Session: https://claude.ai/code/session_01Rk1wZEXbFB23TamMZYuRi6
…er precedence, evidence lifecycle

Delta re-check verdict on 69ccac8: DO-NOT-SHIP 3 (F1/F7 fixed; residuals in
F2-F6 + new F8). All CORRECTIVE residuals closed; F6's CVSS-v4 scoring is
ARCHITECTURAL and goes to the operator as a proposal (inbox note), per R-12.

- F3 source identity (defect class: audited identity != installed artifact):
  any custom source now floors the verdict at WARN for EVERY ecosystem and
  exactness — computed after resolution, so the exact-version short circuit
  can no longer skip it. pip/uv --find-links and --no-index are scanned as
  custom sources; the uv wrapper scans target flags at all; `safe install`
  (bin/safe) threads --tag/--registry/--prefix/--repository/--working-dir
  into the audit. Escape hatches: install.trusted_registries (URL-prefix
  list, npm/PyPI defaults always trusted) or a pinned host-allow. Leading-
  dash project paths are anchored (./-x) before reaching jq/grep.
- F2 pagination: a page must be an object, vulns an array when present, and
  next_page_token a non-empty string when present ({"next_page_token":false}
  no longer reads as complete-and-clean); all seen tokens tracked, not just
  the previous one.
- F4 npm selection: vparse now parses prerelease identifier lists
  (numeric-before-alphanumeric, element-wise, prefix-shorter-first — jq
  array ordering matches all three SemVer rules), and version-qualified
  overrides keys ("pkg@^2.0.0") degrade resolution like bare keys.
- F5 evidence lifecycle: an adverse advisory (affecting > 0 or blocklist)
  revokes matching install-known evidence BEFORE the host-allow override
  decides the current invocation; infra-only WARNs (socket/OSV outage) do
  not revoke — the stale-evidence fallback exists for exactly those windows.
- F8 canonical keys: both stale readers (zsh + bash mirror) canonicalize
  cargo->rust/composer->php/pip|uv->python before the install-known lookup.

Suites: audit 58/58 (7 new regression cases incl. a vparse unit check and
the FIRST 9.8/changed-scope CVSS vectors), install 72/72, dispatcher 15/15.

Claude-Session: https://claude.ai/code/session_01Rk1wZEXbFB23TamMZYuRi6
…ust, cumulative sources, scoped evidence

Delta-2 verdict on 195d080: DO-NOT-SHIP 4. All four closed:

- F2: pagination tokens must be purely printable ([[:graph:]]+) — a "\n"
  token survived validation but stripped to empty in command substitution
  and read as end-of-pagination (defect class: lossy shell round-trip of
  opaque protocol state).
- F3: trust matching is origin-bounded (pypi.org.evil.example and
  userinfo tricks no longer match the PyPI default), and source selectors
  ACCUMULATE — pip/uv consider every given source, so one untrusted
  --find-links floors the verdict even when followed by a trusted index
  (swept all three scanners: zsh, bash mirror; gate-lib ported on the PR2
  branch).
- F4: prerelease identifiers convert to numbers ONLY on the SemVer numeric
  grammar (0|[1-9][0-9]*) — jq's broader number syntax typed "1e-2" numeric
  and let the resolver accept a version npm rejects.
- F5: install-known receipts record the source they were gathered against
  and both stale readers require the current source to match (default-
  registry evidence can never vouch for a custom index); a low Socket score
  now revokes stale clean evidence like advisory/blocklist hits (only
  enumerated pure-infra outages retain it).

Suites: audit 68/68 (10 new: whitespace token, 1e-2 grammar, evil-hostname
origin, real-PyPI trust, cumulative sources, low-score refuse+revoke,
source-scoped receipts), install 74/74 (cumulative pip sources reach the
audit; stale evidence is source-scoped both ways).

Claude-Session: https://claude.ai/code/session_01Rk1wZEXbFB23TamMZYuRi6
…ources, strict core grammar, discriminated identity

- F3.1: npm_registry_url resolves the LAST accumulated --registry (npm
  options are last-wins); the trust floor still checks every selector.
- F3.2: the trust floor runs on the EFFECTIVE source set — for npm this
  includes NPM_CONFIG_REGISTRY and project/user .npmrc, computed on every
  check including exact pins (no network). Receipt identity carries it;
  both stale-fallback readers mirror the derivation. Trusted mirrors stay
  frictionless via install.trusted_registries.
- F4: packument dist-tags and candidate versions are validated against the
  strict SemVer grammar before selection (matching npm's selector);
  resolve_target_versions grammar-guards every resolved target; vparse maps
  non-numeric core segments to -1 (never outranks a valid version) instead
  of jq-numeric coercion, staying lenient for annotation-only PEP440 OSV
  event versions.
- F5: install-known source identity is discriminated (implicit-default vs
  explicit:<sources>) so a literal selector spelled "default" can never
  collide with the sentinel; legacy source-less receipts map to
  implicit-default only.
- F2 test nit: the ws-token fixture now writes valid JSON whose decoded
  pagination token is a real newline (printf '%s').

Tests: check_version_aware 83/83 (15 new assertions: last-wins fetch log,
env/.npmrc floors + trusted-mirror UX, malformed packument selection and
dist-tag degrade, sentinel discipline incl. literal-default collision);
install suite 74/74 (reader-side sentinel, env-registry stale scoping,
legacy mapping).

Claude-Session: https://claude.ai/code/session_01Rk1wZEXbFB23TamMZYuRi6
…ces, full SemVer grammar

- F3.1: wrapper source accumulation moves a repeated selector to the END
  (npm last-wins) instead of first-occurrence dedup; the resolver's
  last-word read now matches the registry npm actually fetches from.
- F3.2: effective-source derivation extended and CENTRALIZED: lowercase
  npm_config_registry, scoped @scope:registry keys (env + project/user
  .npmrc, beating argv as npm's own key precedence does), PIP_INDEX_URL /
  PIP_EXTRA_INDEX_URL / UV_* env, pip.conf index-url/extra-index-url
  (PIP_CONFIG_FILE > venv > user), and GOPROXY chains. New plumbing
  subcommand `safe audit effective-sources` prints the discriminated
  identity; both stale readers call it instead of mirroring — one
  derivation for gate, writer, and readers (the reviewer's suggested fix).
  Un-swept surfaces (uv.toml/pyproject index tables, system pip.conf,
  cargo config.toml, project-declared composer repositories, yarn/bun
  native config) are documented in-code with the injectable-vs-operator-
  owned rationale.
- F4: full SemVer 2.0.0 grammar everywhere a registry-writable version can
  reach selection: SEMVER_STRICT_RE in version_is_exact (npm/rust/php/go)
  and the jq semver_ok used for dist-tag preference and candidate
  filtering — leading-zero numeric prerelease identifiers ("1.0.0-01"),
  empty identifiers, and leading-zero cores are rejected, matching
  npm-pick-manifest.
- N1: source sets canonicalize (per-element trailing-slash strip +
  last-wins dedup) identically on the writer and reader paths, so a
  trailing-slash selector no longer breaks same-source stale evidence.

Tests: check_version_aware 101/101 (18 new: lowercase env, scoped-key
floors incl. beside-argv, prerelease-grammar selection npm-oracle +
dist-tag degrade, version_is_exact unit matrix, effective-sources plumbing
matrix, slash-canonical receipts, PIP_INDEX_URL/GOPROXY floors, trusted
pip mirror UX); install suite 75/75 (repeated-registry true-last case;
stub forwards effective-sources to the real derivation).

Claude-Session: https://claude.ai/code/session_01Rk1wZEXbFB23TamMZYuRi6
… argv, node-semver limits, credential redaction

- F3.2: lowercase npm_config_registry now WINS over the uppercase form
  (npm's own precedence, oracle-verified); the wrapper scanners recognize
  raw scoped --@scope:registry[=] argv flags and --userconfig/--globalconfig
  (registry + scoped keys inside the pointed file become sources);
  python_env_sources adds PIP_FIND_LINKS / UV_FIND_LINKS /
  UV_EXTRA_INDEX_URL and the no-index switches (env + swept pip.conf keys);
  argv --find-links now records its actual endpoint instead of a blanket
  sentinel so operators can trust the specific location.
- F4: npm_semver_within_limits enforces node-semver's version-length (256)
  and Number.MAX_SAFE_INTEGER core-component ceilings in version_is_exact
  (npm only — other ecosystems' parser limits differ) and the jq semver_ok
  mirrors them as string comparisons; an over-limit numeric spec now
  resolves as the dist-tag npm actually sees, and over-limit candidates
  are excluded from range selection.
- N2: canonicalize_source drops URL userinfo everywhere identities,
  receipts, trust matching, or output are built (normalize_source_set,
  derive append, RESOLUTION_SOURCE, wrapper add_source before the audit
  log); operational fetch URLs keep credentials. Trust matching runs on
  the credential-free canonical endpoint.

Tests: check_version_aware 116/116 (both-case env precedence, huge-core
tag + range npm-oracles, node-semver limit unit, PIP_FIND_LINKS /
UV_EXTRA_INDEX_URL / PIP_NO_INDEX floors, credential redaction on
refusal output + receipts); install suite 77/77 (scoped argv +
--userconfig sources reach the audit, credentials stripped from AUDIT
log lines, find-links endpoint recording).

Claude-Session: https://claude.ai/code/session_01Rk1wZEXbFB23TamMZYuRi6
…erational/display split, pip config parser

- F3.2b: scoped CLI selectors are threaded as KEYED tokens
  (@scope:registry=URL) instead of flattened URLs, so npm_registry_url
  resolves the MATCHING scope's registry with npm's key precedence (CLI
  scoped > env scoped > project rc > user rc); generic last-wins selection
  skips scoped tokens; the trust floor checks each token's endpoint.
  npm_config_userconfig/NPM_CONFIG_USERCONFIG replace the user rc in both
  ladders and their registry keys sweep into the effective set. Also fixes
  a pre-existing parse_pkg_spec bug that split a bare scoped name at its
  leading @.
- F3.2c: pip config files are read with pip/configparser syntax (both '='
  and ':' delimiters, comments, indented continuation lines) via a small
  awk parser — line-grep missed forms pip itself accepts; later duplicate
  keys win per file.
- F4: the 256-char node-semver ceiling is measured on the RAW input before
  the v prefix is stripped.
- N2: redaction moved fully to the sinks: canonicalize_source now also
  strips query strings and fragments (token= credentials), while the
  wrappers no longer pre-redact — the accumulated set stays operational so
  authenticated packument fetches work again; identities, receipts,
  refusal output, and trust matching all remain credential-free.
- N3: no-index booleans are parsed per-variable, case-insensitively, with
  pip's vocabulary — PIP_NO_INDEX=False or a 0/0 pair no longer floors a
  clean default-source install.

Tests: check_version_aware 132/132 (scoped-key resolution npm-oracles incl.
CLI-over-env, env userconfig floor, query-token redaction, authed-registry
operational fetch + credential-free receipt, colon/continuation pip
config, no-index boolean matrix, raw-length boundary, scoped plumbing
identity); install suite 77/77 (keyed scoped token threading, operational
credential preservation).

Claude-Session: https://claude.ai/code/session_01Rk1wZEXbFB23TamMZYuRi6
… section semantics, boolean vocabulary

- F3.2b: alternate npm config files thread to safe-audit AS PATHS
  (--npm-userconfig/--npm-globalconfig; wrapper value-flattening deleted):
  npm_user_rc/npm_global_rc slot them into both rc ladders with npm's tier
  precedence, so scoped keys resolve per package and generic keys take
  their true tier — no more false CLI precedence or lost scope. The
  effective-for-this-package endpoint replaces the all-keys sweep (which
  was also over-flooring). The prev-form --@scope:registry now consumes
  its value in both npm package parsers (the URL was audited as a package).
- F3.2c: pip_config_values gains configparser semantics — option-name
  normalization (lowercase, underscores→hyphens), [install]-overrides-
  [global] section precedence independent of file order, and config paths
  iterate as quoted arrays (spaced PIP_CONFIG_FILE was silently missed).
- N2: config-file credentials never leave the file (path threading), and
  the zsh wrappers clear the scanned target state on every gate exit so an
  argv credential cannot linger in the long-lived shell.
- N3: python_bool_truthy accepts pip's full vocabulary (adds y/t).

Tests: check_version_aware 140/140 (userconfig scoped-key npm-oracle,
generic userconfig floor, section precedence, key normalization, spaced
path, y/t booleans); install suite 78/78 (prev-form scoped value
consumption + no URL-as-package, userconfig path threading with no value
leakage, post-gate state cleared).

Claude-Session: https://claude.ai/code/session_01Rk1wZEXbFB23TamMZYuRi6
…figParser fidelity, always-block cleanup

- F3.2b: option-like config paths (a file literally named "-q") anchor to
  ./ at every entry — flag parse in check/effective-sources, the env forms
  via anchor_path in npm_user_rc/npm_global_rc, and the python config path
  array — so grep/awk can never read them as their own options and fail
  the sweep open.
- F3.2c: pip_config_values matches ConfigParser: continuation lines are
  only lines indented DEEPER than their option (indented base options
  parse as options), key normalization also strips one leading "--"
  ("--index-url =" is a valid pip config key), and full-line comments drop
  at any indent without ending a value block.
- N2: scanned target state clears via a zsh always-block around every
  scanning route (npm/pip families, uv, cargo, go, composer) — project
  scans, parser fallbacks, unsupported subcommands, refusals, and
  successful gates alike; cleanup is no longer tied to check_many alone.

Tests: check_version_aware 146/146 (dash-named argv/env userconfig and
PIP_CONFIG_FILE floor instead of failing open; indented option; --key
spelling; continuation regression guard); install suite 78/78
(project-install route clears the scanned state).

Claude-Session: https://claude.ai/code/session_01Rk1wZEXbFB23TamMZYuRi6
… refactor (round 10)

Delta-9 N4: the *_impl split moved the inlined degraded guards behind
strippable helper names and made the always blocks call a strippable
cleanup helper — a public-only snapshot died 127 with command-not-found
noise instead of the documented `safe: BLOCKED` exit 100.

- The degraded guards move back INTO the public uv/cargo/go/composer
  wrappers (per the standing inline-guard contract), with the impl name
  added to the checked function list so a missing impl refuses legibly.
- Every always block inlines the five state assignments instead of
  calling a helper a partial snapshot may strip.
- The degraded test harness now strips *_impl alongside safe_install_*,
  and every degraded case rejects `command not found` on stderr.

Tests: install suite 78/78 (degraded matrix now models the public-only
snapshot); check_version_aware 146/146; other suites exit 0.

Claude-Session: https://claude.ai/code/session_01Rk1wZEXbFB23TamMZYuRi6
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant