use std::collections::HashMap;
use std::path::{Path, PathBuf};
use clap::Args;
use socket_patch_core::crawlers::CrawlerOptions;
use socket_patch_core::manifest::operations::read_manifest;
use socket_patch_core::manifest::schema::PatchManifest;
use socket_patch_core::utils::telemetry::{track_vex_failed, track_vex_generated};
use socket_patch_core::vex::{
build_document, detect_product, BuildOptions, Document, FailedPatch, VerifyOutcome,
};
use crate::args::{apply_env_toggles, GlobalArgs};
use crate::ecosystem_dispatch::{find_packages_for_rollback, partition_purls};
use crate::json_envelope::{
Command, Envelope, EnvelopeError, PatchAction, PatchEvent,
};
#[derive(Args)]
pub struct VexArgs {
#[command(flatten)]
pub common: GlobalArgs,
#[arg(long = "output", short = 'O', env = "SOCKET_VEX_OUTPUT")]
pub output: Option<PathBuf>,
#[arg(long = "product", env = "SOCKET_VEX_PRODUCT")]
pub product: Option<String>,
#[arg(long = "no-verify", env = "SOCKET_VEX_NO_VERIFY", default_value_t = false)]
pub no_verify: bool,
#[arg(long = "doc-id", env = "SOCKET_VEX_DOC_ID")]
pub doc_id: Option<String>,
#[arg(long = "compact", env = "SOCKET_VEX_COMPACT", default_value_t = false)]
pub compact: bool,
}
#[derive(Args, Default, Clone)]
pub struct VexEmbedArgs {
#[arg(long = "vex", env = "SOCKET_VEX")]
pub vex: Option<PathBuf>,
#[arg(long = "vex-product", env = "SOCKET_VEX_PRODUCT")]
pub vex_product: Option<String>,
#[arg(long = "vex-no-verify", env = "SOCKET_VEX_NO_VERIFY", default_value_t = false)]
pub vex_no_verify: bool,
#[arg(long = "vex-doc-id", env = "SOCKET_VEX_DOC_ID")]
pub vex_doc_id: Option<String>,
#[arg(long = "vex-compact", env = "SOCKET_VEX_COMPACT", default_value_t = false)]
pub vex_compact: bool,
}
impl VexEmbedArgs {
pub(crate) fn to_build_params(&self) -> VexBuildParams {
VexBuildParams {
output: self.vex.clone(),
product: self.vex_product.clone(),
no_verify: self.vex_no_verify,
doc_id: self.vex_doc_id.clone(),
compact: self.vex_compact,
}
}
}
pub(crate) struct VexBuildParams {
pub output: Option<PathBuf>,
pub product: Option<String>,
pub no_verify: bool,
pub doc_id: Option<String>,
pub compact: bool,
}
pub(crate) struct VexWriteSummary {
pub statements: usize,
pub failed: Vec<FailedPatch>,
pub wrote_to_file: bool,
pub doc: Document,
}
pub(crate) struct VexGenError {
pub code: &'static str,
pub message: String,
pub failed: Vec<FailedPatch>,
}
pub async fn run(args: VexArgs) -> i32 {
apply_env_toggles(&args.common);
if args.common.json && args.output.is_none() {
emit_envelope_error_and_track(
&args,
"json_requires_output",
"--json requires --output (the VEX document is itself JSON; \
route it to a file so the envelope can use stdout)",
)
.await;
return 2;
}
let manifest_path = args.common.resolved_manifest_path();
let manifest = match read_manifest(&manifest_path).await {
Ok(Some(m)) => m,
Ok(None) => {
emit_envelope_error_and_track(
&args,
"manifest_not_found",
&format!("Manifest not found at {}", manifest_path.display()),
)
.await;
return 2;
}
Err(e) => {
emit_envelope_error_and_track(&args, "manifest_unreadable", &e.to_string()).await;
return 2;
}
};
if manifest.patches.is_empty() {
emit_envelope_error_and_track(
&args,
"no_patches",
"Manifest is empty — nothing to attest. Run `socket-patch get` \
or `socket-patch scan --sync` first.",
)
.await;
return 1;
}
let params = VexBuildParams {
output: args.output.clone(),
product: args.product.clone(),
no_verify: args.no_verify,
doc_id: args.doc_id.clone(),
compact: args.compact,
};
match generate_vex(&args.common, ¶ms, &manifest).await {
Ok(summary) => {
if args.common.json {
emit_envelope_success(&summary.doc, &summary.failed);
} else if summary.wrote_to_file {
if !args.common.silent {
let path = args.output.as_ref().unwrap().display();
println!(
"Wrote OpenVEX document with {} statement(s) to {path}",
summary.statements
);
}
} else if !args.common.silent {
eprintln!("Emitted {} VEX statement(s)", summary.statements);
}
0
}
Err(e) if e.code == "no_applicable_patches" => {
emit_envelope_error_with_failures(&args, e.code, &e.message, &e.failed);
1
}
Err(e) => {
emit_envelope_error(&args, e.code, &e.message);
2
}
}
}
pub(crate) async fn generate_vex(
common: &GlobalArgs,
params: &VexBuildParams,
manifest: &PatchManifest,
) -> Result<VexWriteSummary, VexGenError> {
let product_id = match resolve_product_id(common, params.product.as_deref()).await {
Ok(id) => id,
Err(reason) => return Err(fail(common, "product_undetected", reason).await),
};
let outcome = if params.no_verify {
VerifyOutcome {
applied: manifest.patches.keys().cloned().collect(),
failed: Vec::new(),
}
} else {
let package_paths = resolve_package_paths(common, manifest).await;
socket_patch_core::vex::applied_patches(manifest, &package_paths).await
};
if !outcome.failed.is_empty() && !common.silent && !common.json {
for f in &outcome.failed {
eprintln!(
"Warning: omitting patch for {} from VEX ({})",
f.purl, f.reason
);
}
}
let opts = BuildOptions {
product_id,
doc_id: params
.doc_id
.clone()
.unwrap_or_else(|| format!("urn:uuid:{}", uuid::Uuid::new_v4())),
author: "Socket".to_string(),
tooling: Some(format!("socket-patch {}", env!("CARGO_PKG_VERSION"))),
};
let doc = match build_document(manifest, &outcome.applied, &opts) {
Some(doc) => doc,
None => {
track_vex_failed(
"no_applicable_patches",
common.api_token.as_deref(),
common.org.as_deref(),
)
.await;
return Err(VexGenError {
code: "no_applicable_patches",
message: "No applied patches with vulnerability metadata to attest.".to_string(),
failed: outcome.failed,
});
}
};
let serialized = if params.compact {
match serde_json::to_string(&doc) {
Ok(s) => s,
Err(e) => return Err(fail(common, "serialize_failed", e.to_string()).await),
}
} else {
match serde_json::to_string_pretty(&doc) {
Ok(s) => s,
Err(e) => return Err(fail(common, "serialize_failed", e.to_string()).await),
}
};
let wrote_to_file = match ¶ms.output {
Some(path) => {
if let Err(e) = tokio::fs::write(path, &serialized).await {
return Err(fail(common, "write_failed", e.to_string()).await);
}
true
}
None => {
println!("{serialized}");
false
}
};
track_vex_generated(
doc.statements.len(),
"openvex-0.2.0",
if wrote_to_file { "file" } else { "stdout" },
common.api_token.as_deref(),
common.org.as_deref(),
)
.await;
Ok(VexWriteSummary {
statements: doc.statements.len(),
failed: outcome.failed,
wrote_to_file,
doc,
})
}
pub(crate) async fn generate_vex_from_manifest_path(
common: &GlobalArgs,
params: &VexBuildParams,
manifest_path: &Path,
) -> Result<VexWriteSummary, VexGenError> {
let manifest = match read_manifest(manifest_path).await {
Ok(Some(m)) => m,
Ok(None) => {
return Err(fail(
common,
"manifest_not_found",
format!("Manifest not found at {}", manifest_path.display()),
)
.await)
}
Err(e) => return Err(fail(common, "manifest_unreadable", e.to_string()).await),
};
if manifest.patches.is_empty() {
return Err(fail(
common,
"no_patches",
"Manifest is empty — nothing to attest.".to_string(),
)
.await);
}
generate_vex(common, params, &manifest).await
}
async fn fail(common: &GlobalArgs, code: &'static str, message: String) -> VexGenError {
track_vex_failed(code, common.api_token.as_deref(), common.org.as_deref()).await;
VexGenError {
code,
message,
failed: Vec::new(),
}
}
async fn resolve_product_id(common: &GlobalArgs, product: Option<&str>) -> Result<String, String> {
if let Some(p) = product {
return Ok(p.to_string());
}
let detect = detect_product(&common.cwd).await;
for w in &detect.warnings {
if !common.silent && !common.json {
eprintln!("Warning: {w}");
}
}
detect.purl.ok_or_else(|| {
format!(
"Could not auto-detect a top-level product PURL in {}. \
Provide one with --product <purl> (e.g. pkg:npm/my-app@1.0.0).",
common.cwd.display()
)
})
}
async fn resolve_package_paths(
common: &GlobalArgs,
manifest: &PatchManifest,
) -> HashMap<String, PathBuf> {
let purls: Vec<String> = manifest.patches.keys().cloned().collect();
let partitioned = partition_purls(&purls, common.ecosystems.as_deref());
let crawler_options = CrawlerOptions {
cwd: common.cwd.clone(),
global: common.global,
global_prefix: common.global_prefix.clone(),
batch_size: 0, };
find_packages_for_rollback(&partitioned, &crawler_options, common.silent).await
}
fn emit_envelope_error(args: &VexArgs, code: &str, message: &str) {
if args.common.json {
let mut env = Envelope::new(Command::Vex);
env.mark_error(EnvelopeError::new(code, message.to_string()));
println!("{}", env.to_pretty_json());
} else {
eprintln!("Error: {message}");
}
}
async fn emit_envelope_error_and_track(args: &VexArgs, code: &str, message: &str) {
track_vex_failed(
code,
args.common.api_token.as_deref(),
args.common.org.as_deref(),
)
.await;
emit_envelope_error(args, code, message);
}
fn emit_envelope_error_with_failures(
args: &VexArgs,
code: &str,
message: &str,
failures: &[FailedPatch],
) {
if args.common.json {
let mut env = Envelope::new(Command::Vex);
for f in failures {
env.record(
PatchEvent::new(PatchAction::Skipped, f.purl.clone())
.with_reason(f.reason.clone(), "patch omitted from VEX"),
);
}
env.mark_error(EnvelopeError::new(code, message.to_string()));
println!("{}", env.to_pretty_json());
} else {
eprintln!("Error: {message}");
for f in failures {
eprintln!(" omitted: {} ({})", f.purl, f.reason);
}
}
}
fn emit_envelope_success(doc: &Document, failures: &[FailedPatch]) {
let mut env = Envelope::new(Command::Vex);
for st in &doc.statements {
for prod in &st.products {
for sub in &prod.subcomponents {
env.record(
PatchEvent::new(PatchAction::Verified, sub.id.clone())
.with_details(serde_json::json!({
"vulnerability": st.vulnerability.name,
"aliases": st.vulnerability.aliases,
"status": "not_affected",
})),
);
}
}
}
for f in failures {
env.record(
PatchEvent::new(PatchAction::Skipped, f.purl.clone())
.with_reason(f.reason.clone(), "patch omitted from VEX"),
);
}
if !failures.is_empty() {
env.mark_partial_failure();
}
println!("{}", env.to_pretty_json());
}
#[cfg(test)]
mod tests {
use super::*;
use clap::Parser;
#[derive(Parser)]
struct Wrap {
#[command(subcommand)]
cmd: Sub,
}
#[derive(clap::Subcommand)]
enum Sub {
Vex(VexArgs),
}
#[test]
fn parses_with_defaults() {
let w = Wrap::parse_from(["test", "vex"]);
match w.cmd {
Sub::Vex(args) => {
assert!(args.output.is_none());
assert!(args.product.is_none());
assert!(!args.no_verify);
assert!(args.doc_id.is_none());
assert!(!args.compact);
}
}
}
#[test]
fn parses_all_flags() {
let w = Wrap::parse_from([
"test",
"vex",
"--output",
"out.vex.json",
"--product",
"pkg:npm/app@1.0.0",
"--no-verify",
"--doc-id",
"urn:uuid:fixed",
"--compact",
]);
match w.cmd {
Sub::Vex(args) => {
assert_eq!(args.output.unwrap().to_str(), Some("out.vex.json"));
assert_eq!(args.product.as_deref(), Some("pkg:npm/app@1.0.0"));
assert!(args.no_verify);
assert_eq!(args.doc_id.as_deref(), Some("urn:uuid:fixed"));
assert!(args.compact);
}
}
}
}