Skip to content

Commit 3ef6786

Browse files
feat: add supply chain artifact verification (#431)
1 parent f6a6ae6 commit 3ef6786

7 files changed

Lines changed: 1037 additions & 0 deletions

File tree

.github/workflows/supply-chain.yml

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
name: Supply Chain
2+
3+
on:
4+
schedule:
5+
- cron: "17 4 * * *"
6+
push:
7+
branches:
8+
- main
9+
workflow_dispatch:
10+
11+
jobs:
12+
supply-chain:
13+
runs-on: ubuntu-latest
14+
timeout-minutes: 15
15+
16+
steps:
17+
- name: Checkout
18+
uses: actions/checkout@v4
19+
20+
- name: Set up Rust
21+
uses: dtolnay/rust-toolchain@stable
22+
with:
23+
toolchain: 1.94.0
24+
components: rustfmt, clippy
25+
26+
- name: Cache cargo
27+
uses: Swatinem/rust-cache@v2
28+
29+
- name: Run supply-chain checks
30+
run: bash scripts/ci/supply_chain_check.sh
31+
32+
- name: Upload supply-chain evidence
33+
if: always()
34+
uses: actions/upload-artifact@v4
35+
with:
36+
name: traverse-supply-chain-evidence
37+
path: |
38+
target/supply-chain/traverse-sbom.cdx.json
39+
target/supply-chain/supply-chain-summary.json
40+
target/supply-chain/artifact-verify-report.json
41+
target/supply-chain/traverse-cli.provenance.json

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,8 @@ unwrap_used = "deny"
2626
expect_used = "deny"
2727
panic = "deny"
2828
todo = "deny"
29+
30+
[workspace.metadata.cyclonedx]
31+
all_features = true
32+
format = "json"
33+
output = "traverse-sbom.cdx.json"

crates/traverse-cli/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ rust-version.workspace = true
1010
semver = "1.0.27"
1111
serde = { version = "1.0.228", features = ["derive"] }
1212
serde_json = "1.0.145"
13+
sha2 = "0.10"
1314
traverse-contracts = { path = "../traverse-contracts" }
1415
traverse-registry = { path = "../traverse-registry" }
1516
traverse-runtime = { path = "../traverse-runtime" }

crates/traverse-cli/src/main.rs

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ mod agent_packages;
22
mod browser_adapter;
33
mod federation_operator;
44
mod http_api;
5+
mod supply_chain;
56

67
use agent_packages::load_agent_package;
78
use browser_adapter::serve_local_browser_adapter;
@@ -56,6 +57,9 @@ enum Command {
5657
WasmAbiVerify {
5758
wasm_paths: Vec<PathBuf>,
5859
},
60+
ArtifactVerify {
61+
artifact_path: PathBuf,
62+
},
5963
FederationPeers {
6064
manifest_path: PathBuf,
6165
},
@@ -202,6 +206,7 @@ fn run_command(command: Command) -> Result<String, CliError> {
202206
request_path,
203207
} => execute_agent(&manifest_path, &request_path),
204208
Command::WasmAbiVerify { wasm_paths } => verify_wasm_abi_imports(&wasm_paths),
209+
Command::ArtifactVerify { artifact_path } => verify_supply_chain_artifact(&artifact_path),
205210
Command::FederationPeers { manifest_path } => {
206211
render_federation_peers(&manifest_path).map_err(CliError::IoError)
207212
}
@@ -263,6 +268,7 @@ fn parse_command(args: &[String]) -> Result<Command, String> {
263268
(Some("serve"), _) => parse_serve_command(args),
264269
(Some("federation"), Some(_)) => parse_federation_command(args),
265270
(Some("agent"), Some("execute")) => parse_agent_execute_command(args),
271+
(Some("artifact"), Some("verify")) => parse_artifact_verify_command(args),
266272
(Some("wasm"), Some("abi")) => parse_wasm_abi_command(args),
267273
(Some("expedition"), Some("execute")) => parse_expedition_execute_command(args),
268274
(Some("capability"), Some("discover")) => parse_capability_discover_command(args),
@@ -279,6 +285,8 @@ fn subcommand_help(family: Option<&str>, subcommand: Option<&str>) -> String {
279285
(Some("agent"), Some("inspect")) => help_agent_inspect(),
280286
(Some("agent"), Some("execute")) => help_agent_execute(),
281287
(Some("agent"), _) => help_agent(),
288+
(Some("artifact"), Some("verify")) => help_artifact_verify(),
289+
(Some("artifact"), _) => help_artifact(),
282290
(Some("wasm"), Some("abi")) => help_wasm_abi(),
283291
(Some("wasm"), _) => help_wasm(),
284292
(Some("workflow"), Some("register")) => help_workflow_register(),
@@ -402,6 +410,36 @@ fn help_agent() -> String {
402410
.to_string()
403411
}
404412

413+
fn help_artifact_verify() -> String {
414+
"traverse-cli artifact verify <artifact-or-manifest-path>
415+
416+
Purpose:
417+
Verify one governed artifact's supply-chain evidence. The command reads
418+
either a manifest JSON path or an artifact path with sidecars named
419+
<artifact>.manifest.json and <artifact>.provenance.json, then emits a
420+
structured JSON report for checksum, signature, and provenance checks.
421+
422+
Required arguments:
423+
<artifact-or-manifest-path> Artifact file or artifact manifest JSON path.
424+
425+
Optional flags:
426+
--help Print this help text.
427+
428+
Example:
429+
traverse-cli artifact verify target/release/traverse-cli"
430+
.to_string()
431+
}
432+
433+
fn help_artifact() -> String {
434+
"traverse-cli artifact <subcommand> [options]
435+
436+
Subcommands:
437+
verify <artifact-or-manifest-path> Verify checksum, signature, and provenance evidence.
438+
439+
Run `traverse-cli artifact verify --help` for subcommand-specific help."
440+
.to_string()
441+
}
442+
405443
fn help_wasm_abi() -> String {
406444
"traverse-cli wasm abi verify <wasm-path>...
407445
@@ -824,6 +862,15 @@ fn parse_fixed_arity_command(args: &[String]) -> Result<Command, String> {
824862
}
825863
}
826864

865+
fn parse_artifact_verify_command(args: &[String]) -> Result<Command, String> {
866+
match args {
867+
[_, _, _, artifact_path] => Ok(Command::ArtifactVerify {
868+
artifact_path: PathBuf::from(artifact_path),
869+
}),
870+
_ => Err(usage()),
871+
}
872+
}
873+
827874
fn parse_agent_execute_command(args: &[String]) -> Result<Command, String> {
828875
match args {
829876
[_, _, _, manifest_path, request_path] => Ok(Command::AgentExecute {
@@ -1064,6 +1111,17 @@ fn verify_wasm_abi_imports(wasm_paths: &[PathBuf]) -> Result<String, CliError> {
10641111
Ok(lines.join("\n"))
10651112
}
10661113

1114+
fn verify_supply_chain_artifact(artifact_path: &Path) -> Result<String, CliError> {
1115+
let report = supply_chain::verify_artifact(artifact_path);
1116+
let json = serde_json::to_string_pretty(&report)
1117+
.map_err(|e| CliError::IoError(format!("failed to serialize artifact report: {e}")))?;
1118+
if report.passed() {
1119+
Ok(json)
1120+
} else {
1121+
Err(CliError::ValidationFailed(json))
1122+
}
1123+
}
1124+
10671125
fn execute_expedition(
10681126
request_path: &Path,
10691127
trace_output_path: Option<&Path>,
@@ -2428,6 +2486,12 @@ mod tests {
24282486
"verify".to_string(),
24292487
"examples/hello-world/say-hello-agent/artifacts/say-hello-agent.wasm".to_string(),
24302488
];
2489+
let artifact_verify = vec![
2490+
"traverse-cli".to_string(),
2491+
"artifact".to_string(),
2492+
"verify".to_string(),
2493+
"target/release/traverse-cli".to_string(),
2494+
];
24312495
let expedition_execute = vec![
24322496
"traverse-cli".to_string(),
24332497
"expedition".to_string(),
@@ -2467,6 +2531,7 @@ mod tests {
24672531
assert!(parse_command(&agent_inspect).is_ok());
24682532
assert!(parse_command(&agent_execute).is_ok());
24692533
assert!(parse_command(&wasm_abi_verify).is_ok());
2534+
assert!(parse_command(&artifact_verify).is_ok());
24702535
assert!(parse_command(&expedition_execute).is_ok());
24712536
assert!(parse_command(&expedition_execute_with_trace).is_ok());
24722537
assert!(parse_command(&event).is_ok());
@@ -2678,6 +2743,22 @@ mod tests {
26782743
assert!(text.contains("Example:"));
26792744
}
26802745

2746+
#[test]
2747+
fn parse_command_returns_artifact_verify_help_on_help_flag() {
2748+
let args = vec![
2749+
"traverse-cli".to_string(),
2750+
"artifact".to_string(),
2751+
"verify".to_string(),
2752+
"--help".to_string(),
2753+
];
2754+
let result = parse_command(&args);
2755+
assert!(result.is_err(), "expected Err for --help");
2756+
let text = result.err().unwrap_or_default();
2757+
assert!(text.contains("artifact verify"));
2758+
assert!(text.contains("<artifact-or-manifest-path>"));
2759+
assert!(text.contains("Example:"));
2760+
}
2761+
26812762
#[test]
26822763
fn parse_command_returns_workflow_inspect_help_on_help_flag() {
26832764
let args = vec![

0 commit comments

Comments
 (0)