Skip to content

feat(vex): OpenVEX 0.2.0 attestation generator#81

Merged
Mikola Lysenko (mikolalysenko) merged 6 commits into
mainfrom
feat/openvex-vex-subcommand
May 26, 2026
Merged

feat(vex): OpenVEX 0.2.0 attestation generator#81
Mikola Lysenko (mikolalysenko) merged 6 commits into
mainfrom
feat/openvex-vex-subcommand

Conversation

@mikolalysenko
Copy link
Copy Markdown
Contributor

Summary

  • New socket-patch vex subcommand emits an OpenVEX 0.2.0 document from the local manifest, attesting that patched dependencies render the top-level product not_affected by their associated vulnerabilities.
  • Self-contained socket-patch-core::vex module (schema, builder, product auto-detect, on-disk verify, RFC 3339 time) — no upstream openvex crate dependency (those are unmaintained).
  • CI now installs Go + vexctl so tests/e2e_vex.rs validates the generated document with vexctl inspect on every PR.

Design decisions

  • Status / justification: applied patches → not_affected + inline_mitigations_already_exist, with the Socket patch UUID(s) in impact_statement.
  • Statement grouping: one statement per vulnerability ID (GHSA as name, CVEs flattened into aliases). Two patches that share a GHSA merge their subcomponents into one statement.
  • Product: auto-detected from package.json > pyproject.toml > Cargo.toml; --product <purl> overrides.
  • Verification: on-disk file-hash check is the default. --no-verify skips it. Patches that fail verification are omitted entirely — never emitted as affected or under_investigation. Failed PURLs surface as stderr warnings (plain mode) or skipped envelope events (--json).
  • Empty result: no manifest, empty manifest, or all-failed verification → exit non-zero with no document written.
  • Output channels: VEX → --output or stdout. --json requires --output (the VEX is itself JSON-LD; envelope + VEX cannot share stdout).
  • Tri-state exit codes: 0 produced a doc, 1 no applicable patches, 2 hard error.

What's NOT in this PR

  • Monorepo / multi-product support — single project only for now. The --product flag is the workaround.
  • Subcomponent file hashes — only PURLs for v1. Easy follow-up if downstream scanners want byte-level pinning.
  • Sigstore / cosign signing — out of scope.
  • .github/actions/ (untracked locally) is the maintainer's separate in-progress work and is intentionally NOT included here.

Note on CI pin

The new actions/setup-go step uses @v5 with a TODO(pin) comment. The rest of this repo's workflows use SHA pins; please replace before merging once the SHA is audited.

Test plan

  • cargo test -p socket-patch-core --lib vex::33 passed (schema, build, product detect, verify, RFC 3339)
  • cargo test -p socket-patch-cli --test e2e_vex9 passed (no-verify, GHSA merge, empty manifest, --json envelope, auto-detect, verify-mode happy + failure paths)
  • cargo test --workspace --all-features1425 cases passed, 0 failures
  • cargo clippy --workspace --all-features -- -D warnings — clean
  • CI vexctl inspect confirms the document validates (gated on this PR's CI run)
  • CLI smoke: socket-patch vex --no-verify against a hand-written manifest emits a doc that round-trips through jq

Out-of-scope flake noticed

The pre-existing test partition_purls_no_filter_mixed_ecosystems (introduced in b96a13f) fails under default features — it asserts map.len() == 3 unconditionally even though pkg:cargo only resolves with --features cargo. Not in this PR's scope; flagging for a separate fix.

🤖 Generated with Claude Code

Adds a new `socket-patch vex` subcommand that emits an OpenVEX 0.2.0
document derived from the local manifest. Each statement reports the
top-level product as `not_affected` by a given vulnerability with
justification `inline_mitigations_already_exist`, pointing at the
patched dependency as a subcomponent.

Design highlights:
- New self-contained module `socket-patch-core::vex` (schema, build,
  product detection, on-disk verify, RFC 3339 time). No openvex crate
  dependency — those are unmaintained.
- One statement per vulnerability ID. GHSA becomes `name`, CVEs become
  `aliases`. Two patches that fix the same GHSA merge their
  subcomponents into one statement.
- Auto-detects the product PURL from package.json > pyproject.toml >
  Cargo.toml. `--product` overrides.
- On-disk hash verification is the default; `--no-verify` skips it.
  Patches that fail verification are silently omitted from the
  document (never emitted as `affected` or `under_investigation`) and
  surfaced as stderr warnings / `--json` envelope `skipped` events.
- Exit 0 on success, 1 on no-applicable-patches, 2 on hard errors
  (manifest unreadable, `--json` without `--output`, etc.).

CI installs Go + vexctl before the test step; `tests/e2e_vex.rs`
runs `vexctl inspect` against the generated document whenever the
binary is on PATH. Local devs without Go see a skip message.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@socket-security-staging
Copy link
Copy Markdown

socket-security-staging Bot commented May 24, 2026

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addedgithub/​actions/​setup-go@​4a3601121dd01d1626a1e23e37211e3254c1c06c99100100100100

View full report

@socket-security-staging
Copy link
Copy Markdown

socket-security-staging Bot commented May 24, 2026

All alerts resolved. Learn more about Socket for GitHub.

This PR previously contained dependency changes with security issues that have been resolved, removed, or ignored.

View full report

Org policy (and zizmor) require all action references to use a
40-char SHA pin. Resolved via
`gh api repos/actions/setup-go/git/refs/tags/v6.4.0`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@socket-security
Copy link
Copy Markdown

socket-security Bot commented May 24, 2026

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addedgithub/​actions/​setup-go@​4a3601121dd01d1626a1e23e37211e3254c1c06c99100100100100

View full report

Adds git-remote-origin detection as the top-priority signal in
vex::product::detect_product. The repo is usually the canonical
product identifier when it's a git checkout; package.json /
pyproject.toml / Cargo.toml become fallbacks.

Format:
- git@github.com:owner/repo.git    → pkg:github/owner/repo
- https://github.com/owner/repo    → pkg:github/owner/repo
- gitlab.com / bitbucket.org       → pkg:gitlab|bitbucket/owner/repo
- self-hosted / unknown forge      → raw URL (OpenVEX @id accepts any URI)

Walks ancestors of cwd looking for `.git/config`, so subdir invocations
(common in monorepos) still resolve correctly. SSH (`git@...`),
`ssh://`, `git://`, `git+ssh://`, `http(s)://` all parse to host+path.

Also fixes the vexctl integration: `vexctl inspect` doesn't exist at
v0.3.0; switched to `vexctl list` which loads and parses the document
and exits non-zero on malformed input — the de facto schema gate.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`vexctl list <arg>` parses <arg> as a selector (status / justification),
not a file path. Switch to `vexctl merge --files <path>` which loads,
parses, and re-emits the document — the canonical single-file parse
gate at vexctl v0.3.x. A successful merge proves the input is valid
OpenVEX; we additionally assert the output round-trips through serde
as JSON.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`vexctl merge` accepts file paths positionally, not via `--files`.
Verified locally with vexctl v0.3.0:

    $ vexctl merge ./out.vex.json
    { ... merged document ... }

Exit 0 with our generated doc confirms parse + schema validation
against the OpenVEX 0.2.0 spec.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ompleteness

Drives line coverage of `socket-patch-core::vex` to ~99.7% (1846/1851
lines covered) and locks down OpenVEX 0.2.0 spec conformance.

Schema additions (all `Option<T>`, all `skip_serializing_if = "Option::is_none"`,
no change to what the builder emits today):
- `Document.role`, `Document.last_updated`
- `Statement.@id`, `Statement.last_updated`, `Statement.supplier`,
  `Statement.action_statement`
- `Product.identifiers`, `Product.hashes`
- `Subcomponent.identifiers`, `Subcomponent.hashes`
Backwards-compatible: existing docs round-trip unchanged; new fields
appear only when callers set them.

Test count went from 33 to 144 across the vex module (+111 tests):
- schema.rs:  6 → 24 — every Status/Justification variant, all new
              optional fields, missing-required-field rejection,
              version typing, multi-aliases ordering.
- build.rs:   7 → 17 — applied PURL not in manifest, zero-vuln patch,
              empty CVE list, duplicate CVE dedup, tooling=None,
              empty author, determinism, timestamp consistency,
              subcomponent sort order.
- product.rs: 22 → 53 — `[tool.poetry]` fallback, CRLF git config,
              all URL scheme branches (git+ssh, git://, http://,
              port-suffix, no-user), no-origin/empty-url config
              fallbacks, multi-manifest combos beyond pkg+cargo,
              non-string JSON name/version, missing-version-key,
              parse_toml_kv negative cases, three-segment URL path,
              trailing-slash normalization.
- verify.rs:  5 → 11 — empty manifest, zero-file patch (vacuous),
              extra package_paths ignored, multi-file short-circuit,
              Default/Clone/Eq impls.
- time.rs:    5 → 15 — non-leap Feb, year-end boundary, century
              non-leap (2100), 400-year leap (2000), every
              month-length transition, u64::MAX no-panic.
- mod.rs:     0 → 1 — re-export smoke test (compile-time guard).
- conformance_tests.rs (new): 17 cross-cutting tests pinning OpenVEX
              spec rules — @context literal, JSON-LD @-prefixed keys,
              status/justification interaction (action_statement
              reserved for status=affected; not_affected requires
              justification), required-field presence, non-empty
              identifiers, timestamp consistency, version=1,
              no-null invariant, alias/subcomponent uniqueness.

Remaining 5 uncovered regions documented in-source as unreachable in
practice (e.g. `civil_from_days` negative-`z` arm — requires inputs
past year ~292 billion).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@mikolalysenko Mikola Lysenko (mikolalysenko) merged commit 8ee38a3 into main May 26, 2026
79 of 80 checks passed
@mikolalysenko Mikola Lysenko (mikolalysenko) deleted the feat/openvex-vex-subcommand branch May 26, 2026 18:05
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.

2 participants