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
128 changes: 128 additions & 0 deletions crates/socket-patch-cli/tests/docker_common/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
//! Container plumbing shared by every `docker_e2e_*` suite. Include it with
//! `#[path = "docker_common/mod.rs"] mod docker_common;` (suites that also
//! include `docker_vendor_common` need both: that module builds on this one).
//!
//! `#![allow(dead_code)]`: each suite uses a different subset.

#![allow(dead_code)]

use std::process::{Command, Output};

/// Docker args that shadow the image's baked-in socket-patch. With
/// `SOCKET_PATCH_DOCKER_BIN` set, that host binary is mounted (a local Linux
/// build). Otherwise the CI coverage-docker job sets `SOCKET_PATCH_COV_BIN`
/// (an llvm-cov-instrumented binary) and `SOCKET_PATCH_COV_PROFRAW_DIR` (where
/// the in-container profraws land). Locally all are unset and this is empty.
pub fn cov_docker_args() -> Vec<String> {
if let Ok(bin) = std::env::var("SOCKET_PATCH_DOCKER_BIN") {
return vec!["-v".into(), format!("{bin}:/usr/local/bin/socket-patch:ro")];
}
let (Ok(bin), Ok(dir)) = (
std::env::var("SOCKET_PATCH_COV_BIN"),
std::env::var("SOCKET_PATCH_COV_PROFRAW_DIR"),
) else {
return Vec::new();
};
vec![
"-v".into(),
format!("{bin}:/usr/local/bin/socket-patch:ro"),
"-v".into(),
format!("{dir}:/coverage"),
"-e".into(),
"LLVM_PROFILE_FILE=/coverage/docker-e2e-%p-%14m.profraw".into(),
]
}

/// Returns `true` when the test should skip: `docker` missing from PATH or
/// `image` not built. Prints a skip notice, because Rust integration tests
/// have no native "skipped" outcome and the test then reports `ok`. Build
/// locally with `docker build -f tests/docker/Dockerfile.base -t
/// socket-patch-test-base:latest .`, then `docker build -f
/// tests/docker/Dockerfile.<eco> -t socket-patch-test-<eco>:latest .`.
///
/// With `SOCKET_PATCH_DOCKER_E2E_REQUIRED=1` (a CI job that built the image
/// itself) a missing docker or image fails the test instead, so a tag
/// mismatch cannot pass the job with zero assertions.
#[must_use]
pub fn skip_if_no_image(image: &str) -> bool {
skip_or_require_image(
image,
std::env::var("SOCKET_PATCH_DOCKER_E2E_REQUIRED").is_ok_and(|v| v == "1"),
)
}

/// [`skip_if_no_image`] with the required switch passed in.
#[must_use]
pub fn skip_or_require_image(image: &str, required: bool) -> bool {
let reason = match Command::new("docker")
.args(["image", "inspect", image])
.output()
{
Err(_) => "`docker` not on PATH".to_string(),
Ok(out) if !out.status.success() => format!("docker image `{image}` not present"),
Ok(_) => return false,
};
assert!(
!required,
"{reason}; SOCKET_PATCH_DOCKER_E2E_REQUIRED=1 forbids skipping"
);
eprintln!("skipping: {reason}");
true
}

/// Run `script` (bash) in a throwaway `image` container that can reach a
/// host mock server at `host.docker.internal`.
pub fn run_container(image: &str, script: &str) -> Output {
Command::new("docker")
.args([
"run",
"--rm",
"--add-host=host.docker.internal:host-gateway",
"-i",
])
.args(cov_docker_args())
.args([image, "bash", "-c", script])
.output()
.expect("docker run")
}

/// Host-side oracle over the VEX document the container printed between
/// `===VEX DOC BEGIN===` and `===VEX DOC END===` (the agent suites keep the
/// workspace inside the container, so the doc comes from captured stdout).
/// Asserts exactly one `not_affected` statement for `ghsa` on the
/// `subcomponent_purl`, with a PLAIN impact statement: no `(vendored)` or
/// `(redirected)` marker, which is what distinguishes agent provenance.
pub fn assert_vex_agent_attested(stdout: &str, subcomponent_purl: &str, ghsa: &str) {
const BEGIN: &str = "===VEX DOC BEGIN===";
const END: &str = "===VEX DOC END===";
let start = stdout
.find(BEGIN)
.unwrap_or_else(|| panic!("VEX DOC BEGIN marker missing from stdout:\n{stdout}"))
+ BEGIN.len();
let stop = stdout[start..]
.find(END)
.unwrap_or_else(|| panic!("VEX DOC END marker missing from stdout:\n{stdout}"))
+ start;
let doc: serde_json::Value = serde_json::from_str(stdout[start..stop].trim())
.expect("emitted VEX document must be valid JSON");
let stmts = doc["statements"]
.as_array()
.expect("VEX document must have a statements array");
assert_eq!(stmts.len(), 1, "exactly one VEX statement expected: {doc}");
let st = &stmts[0];
assert_eq!(st["vulnerability"]["name"], ghsa, "attested GHSA mismatch");
assert_eq!(st["status"], "not_affected");
assert_eq!(
st["products"][0]["subcomponents"][0]["@id"], subcomponent_purl,
"subcomponent must be the patched package purl"
);
let impact = st["impact_statement"]
.as_str()
.expect("statement must carry an impact_statement");
assert!(
impact.contains("Patched via Socket patch")
&& !impact.contains("(vendored)")
&& !impact.contains("(redirected)"),
"agent-mode attestation must carry a PLAIN impact statement (no vendored/redirected marker): {impact}"
);
}
113 changes: 7 additions & 106 deletions crates/socket-patch-cli/tests/docker_e2e_cargo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,16 @@

#[path = "common/mod.rs"]
mod common;
#[path = "docker_common/mod.rs"]
mod docker_common;
use common::git_sha256;

use std::process::Command;
use docker_common::{assert_vex_agent_attested, run_container, skip_if_no_image};

use base64::Engine;
use wiremock::matchers::{method, path, path_regex};
use wiremock::{Mock, MockServer, ResponseTemplate};

const IMAGE: &str = "socket-patch-test-cargo:latest";
const ORG: &str = "test-org";
const PURL: &str = "pkg:cargo/cfg-if@1.0.0";
const UUID: &str = "14141414-1414-4141-8141-141414141414";
Expand All @@ -31,26 +33,6 @@ const PATCHED_RS: &[u8] = b"// SOCKET-PATCH-E2E-MARKER\n\
#[macro_export]\n\
macro_rules! cfg_if {\n ($($t:tt)*) => {};\n}\n";

/// See docker_e2e_npm.rs::cov_docker_args for the coverage hook
/// semantics. The CI coverage-docker job sets the env vars; locally
/// they're unset and this returns an empty Vec.
fn cov_docker_args() -> Vec<String> {
let Ok(bin) = std::env::var("SOCKET_PATCH_COV_BIN") else {
return Vec::new();
};
let Ok(dir) = std::env::var("SOCKET_PATCH_COV_PROFRAW_DIR") else {
return Vec::new();
};
vec![
"-v".into(),
format!("{bin}:/usr/local/bin/socket-patch:ro"),
"-v".into(),
format!("{dir}:/coverage"),
"-e".into(),
"LLVM_PROFILE_FILE=/coverage/docker-e2e-%p-%14m.profraw".into(),
]
}

async fn make_mock_server(after_hash: &str) -> MockServer {
let listener = std::net::TcpListener::bind("0.0.0.0:0").expect("bind wiremock");
let server = MockServer::builder().listener(listener).start().await;
Expand Down Expand Up @@ -324,96 +306,15 @@ exit 0
)
}

/// Host-side oracle over the VEX document the container emitted between the
/// `===VEX DOC BEGIN===` / `===VEX DOC END===` markers (these agent suites run
/// the workspace inside the container with no bind mount, so the doc is parsed
/// from captured stdout). Asserts exactly one statement attesting the agent
/// patch: the fixture GHSA, `not_affected`, the installed-package subcomponent
/// purl, and a PLAIN impact statement with NO `(vendored)`/`(redirected)`
/// marker — the marker's absence is what distinguishes agent provenance.
fn assert_vex_agent_attested(stdout: &str, subcomponent_purl: &str) {
const BEGIN: &str = "===VEX DOC BEGIN===";
const END: &str = "===VEX DOC END===";
let start = stdout
.find(BEGIN)
.unwrap_or_else(|| panic!("VEX DOC BEGIN marker missing from stdout:\n{stdout}"))
+ BEGIN.len();
let stop = stdout[start..]
.find(END)
.unwrap_or_else(|| panic!("VEX DOC END marker missing from stdout:\n{stdout}"))
+ start;
let doc: serde_json::Value = serde_json::from_str(stdout[start..stop].trim())
.expect("emitted VEX document must be valid JSON");
let stmts = doc["statements"]
.as_array()
.expect("VEX document must have a statements array");
assert_eq!(stmts.len(), 1, "exactly one VEX statement expected: {doc}");
let st = &stmts[0];
assert_eq!(st["vulnerability"]["name"], GHSA, "attested GHSA mismatch");
assert_eq!(st["status"], "not_affected");
assert_eq!(
st["products"][0]["subcomponents"][0]["@id"], subcomponent_purl,
"subcomponent must be the patched package purl"
);
let impact = st["impact_statement"]
.as_str()
.expect("statement must carry an impact_statement");
assert!(
impact.contains("Patched via Socket patch")
&& !impact.contains("(vendored)")
&& !impact.contains("(redirected)"),
"agent-mode attestation must carry a PLAIN impact statement (no vendored/redirected marker): {impact}"
);
}

/// Returns `true` when the test should skip (docker missing, image
/// missing). Prints a skip notice to stderr in that case so the test
/// log shows *why* the test did nothing — the test still reports as
/// `ok` because Rust integration tests have no native "skipped" outcome.
///
/// Local devs: build the image with
/// `docker build -f tests/docker/Dockerfile.base -t socket-patch-test-base:latest .`
/// then
/// `docker build -f tests/docker/Dockerfile.cargo -t socket-patch-test-cargo:latest .`
#[must_use]
fn skip_if_no_image() -> bool {
let Ok(out) = Command::new("docker")
.args(["image", "inspect", "socket-patch-test-cargo:latest"])
.output()
else {
eprintln!("skipping: `docker` not on PATH");
return true;
};
if !out.status.success() {
eprintln!("skipping: docker image `socket-patch-test-cargo:latest` not present");
return true;
}
false
}

#[tokio::test]
async fn cargo_fetch_full_apply_chain() {
let after_hash = git_sha256(PATCHED_RS);
let server = make_mock_server(&after_hash).await;
let api_url = format!("http://host.docker.internal:{}", server.address().port());
if skip_if_no_image() {
if skip_if_no_image(IMAGE) {
return;
}
let mut cmd = Command::new("docker");
cmd.args([
"run",
"--rm",
"--add-host=host.docker.internal:host-gateway",
"-i",
])
.args(cov_docker_args())
.args([
"socket-patch-test-cargo:latest",
"bash",
"-c",
&local_script(&api_url, &after_hash),
]);
let out = cmd.output().expect("docker run");
let out = run_container(IMAGE, &local_script(&api_url, &after_hash));
let stdout = String::from_utf8_lossy(&out.stdout);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
Expand All @@ -429,7 +330,7 @@ async fn cargo_fetch_full_apply_chain() {
stderr.contains("===VEX VERIFIED==="),
"agent-mode VEX leg did not run/pass (===VEX VERIFIED=== missing).\nstderr=\n{stderr}"
);
assert_vex_agent_attested(&stdout, PURL);
assert_vex_agent_attested(&stdout, PURL, GHSA);

// The script gates on an exact git-blob-hash match; confirm the
// expected hash actually appears in the log so a future edit that
Expand Down
Loading
Loading