commit a7d0e471bd32d891248762b8f6d8d6428fa16f9d
parent ec17de580ec174b2c0915d9b915e5cdf9272dfe8
Author: triesap <tyson@radroots.org>
Date: Wed, 9 Sep 2026 22:06:03 +0000
xtask: Repair coverage attribution and admission qualification
- Measure tooling sources from fresh owned coverage artifacts
- Reject malformed advisory data and nonconforming ELF binaries
- Exercise bounded process, filesystem and retained validator failures
- Verify all required package coverage, APIs and release preflight
Diffstat:
23 files changed, 5932 insertions(+), 368 deletions(-)
diff --git a/contracts/architecture/deviations.toml b/contracts/architecture/deviations.toml
@@ -2,6 +2,43 @@ schema_version = 1
architecture_id = "radroots.crates.release.v1"
[[deviation]]
+id = "RCRV1-DEV-014"
+date = "2026-09-09"
+status = "closed"
+approval = "Explicit user authorization to repair required repository coverage and release-preflight blockers while preserving quality thresholds and deferred qualification scope."
+affected_steps = ["315"]
+spec_anchors = [
+ "contracts/crates/release_v1/radroots_crates_release_v1.toml#quality_policy.coverage",
+]
+source_evidence = [
+ "Current release preflight requires all current coverage-policy package reports and their aggregate; the aggregate is absent and the existing xtask line/branch measurements are below policy.",
+ "The advisory, bounded-process and safe-artifact contracts name executable self-tests, but Cargo tests check their decision metadata without executing the complete CLI self-test suites.",
+ "The detailed coverage scope mapper assumes crates/<package>/src even for xtask, whose source is tools/xtask/src; this selects no detailed functions or regions and reports empty perfect percentages.",
+ "A deterministic malformed RustSec affected-object regression reproduces a panic when the functions member is absent; checked optional lookup must return the existing InvalidReport error instead.",
+ "Workspace-only coverage cleanup leaves orphan xtask executables under debug/build/xtask with older source maps. A measured one-line parser shift adds stale branch/region records; reset the owned coverage build tree before current collection instead of filtering those records or trusting contaminated metrics.",
+ "Retained gate and platform validators lack isolated argument, canonical-authority, inventory and result-encoding coverage. Extract those private transformations without changing pinned historical contracts or invoking their external qualification commands.",
+ "Synthetic parsed ELF fixtures reproduce acceptance of 32-bit and big-endian binaries despite the existing elf64_little_endian_x86_64_execute_or_pie contract. Enforce the existing class and byte-order requirements alongside machine, entrypoint and executable-segment validation.",
+ "Normalized Gradle graph validation is coupled to descriptor-bound admission that rejects changed projections before their structural checks. Isolate the existing private structural validator, preserve its call order, and exercise malformed graph identity, lineage and artifact bindings without fabricating admitted filesystem evidence.",
+ "Gradle projection metadata and process-receipt bindings share the same admitted-projection short circuit as graph structure. Isolate their private validator without changing check order or errors so receipt, raw-source, count and normalization bindings can be tested with structurally valid inputs.",
+ "Fresh collection succeeds for all 49 required packages, with 47 passing every coverage gate. Runtime paths has 82.407407 percent branch coverage and service SQLite has 89.316239 percent; their other metrics pass. Add focused owner tests for retained directory identity and cleanup, bounded schema catalogs, metadata and migration-history rejection without changing public behavior, policy or historical qualification.",
+]
+replacement_action = "Qualify current required-package behavior through the governed coverage commands, execute omitted existing CLI self-tests, and add narrow owner tests or testability repairs justified by actual uncovered paths. This repairs current qualification and does not reopen historical release or deferred Nix/device gates."
+verification = [
+ "Current owned command self-tests and their failure assertions execute from Cargo integration tests with no external provider or Nix access.",
+ "All currently required package reports retain the existing coverage policy; the generated aggregate and release preflight must pass before qualification is complete.",
+ "Relevant Rust, contract, architecture, API, feature and generated-freshness checks remain required.",
+]
+unresolved_risk = "Current macOS aarch64 qualification is complete. Deferred Nix, device, historical release and deployment qualification remain outside this repair; two unchanged opt-in SDK generators are not claimed by the workspace test lane."
+normative_architecture_change = false
+adr_required = false
+closure_evidence = [
+ "All 49 required package reports pass the unchanged four-metric coverage policy, and the governed aggregate and release preflight pass.",
+ "The full workspace check, test, Clippy and Rustdoc lanes pass, together with catalog, architecture, contracts, DTO freshness, API boundaries, resolved dependency graph and portable checks.",
+ "Runtime-paths and service-SQLite documentation tests pass; generated public API text is byte-identical to each reviewed baseline. Their five changed source files contain test-only additions.",
+ "Cargo tests execute the three complete offline command self-tests. The workspace reports 3816 passing tests and eight ignored entrypoints: six process children exercised by parents and two unchanged opt-in SDK generators outside this lane.",
+]
+
+[[deviation]]
id = "RCRV1-DEV-001"
date = "2026-07-27"
status = "active"
diff --git a/crates/runtime_paths/src/provision.rs b/crates/runtime_paths/src/provision.rs
@@ -675,6 +675,110 @@ mod tests {
}
#[test]
+ fn root_and_owner_validation_rejects_invalid_inputs() {
+ for root in ["relative", "/", "/valid/../other"] {
+ assert_eq!(
+ super::validate_absolute_root(Path::new(root)),
+ Err(StateDirectoryProvisionError::InvalidPlan)
+ );
+ }
+ super::validate_absolute_root(Path::new("/valid/root")).unwrap();
+ let owner = rustix::process::geteuid().as_raw();
+ for (directory, uid) in [(false, owner), (true, owner.wrapping_add(1))] {
+ assert_eq!(
+ super::validate_directory_status(directory, uid, 0o700, true),
+ Err(StateDirectoryProvisionError::UnsafeDirectory)
+ );
+ }
+ }
+
+ #[test]
+ fn retained_directory_checks_reject_a_descriptor_for_another_directory() {
+ let temporary = TempDir::new().unwrap();
+ let root = temporary.path().canonicalize().unwrap();
+ for name in ["expected", "other"] {
+ fs::create_dir(root.join(name)).unwrap();
+ fs::set_permissions(root.join(name), fs::Permissions::from_mode(0o700)).unwrap();
+ }
+ let parent = super::open_absolute_directory(&root).unwrap();
+ let held = super::open_directory_at(&parent, "expected".as_ref()).unwrap();
+ let expected = super::validate_secure_directory(&held, true).unwrap();
+ let mut binding = super::DirectoryBinding {
+ parent,
+ name: "expected".into(),
+ held,
+ identity: expected,
+ exact_owner_mode: true,
+ };
+ super::validate_directory_binding(&binding).unwrap();
+ super::validate_absolute_directory_binding(&root.join("expected"), &binding.held, expected)
+ .unwrap();
+ binding.held = super::open_directory_at(&binding.parent, "other".as_ref()).unwrap();
+ assert!(super::validate_directory_binding(&binding).is_err());
+ assert!(
+ super::validate_absolute_directory_binding(
+ &root.join("expected"),
+ &binding.held,
+ expected
+ )
+ .is_err()
+ );
+ assert!(root.join("expected").is_dir());
+ assert!(root.join("other").is_dir());
+ }
+
+ struct ReplaceAfterCreation {
+ component: usize,
+ original: PathBuf,
+ displaced: PathBuf,
+ }
+
+ impl ProvisionOperations for ReplaceAfterCreation {
+ fn after_create(&self, component_index: usize) -> Result<(), StateDirectoryProvisionError> {
+ if component_index == self.component {
+ fs::rename(&self.original, &self.displaced).unwrap();
+ fs::create_dir(&self.original).unwrap();
+ fs::set_permissions(&self.original, fs::Permissions::from_mode(0o700)).unwrap();
+ }
+ Ok(())
+ }
+ }
+
+ #[test]
+ fn successful_creation_hooks_cannot_hide_root_or_suffix_replacement() {
+ for (component, replace_root) in [(0, true), (2, true), (2, false)] {
+ let temporary = TempDir::new().unwrap();
+ let context = context(
+ RadrootsPlatform::Linux,
+ RadrootsPathProfile::RepoLocal,
+ temporary.path(),
+ );
+ let root = prepare_state_root(&context);
+ let original = if replace_root {
+ root.clone()
+ } else {
+ root.join("services")
+ };
+ let displaced = temporary.path().join("displaced");
+ let operations = ReplaceAfterCreation {
+ component,
+ original: original.clone(),
+ displaced: displaced.clone(),
+ };
+ assert_eq!(
+ provision_with_operations(&context.state_directory_plan().unwrap(), &operations),
+ Err(StateDirectoryProvisionError::Cleanup)
+ );
+ assert!(original.is_dir());
+ assert!(displaced.is_dir());
+ assert_ne!(
+ fs::metadata(&original).unwrap().ino(),
+ fs::metadata(&displaced).unwrap().ino()
+ );
+ }
+ }
+
+ #[test]
fn repo_local_creates_only_the_exact_canonical_suffix() {
let temporary = TempDir::new().expect("temporary root");
let context = context(
diff --git a/crates/service_sqlite/src/backup/verify.rs b/crates/service_sqlite/src/backup/verify.rs
@@ -1526,6 +1526,28 @@ mod tests {
#[cfg(any(target_os = "linux", target_os = "macos"))]
#[test]
fn bounded_metadata_and_foreign_key_integrity_reject() {
+ for statement in [
+ "UPDATE radroots_service_metadata SET singleton = 2",
+ "UPDATE radroots_service_metadata SET state_schema_version = 'invalid'",
+ "UPDATE radroots_service_metadata SET created_at_unix_ms = 'invalid'",
+ ] {
+ let mut fixture = Fixture::new("malformed-metadata");
+ fixture.verify().expect("valid original bundle");
+ let state = fixture.bundle.join(crate::BACKUP_STATE_MEMBER_NAME);
+ let mut connection = open_test_database(&state);
+ futures::executor::block_on(async {
+ sqlx::query(sqlx::AssertSqlSafe(statement))
+ .execute(&mut connection)
+ .await
+ .expect("substituted metadata value");
+ connection.close().await.expect("close fixture");
+ });
+ fixture.refresh_manifest();
+ assert_eq!(
+ fixture.verify().expect_err("invalid metadata").kind(),
+ ServiceSqliteErrorKind::Metadata
+ );
+ }
let mut metadata_fixture = Fixture::new("oversized-metadata");
let state = metadata_fixture
.bundle
diff --git a/crates/service_sqlite/src/initialize.rs b/crates/service_sqlite/src/initialize.rs
@@ -1327,6 +1327,21 @@ mod supported {
assert!(!called.get());
assert!(!expected_paths.state_database().exists());
assert!(!expected_paths.state_lock().exists());
+ let error = initialize_or_existing_database(
+ &expected_paths,
+ &other_metadata,
+ &base_schema_catalog(),
+ |_| {
+ called.set(true);
+ Box::pin(ready(Ok::<(), CallbackFailure>(())))
+ },
+ )
+ .await
+ .expect_err("existing-or-new initialization must reject mismatched identity");
+ assert_eq!(error.kind(), ServiceSqliteErrorKind::Metadata);
+ assert!(!called.get());
+ assert!(!expected_paths.state_database().exists());
+ assert!(!expected_paths.state_lock().exists());
}
#[tokio::test(flavor = "current_thread")]
diff --git a/crates/service_sqlite/src/integrity/catalog.rs b/crates/service_sqlite/src/integrity/catalog.rs
@@ -662,6 +662,38 @@ mod tests {
}
#[test]
+ fn aggregate_schema_sql_accepts_its_exact_limit_and_rejects_one_extra_byte() {
+ fn object(name: &'static str, size: usize) -> SchemaObject {
+ let prefix = format!("CREATE TABLE {name} (value INTEGER) /*");
+ let sql: &'static str = Box::leak(
+ format!("{prefix}{}*/", "x".repeat(size - prefix.len() - 2)).into_boxed_str(),
+ );
+ assert_eq!(sql.len(), size);
+ let digest =
+ SchemaObject::computed_digest(SchemaObjectKind::Table, name, name, sql).unwrap();
+ SchemaObject::new(SchemaObjectKind::Table, name, name, sql, digest).unwrap()
+ }
+ let shared = shared_objects()
+ .iter()
+ .map(|object| object.sql.len())
+ .sum::<usize>();
+ let mut objects = (0..15)
+ .map(|index| {
+ let name: &'static str = Box::leak(format!("table_{index}").into_boxed_str());
+ object(name, MAX_SCHEMA_SQL_UTF8_BYTES)
+ })
+ .collect::<Vec<_>>();
+ let last_size = MAX_SCHEMA_CATALOG_UTF8_BYTES - shared - 15 * MAX_SCHEMA_SQL_UTF8_BYTES;
+ objects.push(object("last_table", last_size));
+ SchemaVersionCatalog::computed_digest(1, objects.iter().cloned()).unwrap();
+ *objects.last_mut().unwrap() = object("last_table", last_size + 1);
+ assert_eq!(
+ SchemaVersionCatalog::computed_digest(1, objects),
+ Err(SchemaCatalogContractError::TooManyObjects)
+ );
+ }
+
+ #[test]
fn exact_object_snapshot_and_catalog_vectors_are_stable() {
let object = table();
assert_eq!(
diff --git a/crates/service_sqlite/src/migration.rs b/crates/service_sqlite/src/migration.rs
@@ -1498,6 +1498,38 @@ mod tests {
use super::*;
#[cfg(any(target_os = "linux", target_os = "macos"))]
+ #[tokio::test(flavor = "current_thread")]
+ async fn schema_version_reads_require_one_integer_metadata_row() {
+ let mut connection = SqliteConnection::connect("sqlite::memory:").await.unwrap();
+ sqlx::query("CREATE TABLE radroots_service_metadata (singleton, state_schema_version)")
+ .execute(&mut connection)
+ .await
+ .unwrap();
+ for statement in [
+ "DELETE FROM radroots_service_metadata",
+ "INSERT INTO radroots_service_metadata VALUES (1, 1), (1, 1)",
+ "DELETE FROM radroots_service_metadata; INSERT INTO radroots_service_metadata VALUES (1, 'invalid')",
+ "DELETE FROM radroots_service_metadata; INSERT INTO radroots_service_metadata VALUES (1, NULL)",
+ ] {
+ sqlx::raw_sql(sqlx::AssertSqlSafe(statement))
+ .execute(&mut connection)
+ .await
+ .unwrap();
+ assert_eq!(
+ read_state_schema_version(&mut connection)
+ .await
+ .unwrap_err()
+ .kind(),
+ ServiceSqliteErrorKind::Migration
+ );
+ }
+ sqlx::raw_sql("DELETE FROM radroots_service_metadata; INSERT INTO radroots_service_metadata VALUES (1, 1)")
+ .execute(&mut connection).await.unwrap();
+ assert_eq!(read_state_schema_version(&mut connection).await.unwrap(), 1);
+ connection.close().await.unwrap();
+ }
+
+ #[cfg(any(target_os = "linux", target_os = "macos"))]
#[test]
fn migration_failure_inventory_is_complete_and_source_aware() {
use std::error::Error as _;
diff --git a/tools/xtask/src/advisory_snapshot.rs b/tools/xtask/src/advisory_snapshot.rs
@@ -2542,198 +2542,227 @@ fn validate_gradle_projections(
require_complete(projection.state)?;
let receipt = process_receipt(receipts, &format!("gradle:{}", authority.id))?;
admitted.revalidate()?;
- if !admitted.matches_projection(projection)
- || projection.workload_id != authority.id
- || projection.raw_graph_byte_length == 0
- || projection.raw_graph_byte_length > MAX_GRADLE_GRAPH_BYTES
- || !valid_hex(&projection.raw_graph_sha256, 64)
- || projection.init_script_sha256 != GRADLE_INIT_SCRIPT_SHA256
- || projection.wrapper_arguments != expected_gradle_arguments(authority)?
- || projection.environment_keys
- != [
- "GRADLE_USER_HOME",
- "HOME",
- "JAVA_HOME",
- "LC_ALL",
- "PATH",
- "TMPDIR",
- "TZ",
- ]
- || !valid_hex(&projection.environment_sha256, 64)
- || projection.exit_code != 0
- || projection.source_revision != harvest_source.revision
- || projection.source_tree != harvest_source.tree
- || projection.input_sha256
- != gradle_candidate_input_digest(request, inventory, harvest_source, projection)?
- || projection.dependency_count != inventory.dependency_count
- || projection.component_count != projection.components.len() as u64
- || projection.edge_count != projection.edges.len() as u64
- || projection.artifact_count != projection.artifacts.len() as u64
- || projection.component_count == 0
- || !valid_hex(&projection.materialized_tree_sha256, 64)
- || !valid_hex(&projection.artifact_source_roots_sha256, 64)
- || !valid_hex(&projection.seed_cache_inventory_sha256, 64)
- || projection.wrapper_distribution_sha256
- != "553c78f50dafcd54d65b9a444649057857469edf836431389695608536d6b746"
- || projection.process_receipt_sha256 != process_receipt_digest(receipt)?
- || projection.canonical_graph_sha256 != gradle_graph_digest(projection)?
- || projection.normalization_receipt_sha256
- != gradle_normalization_receipt_digest(
- &projection.workload_id,
- projection.raw_graph_byte_length,
- &projection.raw_graph_sha256,
- &projection.canonical_graph_sha256,
- &projection.materialized_tree_sha256,
- &projection.artifact_source_roots_sha256,
- )?
- || receipt.program_sha256 != TOOL_PINS[3].executable_sha256
- || receipt.arguments_sha256
- != domain_json_digest(
- b"radroots-advisory-process-arguments-v1\0",
- &projection.wrapper_arguments,
- )?
- || receipt.environment_sha256 != projection.environment_sha256
- || receipt.input_sha256 != gradle_process_input_digest(projection)?
- || receipt.output_sha256 != gradle_process_output_digest(projection)?
- || receipt.path_binding
- != vec![
- ProcessPathBinding {
- argument_index: 5,
- logical_role: "gradle_build_root".to_owned(),
- identity_sha256: gradle_build_root_binding_digest(projection)?,
- },
- ProcessPathBinding {
- argument_index: 7,
- logical_role: "gradle_init_script".to_owned(),
- identity_sha256: projection.init_script_sha256.clone(),
- },
- ProcessPathBinding {
- argument_index: 10,
- logical_role: "gradle_projection_output".to_owned(),
- identity_sha256: projection.raw_graph_sha256.clone(),
- },
- ]
- || receipt.exit_code != projection.exit_code
- {
+ if !admitted.matches_projection(projection) {
return Err(AdvisoryError::new(AdvisoryFailureKind::InventoryMismatch));
}
- let mut component_ids = BTreeSet::new();
- let mut component_by_id = BTreeMap::new();
- let mut previous_component = None::<&str>;
- for component in &projection.components {
- if previous_component
- .is_some_and(|previous| previous >= component.identity_sha256.as_str())
- || component.identity_sha256 != gradle_component_digest(component)?
- || component.variant_sha256 != gradle_variant_digest(&component.variant)?
- || !valid_gradle_variant_envelope(&component.variant)
- || !valid_gradle_component(component)
- || !component_ids.insert(component.identity_sha256.as_str())
- {
- return Err(AdvisoryError::new(AdvisoryFailureKind::InventoryMismatch));
- }
- component_by_id.insert(component.identity_sha256.as_str(), component);
- previous_component = Some(&component.identity_sha256);
+ validate_gradle_projection_bindings(
+ projection,
+ authority,
+ inventory,
+ harvest_source,
+ request,
+ receipt,
+ )?;
+ validate_gradle_graph_structure(projection, authority, &harvest_source.revision)?;
+ admitted.revalidate()?;
+ }
+ Ok(())
+}
+
+fn validate_gradle_projection_bindings(
+ projection: &GradleProjection,
+ authority: &WorkloadAuthority,
+ inventory: &WorkloadInventory,
+ harvest_source: &SourceIdentity,
+ request: &AdmissionRequest,
+ receipt: &TrustedProcessReceipt,
+) -> Result<(), AdvisoryError> {
+ if projection.workload_id != authority.id
+ || projection.raw_graph_byte_length == 0
+ || projection.raw_graph_byte_length > MAX_GRADLE_GRAPH_BYTES
+ || !valid_hex(&projection.raw_graph_sha256, 64)
+ || projection.init_script_sha256 != GRADLE_INIT_SCRIPT_SHA256
+ || projection.wrapper_arguments != expected_gradle_arguments(authority)?
+ || projection.environment_keys
+ != [
+ "GRADLE_USER_HOME",
+ "HOME",
+ "JAVA_HOME",
+ "LC_ALL",
+ "PATH",
+ "TMPDIR",
+ "TZ",
+ ]
+ || !valid_hex(&projection.environment_sha256, 64)
+ || projection.exit_code != 0
+ || projection.source_revision != harvest_source.revision
+ || projection.source_tree != harvest_source.tree
+ || projection.input_sha256
+ != gradle_candidate_input_digest(request, inventory, harvest_source, projection)?
+ || projection.dependency_count != inventory.dependency_count
+ || projection.component_count != projection.components.len() as u64
+ || projection.edge_count != projection.edges.len() as u64
+ || projection.artifact_count != projection.artifacts.len() as u64
+ || projection.component_count == 0
+ || !valid_hex(&projection.materialized_tree_sha256, 64)
+ || !valid_hex(&projection.artifact_source_roots_sha256, 64)
+ || !valid_hex(&projection.seed_cache_inventory_sha256, 64)
+ || projection.wrapper_distribution_sha256
+ != "553c78f50dafcd54d65b9a444649057857469edf836431389695608536d6b746"
+ || projection.process_receipt_sha256 != process_receipt_digest(receipt)?
+ || projection.canonical_graph_sha256 != gradle_graph_digest(projection)?
+ || projection.normalization_receipt_sha256
+ != gradle_normalization_receipt_digest(
+ &projection.workload_id,
+ projection.raw_graph_byte_length,
+ &projection.raw_graph_sha256,
+ &projection.canonical_graph_sha256,
+ &projection.materialized_tree_sha256,
+ &projection.artifact_source_roots_sha256,
+ )?
+ || receipt.program_sha256 != TOOL_PINS[3].executable_sha256
+ || receipt.arguments_sha256
+ != domain_json_digest(
+ b"radroots-advisory-process-arguments-v1\0",
+ &projection.wrapper_arguments,
+ )?
+ || receipt.environment_sha256 != projection.environment_sha256
+ || receipt.input_sha256 != gradle_process_input_digest(projection)?
+ || receipt.output_sha256 != gradle_process_output_digest(projection)?
+ || receipt.path_binding
+ != vec![
+ ProcessPathBinding {
+ argument_index: 5,
+ logical_role: "gradle_build_root".to_owned(),
+ identity_sha256: gradle_build_root_binding_digest(projection)?,
+ },
+ ProcessPathBinding {
+ argument_index: 7,
+ logical_role: "gradle_init_script".to_owned(),
+ identity_sha256: projection.init_script_sha256.clone(),
+ },
+ ProcessPathBinding {
+ argument_index: 10,
+ logical_role: "gradle_projection_output".to_owned(),
+ identity_sha256: projection.raw_graph_sha256.clone(),
+ },
+ ]
+ || receipt.exit_code != projection.exit_code
+ {
+ return Err(AdvisoryError::new(AdvisoryFailureKind::InventoryMismatch));
+ }
+ Ok(())
+}
+
+fn validate_gradle_graph_structure(
+ projection: &GradleProjection,
+ authority: &WorkloadAuthority,
+ source_revision: &str,
+) -> Result<(), AdvisoryError> {
+ let mut component_ids = BTreeSet::new();
+ let mut component_by_id = BTreeMap::new();
+ let mut previous_component = None::<&str>;
+ for component in &projection.components {
+ if previous_component.is_some_and(|previous| previous >= component.identity_sha256.as_str())
+ || component.identity_sha256 != gradle_component_digest(component)?
+ || component.variant_sha256 != gradle_variant_digest(&component.variant)?
+ || !valid_gradle_variant_envelope(&component.variant)
+ || !valid_gradle_component(component)
+ || !component_ids.insert(component.identity_sha256.as_str())
+ {
+ return Err(AdvisoryError::new(AdvisoryFailureKind::InventoryMismatch));
}
- let roots = projection
- .components
- .iter()
- .filter(|component| {
- component.root
- && component.kind == "project"
- && component.build_root.as_deref() == Some(authority.build_root)
- && component.project_path.as_deref() == authority.project_path
- })
- .collect::<Vec<_>>();
- if roots.len() != 1 {
+ component_by_id.insert(component.identity_sha256.as_str(), component);
+ previous_component = Some(&component.identity_sha256);
+ }
+ let roots = projection
+ .components
+ .iter()
+ .filter(|component| {
+ component.root
+ && component.kind == "project"
+ && component.build_root.as_deref() == Some(authority.build_root)
+ && component.project_path.as_deref() == authority.project_path
+ })
+ .collect::<Vec<_>>();
+ if roots.len() != 1 {
+ return Err(AdvisoryError::new(AdvisoryFailureKind::InventoryMismatch));
+ }
+ let mut previous_edge = None::<Vec<u8>>;
+ for edge in &projection.edges {
+ let key = canonical_row_key(edge)?;
+ if previous_edge
+ .as_ref()
+ .is_some_and(|previous| previous >= &key)
+ || !component_ids.contains(edge.from_identity_sha256.as_str())
+ || !component_ids.contains(edge.to_identity_sha256.as_str())
+ || edge.from_identity_sha256 == edge.to_identity_sha256
+ || edge.requested_sha256 != gradle_request_digest(&edge.requested)?
+ || edge.selected_variant_sha256 != gradle_variant_digest(&edge.selected_variant)?
+ || !valid_gradle_request(&edge.requested)
+ || !valid_gradle_variant(&edge.selected_variant)
+ {
return Err(AdvisoryError::new(AdvisoryFailureKind::InventoryMismatch));
}
- let mut previous_edge = None::<Vec<u8>>;
+ previous_edge = Some(key);
+ }
+ let mut reachable = BTreeSet::from([roots[0].identity_sha256.as_str()]);
+ loop {
+ let before = reachable.len();
for edge in &projection.edges {
- let key = canonical_row_key(edge)?;
- if previous_edge
- .as_ref()
- .is_some_and(|previous| previous >= &key)
- || !component_ids.contains(edge.from_identity_sha256.as_str())
- || !component_ids.contains(edge.to_identity_sha256.as_str())
- || edge.from_identity_sha256 == edge.to_identity_sha256
- || edge.requested_sha256 != gradle_request_digest(&edge.requested)?
- || edge.selected_variant_sha256 != gradle_variant_digest(&edge.selected_variant)?
- || !valid_gradle_request(&edge.requested)
- || !valid_gradle_variant(&edge.selected_variant)
- {
- return Err(AdvisoryError::new(AdvisoryFailureKind::InventoryMismatch));
- }
- previous_edge = Some(key);
- }
- let mut reachable = BTreeSet::from([roots[0].identity_sha256.as_str()]);
- loop {
- let before = reachable.len();
- for edge in &projection.edges {
- if reachable.contains(edge.from_identity_sha256.as_str()) {
- reachable.insert(edge.to_identity_sha256.as_str());
- }
- }
- if reachable.len() == before {
- break;
+ if reachable.contains(edge.from_identity_sha256.as_str()) {
+ reachable.insert(edge.to_identity_sha256.as_str());
}
}
- if reachable.len() != projection.components.len() {
+ if reachable.len() == before {
+ break;
+ }
+ }
+ if reachable.len() != projection.components.len() {
+ return Err(AdvisoryError::new(AdvisoryFailureKind::InventoryMismatch));
+ }
+ let mut previous = None::<Vec<u8>>;
+ let mut materialized = BTreeMap::<(&str, &str), (&str, u64)>::new();
+ for artifact in &projection.artifacts {
+ let key = canonical_row_key(artifact)?;
+ if previous.as_ref().is_some_and(|prior| prior >= &key)
+ || !component_ids.contains(artifact.component_identity_sha256.as_str())
+ || !valid_bounded_text(&artifact.component, 512)
+ || artifact.package_ecosystem != "maven"
+ || !valid_bounded_text(&artifact.package_namespace, 256)
+ || !valid_bounded_text(&artifact.package_name, 256)
+ || !valid_bounded_text(&artifact.package_version, 128)
+ || !valid_hex(&artifact.artifact_sha256, 64)
+ || artifact.variant_sha256 != gradle_variant_digest(&artifact.variant)?
+ || !valid_gradle_variant(&artifact.variant)
+ || artifact.byte_length == 0
+ || !valid_bounded_text(&artifact.artifact_name, 256)
+ || !valid_bounded_text(&artifact.logical_name, 256)
+ || !valid_bounded_text(&artifact.artifact_type, 64)
+ || artifact
+ .classifier
+ .as_deref()
+ .is_some_and(|classifier| !valid_bounded_text(classifier, 128))
+ || !valid_artifact_extension(&artifact.extension)
+ || artifact.materialized_name
+ != format!("{}.{}", artifact.artifact_sha256, artifact.extension)
+ {
return Err(AdvisoryError::new(AdvisoryFailureKind::InventoryMismatch));
}
- let mut previous = None::<Vec<u8>>;
- let mut materialized = BTreeMap::<(&str, &str), (&str, u64)>::new();
- for artifact in &projection.artifacts {
- let key = canonical_row_key(artifact)?;
- if previous.as_ref().is_some_and(|prior| prior >= &key)
- || !component_ids.contains(artifact.component_identity_sha256.as_str())
- || !valid_bounded_text(&artifact.component, 512)
- || artifact.package_ecosystem != "maven"
- || !valid_bounded_text(&artifact.package_namespace, 256)
- || !valid_bounded_text(&artifact.package_name, 256)
- || !valid_bounded_text(&artifact.package_version, 128)
- || !valid_hex(&artifact.artifact_sha256, 64)
- || artifact.variant_sha256 != gradle_variant_digest(&artifact.variant)?
- || !valid_gradle_variant(&artifact.variant)
- || artifact.byte_length == 0
- || !valid_bounded_text(&artifact.artifact_name, 256)
- || !valid_bounded_text(&artifact.logical_name, 256)
- || !valid_bounded_text(&artifact.artifact_type, 64)
- || artifact
- .classifier
- .as_deref()
- .is_some_and(|classifier| !valid_bounded_text(classifier, 128))
- || !valid_artifact_extension(&artifact.extension)
- || artifact.materialized_name
- != format!("{}.{}", artifact.artifact_sha256, artifact.extension)
- {
- return Err(AdvisoryError::new(AdvisoryFailureKind::InventoryMismatch));
- }
- let component = component_by_id
- .get(artifact.component_identity_sha256.as_str())
- .copied()
- .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InventoryMismatch))?;
- if !artifact_matches_component(artifact, component, &harvest_source.revision)
- || !projection.edges.iter().any(|edge| {
- edge.to_identity_sha256 == artifact.component_identity_sha256
- && edge.selected_variant_sha256 == artifact.variant_sha256
- })
- {
- return Err(AdvisoryError::new(AdvisoryFailureKind::InventoryMismatch));
- }
- let materialized_key = (
- artifact.artifact_sha256.as_str(),
- artifact.extension.as_str(),
- );
- if let Some((name, length)) = materialized.insert(
- materialized_key,
- (artifact.materialized_name.as_str(), artifact.byte_length),
- ) && (name != artifact.materialized_name || length != artifact.byte_length)
- {
- return Err(AdvisoryError::new(AdvisoryFailureKind::InventoryMismatch));
- }
- previous = Some(key);
+ let component = component_by_id
+ .get(artifact.component_identity_sha256.as_str())
+ .copied()
+ .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InventoryMismatch))?;
+ if !artifact_matches_component(artifact, component, source_revision)
+ || !projection.edges.iter().any(|edge| {
+ edge.to_identity_sha256 == artifact.component_identity_sha256
+ && edge.selected_variant_sha256 == artifact.variant_sha256
+ })
+ {
+ return Err(AdvisoryError::new(AdvisoryFailureKind::InventoryMismatch));
}
- admitted.revalidate()?;
+ let materialized_key = (
+ artifact.artifact_sha256.as_str(),
+ artifact.extension.as_str(),
+ );
+ if let Some((name, length)) = materialized.insert(
+ materialized_key,
+ (artifact.materialized_name.as_str(), artifact.byte_length),
+ ) && (name != artifact.materialized_name || length != artifact.byte_length)
+ {
+ return Err(AdvisoryError::new(AdvisoryFailureKind::InventoryMismatch));
+ }
+ previous = Some(key);
}
Ok(())
}
@@ -4112,8 +4141,9 @@ fn validate_rustsec_affected(value: &Value) -> Result<(), AdvisoryError> {
let object = value
.as_object()
.ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InvalidReport))?;
- let functions = object["functions"]
- .as_object()
+ let functions = object
+ .get("functions")
+ .and_then(Value::as_object)
.filter(|rows| rows.len() <= 65_536)
.ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InvalidReport))?;
if !object_keys_exact(object, &["arch", "functions", "os"], &[])
@@ -7554,3 +7584,2675 @@ mod step_296_tests {
validate_decision(root).expect("checked-in advisory decision must be accepted");
}
}
+
+#[cfg(test)]
+mod admission_boundary_tests {
+ use super::*;
+
+ #[test]
+ fn snapshot_inventory_rejects_missing_nonfile_and_unrecognized_members() {
+ for case in ["missing", "directory", "unknown"] {
+ let directory = trusted_tempdir("radroots-snapshot-inventory-").unwrap();
+ for name in SNAPSHOT_FILE_NAMES {
+ std::fs::write(directory.path().join(name), b"{}").unwrap();
+ }
+ let limits = TraversalLimits {
+ max_entries: 16,
+ max_files: 16,
+ max_total_bytes: 1024,
+ max_file_bytes: 1024,
+ max_depth: 2,
+ max_path_bytes: 128,
+ };
+ let original =
+ safe_artifact_io::traverse_regular_files(directory.path(), limits, &[]).unwrap();
+ exact_snapshot_files(&original).unwrap();
+ let member = directory.path().join(MANIFEST_NAME);
+ match case {
+ "missing" => std::fs::remove_file(member).unwrap(),
+ "directory" => {
+ std::fs::remove_file(&member).unwrap();
+ std::fs::create_dir(member).unwrap();
+ }
+ "unknown" => {
+ std::fs::rename(member, directory.path().join("unknown.json")).unwrap()
+ }
+ _ => unreachable!(),
+ }
+ let changed =
+ safe_artifact_io::traverse_regular_files(directory.path(), limits, &[]).unwrap();
+ assert!(exact_snapshot_files(&changed).is_err(), "{case}");
+ }
+ let mut binding = blob(RUSTSEC_REPORT_NAME, b"{}").unwrap();
+ assert!(valid_blob_binding(&binding));
+ binding.path = "other.json".into();
+ assert!(!valid_blob_binding(&binding));
+ }
+
+ #[test]
+ fn reports_bind_one_database_across_all_workloads_and_the_exact_network_trace() {
+ let fixture = SyntheticFixture::new().unwrap();
+ for provider in &fixture.manifest.provider_snapshot {
+ let original = fixture.read_report_value(provider.provider).unwrap();
+ let check = |value: &Value| {
+ database_identity_from_report(
+ provider.provider,
+ &canonical_json(value),
+ &provider.materialized_tree_sha256,
+ )
+ };
+ assert_eq!(check(&original).unwrap(), provider.database_identity_sha256);
+ let mut empty = original.clone();
+ empty["workload_result"] = json!([]);
+ assert!(check(&empty).is_err());
+ let mut wrong_provider = original.clone();
+ wrong_provider["provider"] = json!(if provider.provider == ProviderId::Rustsec {
+ "owasp_nvd"
+ } else {
+ "rustsec"
+ });
+ assert!(check(&wrong_provider).is_err());
+ let mut changed = original.clone();
+ let mut second = original["workload_result"][0].clone();
+ let mut raw: Value =
+ serde_json::from_str(second["raw_scanner_output"].as_str().unwrap()).unwrap();
+ match provider.provider {
+ ProviderId::Rustsec => raw["database"]["last-commit"] = json!("f".repeat(40)),
+ ProviderId::OwaspNvd => {
+ raw["scanInfo"]["dataSource"][0]["name"] = json!("NVD CVE substituted")
+ }
+ }
+ second["raw_scanner_output"] = json!(String::from_utf8(canonical_json(&raw)).unwrap());
+ changed["workload_result"]
+ .as_array_mut()
+ .unwrap()
+ .push(second);
+ assert_eq!(
+ check(&changed).unwrap_err().kind(),
+ AdvisoryFailureKind::BindingChanged
+ );
+ }
+ let provider = &fixture.manifest.provider_snapshot[1];
+ let trace: NvdNetworkTrace =
+ parse_canonical_authority(&fixture.request.nvd_network_trace).unwrap();
+ validate_provider(
+ provider,
+ &fixture.request,
+ &trace,
+ &fixture.process_receipts,
+ )
+ .unwrap();
+ let mut changed = provider.clone();
+ changed.network_trace_sha256 = "f".repeat(64);
+ assert_eq!(
+ validate_provider(
+ &changed,
+ &fixture.request,
+ &trace,
+ &fixture.process_receipts
+ )
+ .unwrap_err()
+ .kind(),
+ AdvisoryFailureKind::BindingChanged
+ );
+ let mut changed_trace = trace;
+ changed_trace.producer_request_sha256 = "f".repeat(64);
+ assert_eq!(
+ validate_provider(
+ provider,
+ &fixture.request,
+ &changed_trace,
+ &fixture.process_receipts
+ )
+ .unwrap_err()
+ .kind(),
+ AdvisoryFailureKind::BindingChanged
+ );
+ }
+
+ #[test]
+ fn related_artifact_digests_and_exact_package_tokens_are_bound() {
+ let fixture = SyntheticFixture::new().unwrap();
+ let artifact = &fixture.manifest.gradle_projection[0].artifacts[0];
+ let expected = BTreeMap::from([(artifact.materialized_name.as_str(), vec![artifact])]);
+ let original = json!([{"fileName":artifact.materialized_name,"filePath":format!("/fixture/{}",artifact.materialized_name),"isVirtual":false,
+ "md5":"1".repeat(32),"sha1":"2".repeat(40),"sha256":artifact.artifact_sha256}]);
+ parse_owasp_related(Some(&original), &expected).unwrap();
+ let mut changed = original.clone();
+ changed[0]["sha256"] = json!("f".repeat(64));
+ assert!(parse_owasp_related(Some(&changed), &expected).is_err());
+ let id = format!(
+ "pkg:maven/{}/{}@{}",
+ artifact.package_namespace, artifact.package_name, artifact.package_version
+ );
+ assert!(package_identifier_compatible(&id, artifact));
+ let mut changed = artifact.clone();
+ changed.package_ecosystem = "cargo".into();
+ assert!(!package_identifier_compatible(&id, &changed));
+ changed = artifact.clone();
+ changed.package_namespace = "has space".into();
+ assert!(!package_identifier_compatible(&id, &changed));
+ let package = json!({"name":"example","version":"1.0.0","checksum":null,"replace":null,"source":null});
+ validate_rustsec_package(&package).unwrap();
+ for field in ["name", "version"] {
+ let mut changed = package.clone();
+ changed[field] = json!("has space");
+ assert!(validate_rustsec_package(&changed).is_err());
+ }
+ assert!(!valid_rustsec_dependency(
+ &json!({"name":"example","version":"1.0.0","source":null,"unexpected":true})
+ ));
+ assert!(!valid_text_allow_empty("too long", 3));
+ }
+
+ #[test]
+ fn artifact_source_roots_and_project_versions_have_closed_identity() {
+ let digest = "1".repeat(64);
+ let make = |role| GradleArtifactSourceRoot {
+ path: Path::new("/fixture"),
+ logical_role: role,
+ identity_sha256: &digest,
+ };
+ artifact_source_roots_digest(&[
+ make("candidate_build_output"),
+ make("governed_seed_cache"),
+ ])
+ .unwrap();
+ for roots in [
+ vec![],
+ vec![
+ make("candidate_build_output"),
+ make("governed_seed_cache"),
+ make("candidate_build_output"),
+ ],
+ vec![make("unknown")],
+ vec![
+ make("candidate_build_output"),
+ make("candidate_build_output"),
+ ],
+ vec![GradleArtifactSourceRoot {
+ path: Path::new("relative"),
+ ..make("candidate_build_output")
+ }],
+ vec![GradleArtifactSourceRoot {
+ identity_sha256: "invalid",
+ ..make("candidate_build_output")
+ }],
+ ] {
+ assert!(artifact_source_roots_digest(&roots).is_err());
+ }
+ let version = RawGradleModuleVersion {
+ group: "harvestcircle.app".into(),
+ name: "shared".into(),
+ version: "unspecified".into(),
+ };
+ assert!(valid_project_module_version(
+ ".",
+ ":app:shared",
+ &version,
+ &"1".repeat(40)
+ ));
+ assert!(!valid_project_module_version(
+ ".",
+ ":app:shared",
+ &version,
+ "invalid"
+ ));
+ assert!(!valid_project_module_version(
+ ".",
+ ":app:other",
+ &version,
+ &"1".repeat(40)
+ ));
+ assert!(!valid_gradle_project_path(":app:"));
+ for value in ["CVE-2026", "CVE-26-1234", "CVE-2026-123"] {
+ assert!(!valid_advisory_id(ProviderId::OwaspNvd, value));
+ }
+ for value in ["a\0b", "a\nb"] {
+ assert!(!valid_text_allow_empty(value, 128));
+ }
+ assert!(parse_report_epoch("2026-04-01T00:00:00Z").is_ok());
+ assert_eq!(
+ format_report_epoch(parse_report_epoch("2026-01-01T00:00:00Z").unwrap()).unwrap(),
+ "2026-01-01T00:00:00Z"
+ );
+ assert!(parse_exact_package_url("pkg:maven/example/bad name@1.0").is_err());
+ assert!(!valid_owasp_software_identifier(""));
+ }
+
+ #[test]
+ fn process_environment_is_private_per_attempt_and_shared_tools_are_consistent() {
+ let id = "gradle:harvestcircle.android.app.debugRuntimeClasspath";
+ let fixture = SyntheticFixture::new().unwrap();
+ let original = &fixture
+ .process_receipts
+ .iter()
+ .find(|row| row.id.starts_with("gradle:"))
+ .unwrap()
+ .environment;
+ let validate = |environment: &[ProcessEnvironmentRow]| {
+ validate_process_environment(
+ id,
+ environment,
+ &mut BTreeSet::new(),
+ &mut BTreeMap::new(),
+ )
+ };
+ validate(original).unwrap();
+ assert!(validate(&original[..original.len() - 1]).is_err());
+ for name in ["LC_ALL", "TZ"] {
+ let mut changed = original.clone();
+ changed
+ .iter_mut()
+ .find(|row| row.name == name)
+ .unwrap()
+ .value_sha256 = "f".repeat(64);
+ assert!(validate(&changed).is_err(), "{name}");
+ }
+ let mut changed = original.clone();
+ let home = changed
+ .iter()
+ .find(|row| row.name == "HOME")
+ .unwrap()
+ .value_sha256
+ .clone();
+ changed
+ .iter_mut()
+ .find(|row| row.name == "TMPDIR")
+ .unwrap()
+ .value_sha256 = home;
+ assert!(validate(&changed).is_err());
+ let mut shared = BTreeMap::from([("PATH".into(), "f".repeat(64))]);
+ assert!(
+ validate_process_environment(id, original, &mut BTreeSet::new(), &mut shared).is_err()
+ );
+ let mut receipts = fixture.process_receipts.clone();
+ receipts[1].working_directory.identity_sha256 =
+ receipts[0].working_directory.identity_sha256.clone();
+ receipts[1].working_directory_sha256 =
+ process_working_directory_digest(&receipts[1].working_directory).unwrap();
+ assert!(validate_process_receipts(&receipts, fixture.request.evaluation_epoch).is_err());
+ let mut receipts = fixture.process_receipts.clone();
+ receipts[0].working_directory.identity_sha256 = receipts[0]
+ .environment
+ .iter()
+ .find(|row| row.name == "HOME")
+ .unwrap()
+ .value_sha256
+ .clone();
+ receipts[0].working_directory_sha256 =
+ process_working_directory_digest(&receipts[0].working_directory).unwrap();
+ assert!(validate_process_receipts(&receipts, fixture.request.evaluation_epoch).is_err());
+ }
+
+ #[test]
+ fn gradle_projection_bindings_reject_independent_metadata_and_receipt_substitution() {
+ let fixture = SyntheticFixture::new().unwrap();
+ let original = &fixture.manifest.gradle_projection[0];
+ let authority = WORKLOADS
+ .iter()
+ .find(|row| row.id == original.workload_id)
+ .unwrap();
+ let inventory = fixture
+ .request
+ .inventory
+ .iter()
+ .find(|row| row.id == original.workload_id)
+ .unwrap();
+ let source = fixture
+ .request
+ .sources
+ .iter()
+ .find(|row| row.repository == "oss/harvestcircle")
+ .unwrap();
+ let receipt = process_receipt(
+ &fixture.process_receipts,
+ &format!("gradle:{}", authority.id),
+ )
+ .unwrap();
+ let validate = |projection: &GradleProjection, receipt: &TrustedProcessReceipt| {
+ validate_gradle_projection_bindings(
+ projection,
+ authority,
+ inventory,
+ source,
+ &fixture.request,
+ receipt,
+ )
+ };
+ validate(original, receipt).unwrap();
+ for (pointer, replacement) in [
+ ("/workload_id", json!("unknown")),
+ ("/raw_graph_byte_length", json!(0)),
+ ("/raw_graph_byte_length", json!(MAX_GRADLE_GRAPH_BYTES + 1)),
+ ("/raw_graph_sha256", json!("invalid")),
+ ("/init_script_sha256", json!("f".repeat(64))),
+ ("/wrapper_arguments", json!([])),
+ ("/environment_keys", json!([])),
+ ("/environment_sha256", json!("invalid")),
+ ("/exit_code", json!(1)),
+ ("/source_revision", json!("f".repeat(40))),
+ ("/source_tree", json!("f".repeat(40))),
+ ("/input_sha256", json!("f".repeat(64))),
+ ("/dependency_count", json!(0)),
+ ("/component_count", json!(0)),
+ ("/edge_count", json!(0)),
+ ("/artifact_count", json!(0)),
+ ("/materialized_tree_sha256", json!("invalid")),
+ ("/artifact_source_roots_sha256", json!("invalid")),
+ ("/seed_cache_inventory_sha256", json!("invalid")),
+ ("/wrapper_distribution_sha256", json!("f".repeat(64))),
+ ("/process_receipt_sha256", json!("f".repeat(64))),
+ ("/canonical_graph_sha256", json!("f".repeat(64))),
+ ("/normalization_receipt_sha256", json!("f".repeat(64))),
+ ] {
+ let mut value = serde_json::to_value(original).unwrap();
+ assert_ne!(value.pointer(pointer).unwrap(), &replacement);
+ *value.pointer_mut(pointer).unwrap() = replacement;
+ let mut projection: GradleProjection = serde_json::from_value(value).unwrap();
+ if [
+ "/artifact_source_roots_sha256",
+ "/seed_cache_inventory_sha256",
+ "/wrapper_distribution_sha256",
+ ]
+ .contains(&pointer)
+ {
+ projection.input_sha256 =
+ gradle_candidate_input_digest(&fixture.request, inventory, source, &projection)
+ .unwrap();
+ }
+ assert!(validate(&projection, receipt).is_err(), "{pointer}");
+ }
+ let mut empty = original.clone();
+ empty.components.clear();
+ empty.component_count = 0;
+ assert!(validate(&empty, receipt).is_err());
+ for (field, value) in [
+ ("program_sha256", json!("f".repeat(64))),
+ ("arguments_sha256", json!("f".repeat(64))),
+ ("environment_sha256", json!("f".repeat(64))),
+ ("input_sha256", json!("f".repeat(64))),
+ ("output_sha256", json!("f".repeat(64))),
+ ("path_binding", json!([])),
+ ("exit_code", json!(1)),
+ ] {
+ let mut changed = serde_json::to_value(receipt).unwrap();
+ changed[field] = value;
+ let changed: TrustedProcessReceipt = serde_json::from_value(changed).unwrap();
+ let mut projection = original.clone();
+ projection.process_receipt_sha256 = process_receipt_digest(&changed).unwrap();
+ assert!(validate(&projection, &changed).is_err(), "{field}");
+ }
+ }
+
+ #[test]
+ fn raw_scanner_files_and_archive_bindings_reject_changed_bytes_and_counters() {
+ let directory = trusted_tempdir("radroots-scanner-boundary-").unwrap();
+ let path = directory.path().join(RAW_SCANNER_OUTPUT_NAME);
+ for bytes in [b"".as_slice(), b"invalid"] {
+ std::fs::write(&path, bytes).unwrap();
+ assert!(
+ admit_raw_scanner_output(directory.path(), ProviderId::Rustsec, "fixture").is_err()
+ );
+ }
+ std::fs::write(&path, b"{}").unwrap();
+ let admitted =
+ admit_raw_scanner_output(directory.path(), ProviderId::Rustsec, "fixture").unwrap();
+ admitted.revalidate().unwrap();
+ std::fs::write(&path, b"{\"changed\":true}").unwrap();
+ assert!(admitted.revalidate().is_err());
+ let binding = blob(RUSTSEC_REPORT_NAME, b"{}").unwrap();
+ validate_blob(
+ &binding,
+ &FileEvidence {
+ byte_length: 2,
+ sha256: sha256(b"{}"),
+ },
+ )
+ .unwrap();
+ for evidence in [
+ FileEvidence {
+ byte_length: 3,
+ sha256: sha256(b"{}"),
+ },
+ FileEvidence {
+ byte_length: 2,
+ sha256: sha256(b"[]"),
+ },
+ ] {
+ assert!(validate_blob(&binding, &evidence).is_err());
+ }
+ for field in [
+ "archive_expanded_bytes",
+ "archive_member_count",
+ "archive_payload_bytes",
+ "materialized_tree_sha256",
+ ] {
+ let mut fixture = SyntheticFixture::new().unwrap();
+ let mut value = serde_json::to_value(&fixture.manifest.provider_snapshot[0]).unwrap();
+ value[field] = if field == "materialized_tree_sha256" {
+ json!("f".repeat(64))
+ } else {
+ json!(value[field].as_u64().unwrap() + 1)
+ };
+ fixture.manifest.provider_snapshot[0] = serde_json::from_value(value).unwrap();
+ fixture.seal().unwrap();
+ assert_eq!(
+ admit_snapshot(
+ fixture.root(),
+ fixture.materialization_parent.path(),
+ &fixture.request
+ )
+ .unwrap_err()
+ .kind(),
+ AdvisoryFailureKind::BindingChanged,
+ "{field}"
+ );
+ }
+ let fixture = SyntheticFixture::new().unwrap();
+ let path = fixture.root().join(MANIFEST_NAME);
+ let mut bytes = std::fs::read(&path).unwrap();
+ bytes.push(b' ');
+ std::fs::write(path, bytes).unwrap();
+ assert_eq!(
+ admit_snapshot(
+ fixture.root(),
+ fixture.materialization_parent.path(),
+ &fixture.request
+ )
+ .unwrap_err()
+ .kind(),
+ AdvisoryFailureKind::InvalidSnapshot
+ );
+ }
+
+ #[test]
+ fn normalized_graph_structure_rejects_each_independent_link_and_artifact_fault() {
+ let fixture = SyntheticFixture::new().unwrap();
+ let original = &fixture.manifest.gradle_projection[0];
+ let authority = WORKLOADS
+ .iter()
+ .find(|row| row.id == original.workload_id)
+ .unwrap();
+ let revision = "1".repeat(40);
+ let validate = |projection: &GradleProjection| {
+ validate_gradle_graph_structure(projection, authority, &revision).is_ok()
+ };
+ rejects_typed_changes(
+ original,
+ &[
+ ("/components/0/identity_sha256", json!("invalid")),
+ ("/components/0/variant_sha256", json!("invalid")),
+ ("/edges/0/from_identity_sha256", json!("invalid")),
+ ("/edges/0/to_identity_sha256", json!("invalid")),
+ ("/edges/0/requested_sha256", json!("invalid")),
+ ("/edges/0/selected_variant_sha256", json!("invalid")),
+ ("/edges", json!([])),
+ ("/artifacts/0/component_identity_sha256", json!("invalid")),
+ ("/artifacts/0/component", json!("")),
+ ("/artifacts/0/package_ecosystem", json!("cargo")),
+ ("/artifacts/0/package_namespace", json!("")),
+ ("/artifacts/0/package_name", json!("")),
+ ("/artifacts/0/package_version", json!("")),
+ ("/artifacts/0/artifact_sha256", json!("invalid")),
+ ("/artifacts/0/variant_sha256", json!("invalid")),
+ ("/artifacts/0/byte_length", json!(0)),
+ ("/artifacts/0/artifact_name", json!("")),
+ ("/artifacts/0/logical_name", json!("")),
+ ("/artifacts/0/artifact_type", json!("")),
+ ("/artifacts/0/classifier", json!("")),
+ ("/artifacts/0/extension", json!("unknown")),
+ ("/artifacts/0/materialized_name", json!("invalid")),
+ ("/artifacts/0/package_name", json!("wrong-binding")),
+ ],
+ validate,
+ );
+ for collection in ["components", "edges", "artifacts"] {
+ let mut changed = serde_json::to_value(original).unwrap();
+ let duplicate = changed[collection][0].clone();
+ changed[collection].as_array_mut().unwrap().push(duplicate);
+ assert!(
+ !validate(&serde_json::from_value(changed).unwrap()),
+ "duplicate {collection}"
+ );
+ }
+ let mut changed = original.clone();
+ changed.edges[0].to_identity_sha256 = changed.edges[0].from_identity_sha256.clone();
+ assert!(!validate(&changed));
+ let mut changed = original.clone();
+ // Reversing the sole edge leaves the non-root component unreachable.
+ changed.edges[0].from_identity_sha256 = original.edges[0].to_identity_sha256.clone();
+ changed.edges[0].to_identity_sha256 = original.edges[0].from_identity_sha256.clone();
+ assert!(!validate(&changed));
+ for case in 0..6 {
+ let mut changed = original.clone();
+ match case {
+ 0 => {
+ changed.components[0].variant = json!({"selected": []});
+ changed.components[0].variant_sha256 =
+ gradle_variant_digest(&changed.components[0].variant).unwrap();
+ changed.components[0].identity_sha256 =
+ gradle_component_digest(&changed.components[0]).unwrap();
+ changed
+ .components
+ .sort_by(|a, b| a.identity_sha256.cmp(&b.identity_sha256));
+ }
+ 1 => {
+ changed.edges[0].requested = json!({});
+ changed.edges[0].requested_sha256 =
+ gradle_request_digest(&changed.edges[0].requested).unwrap();
+ }
+ 2 => {
+ changed.edges[0].selected_variant = json!({});
+ changed.edges[0].selected_variant_sha256 =
+ gradle_variant_digest(&changed.edges[0].selected_variant).unwrap();
+ }
+ 3 => {
+ changed.artifacts[0].variant = json!({});
+ changed.artifacts[0].variant_sha256 =
+ gradle_variant_digest(&changed.artifacts[0].variant).unwrap();
+ }
+ 4 => {
+ changed.artifacts[0].variant["attributes"][0]["value"] =
+ json!("different-selection");
+ changed.artifacts[0].variant_sha256 =
+ gradle_variant_digest(&changed.artifacts[0].variant).unwrap();
+ }
+ _ => {
+ for component in &mut changed.components {
+ component.root = false;
+ component.identity_sha256 = gradle_component_digest(component).unwrap();
+ }
+ changed
+ .components
+ .sort_by(|a, b| a.identity_sha256.cmp(&b.identity_sha256));
+ }
+ }
+ assert!(!validate(&changed), "graph structure case {case}");
+ }
+ let mut duplicates = original.clone();
+ let mut second = duplicates.artifacts[0].clone();
+ second.classifier = Some("sources".to_owned());
+ duplicates.artifacts.push(second);
+ duplicates
+ .artifacts
+ .sort_by_key(|row| canonical_row_key(row).unwrap());
+ assert!(
+ validate(&duplicates),
+ "same materialized bytes can support distinct artifacts"
+ );
+ duplicates.artifacts[1].byte_length += 1;
+ duplicates
+ .artifacts
+ .sort_by_key(|row| canonical_row_key(row).unwrap());
+ assert!(
+ !validate(&duplicates),
+ "one content digest cannot bind different lengths"
+ );
+ }
+
+ #[test]
+ fn process_directories_bind_identity_kind_and_before_after_state() {
+ let fixture = SyntheticFixture::new().unwrap();
+ for prefix in [
+ "cargo_audit:",
+ "gradle:",
+ "owasp_analysis:",
+ "owasp_nvd_update",
+ ] {
+ let receipt = fixture
+ .process_receipts
+ .iter()
+ .find(|row| row.id.starts_with(prefix))
+ .unwrap();
+ let directory = &receipt.working_directory;
+ let mut changes = vec![
+ ("/logical_uri", json!("unknown")),
+ ("/kind", json!("unknown")),
+ ("/identity_sha256", json!("invalid")),
+ ("/pre_execution_tree_sha256", json!("invalid")),
+ ("/post_execution_tree_sha256", json!("invalid")),
+ (
+ "/pre_execution_entry_count",
+ json!(if directory.pre_execution_entry_count == 0 {
+ 1
+ } else {
+ 0
+ }),
+ ),
+ (
+ "/post_execution_entry_count",
+ json!(if directory.post_execution_entry_count == 0 {
+ 1
+ } else {
+ 0
+ }),
+ ),
+ ("/pre_execution_tree_sha256", json!("f".repeat(64))),
+ ];
+ if prefix != "owasp_nvd_update" {
+ changes.push(("/post_execution_tree_sha256", json!("f".repeat(64))));
+ }
+ rejects_typed_changes(directory, &changes, |changed| {
+ validate_process_working_directory(&receipt.id, changed).is_ok()
+ });
+ }
+ assert!(expected_process_working_directory("unknown").is_err());
+ assert!(expected_environment_logical_value("UNKNOWN").is_err());
+ }
+
+ #[test]
+ fn report_timestamps_and_json_bounds_reject_malformed_or_oversized_values() {
+ assert!(parse_report_epoch("1970-01-01T00:00:00Z").is_err());
+ assert_eq!(parse_report_epoch("1970-01-01T00:00:01.125Z").unwrap(), 1);
+ for year in [2000, 2024] {
+ let timestamp = format!("{year}-02-29T12:30:45Z");
+ let epoch = parse_report_epoch(×tamp).unwrap();
+ assert_eq!(format_report_epoch(epoch).unwrap(), timestamp);
+ }
+ for value in [
+ "1970-01-00T00:00:00Z",
+ "1969-01-01T00:00:00Z",
+ "2100-02-29T00:00:00Z",
+ "2023-02-29T00:00:00Z",
+ "2026-04-31T00:00:00Z",
+ "2026-13-01T00:00:00Z",
+ "2026-01-01T24:00:00Z",
+ "2026-01-01T00:60:00Z",
+ "2026-01-01T00:00:60Z",
+ "2026-01-01T00:00:00.xZ",
+ "2026-01-01T00:00:00,1Z",
+ ] {
+ assert!(parse_report_epoch(value).is_err(), "{value}");
+ }
+ let original = b"2026-03-01T00:00:00Z";
+ for index in [4, 7, 10, 13, 16, 19, 0, 5, 8, 11, 14, 17] {
+ let mut bytes = original.to_vec();
+ bytes[index] = b'x';
+ assert!(parse_report_epoch(std::str::from_utf8(&bytes).unwrap()).is_err());
+ }
+ validate_json_bounds(&json!({"rows": [null, true, 1, "safe"]})).unwrap();
+ validate_json_bounds(&json!([])).unwrap();
+ for value in [json!("x".repeat(1_048_577)), json!("a\nb")] {
+ assert!(validate_json_bounds(&value).is_err());
+ }
+ let mut deep = Value::Null;
+ for _ in 0..33 {
+ deep = Value::Array(vec![deep]);
+ }
+ assert!(validate_json_bounds(&deep).is_err());
+ assert!(validate_json_bounds(&Value::Array(vec![Value::Null; 65_537])).is_err());
+ let object = (0..1_025)
+ .map(|index| (format!("key{index}"), Value::Null))
+ .collect();
+ assert!(validate_json_bounds(&Value::Object(object)).is_err());
+ assert!(validate_json_bounds(&json!({"bad\nkey": null})).is_err());
+ let million = Value::Array(vec![Value::Array(vec![Value::Null; 65_536]); 16]);
+ assert!(validate_json_bounds(&million).is_err());
+ for id in ["CVE-2026", "CVE-26-0001", "CVE-2026-1"] {
+ assert!(!valid_advisory_id(ProviderId::OwaspNvd, id));
+ }
+ }
+
+ #[test]
+ fn workload_reports_bind_raw_bytes_counts_database_and_process_receipts() {
+ let fixture = SyntheticFixture::new().unwrap();
+ for provider in &fixture.manifest.provider_snapshot {
+ let original = fixture.read_report_value(provider.provider).unwrap();
+ let envelope: ReportEnvelope = serde_json::from_value(original.clone()).unwrap();
+ let result = &envelope.workload_result[0];
+ let inventory = fixture
+ .request
+ .inventory
+ .iter()
+ .find(|row| row.id == result.workload_id)
+ .unwrap();
+ let mut changes = vec![
+ ("/raw_scanner_output", json!("")),
+ (
+ "/raw_scanner_output",
+ json!("x".repeat(MAX_RAW_WORKLOAD_REPORT_BYTES as usize + 1)),
+ ),
+ ("/input_sha256", json!("f".repeat(64))),
+ ("/dependency_count", json!(0)),
+ ("/provider_archive_sha256", json!("f".repeat(64))),
+ ("/materialized_tree_sha256", json!("f".repeat(64))),
+ ("/tool_observation_sha256", json!("f".repeat(64))),
+ ("/raw_output_byte_length", json!(0)),
+ ("/raw_output_sha256", json!("f".repeat(64))),
+ ("/environment_sha256", json!("invalid")),
+ ("/environment_sha256", json!("f".repeat(64))),
+ ("/process_receipt_sha256", json!("f".repeat(64))),
+ ("/arguments", json!([])),
+ ("/exit_code", json!(2)),
+ ];
+ if provider.provider == ProviderId::OwaspNvd {
+ changes.extend([
+ ("/database_copy", json!(null)),
+ ("/database_copy/root_identity_sha256", json!("invalid")),
+ ("/database_copy/source_tree_sha256", json!("f".repeat(64))),
+ ("/database_copy/pre_scan_tree_sha256", json!("f".repeat(64))),
+ (
+ "/database_copy/post_scan_tree_sha256",
+ json!("f".repeat(64)),
+ ),
+ ]);
+ }
+ validate_workload_execution(provider, result, inventory, &fixture.request).unwrap();
+ for (pointer, replacement) in changes {
+ let mut changed = original["workload_result"][0].clone();
+ *changed.pointer_mut(pointer).unwrap() = replacement;
+ let changed: WorkloadResult = serde_json::from_value(changed).unwrap();
+ assert!(
+ validate_workload_execution(provider, &changed, inventory, &fixture.request)
+ .is_err(),
+ "{pointer}"
+ );
+ }
+ let bytes = canonical_json(&original);
+ assert!(
+ parse_unsuppressed_report(
+ provider,
+ &bytes,
+ &fixture.request,
+ &fixture.manifest.gradle_projection
+ )
+ .is_ok()
+ );
+ for (pointer, replacement) in [
+ ("/schema", json!("unknown")),
+ (
+ "/provider",
+ json!(if provider.provider == ProviderId::Rustsec {
+ "owasp_nvd"
+ } else {
+ "rustsec"
+ }),
+ ),
+ ("/workload_result", json!([])),
+ ("/workload_result/0/workload_id", json!("unknown")),
+ ] {
+ let mut changed = original.clone();
+ *changed.pointer_mut(pointer).unwrap() = replacement;
+ assert!(
+ parse_unsuppressed_report(
+ provider,
+ &canonical_json(&changed),
+ &fixture.request,
+ &fixture.manifest.gradle_projection
+ )
+ .is_err()
+ );
+ }
+ let mut noncanonical = bytes;
+ noncanonical.push(b' ');
+ assert!(
+ parse_unsuppressed_report(
+ provider,
+ &noncanonical,
+ &fixture.request,
+ &fixture.manifest.gradle_projection
+ )
+ .is_err()
+ );
+ }
+ }
+
+ #[test]
+ fn raw_gradle_graph_rejects_identity_variant_artifact_and_source_drift() {
+ let fixture = SyntheticFixture::new().unwrap();
+ let authority = WORKLOADS
+ .iter()
+ .find(|row| row.package_manager == "gradle")
+ .unwrap();
+ let workload_root = fixture._gradle_fixture_root.path().join(authority.id);
+ let raw: RawGradleGraph = serde_json::from_slice(
+ &std::fs::read(workload_root.join("raw").join(GRADLE_RAW_GRAPH_NAME)).unwrap(),
+ )
+ .unwrap();
+ let source = workload_root.join("source");
+ let source_identity = "a".repeat(64);
+ let roots = [GradleArtifactSourceRoot {
+ path: &source,
+ logical_role: "candidate_build_output",
+ identity_sha256: &source_identity,
+ }];
+ let revision = "1".repeat(40);
+ let normalize =
+ |raw: &RawGradleGraph| normalize_raw_gradle_graph(raw, authority, &revision, 1, &roots);
+ assert_eq!(normalize(&raw).unwrap().artifacts.len(), 1);
+ rejects_typed_changes(
+ &raw,
+ &[
+ ("/schema", json!("unknown")),
+ ("/workload_id", json!("unknown")),
+ ("/build_root", json!("unknown")),
+ ("/project_path", json!(":other")),
+ ("/configuration", json!("unknown")),
+ ("/components", json!([])),
+ ("/edges", json!([])),
+ (
+ "/edges/0/selected_variant/attributes/0/value",
+ json!("unselected"),
+ ),
+ (
+ "/artifacts/0/variant/attributes/0/value",
+ json!("unselected"),
+ ),
+ ("/artifacts/0/group", json!("other")),
+ ("/artifacts/0/name", json!("other")),
+ ("/artifacts/0/version", json!("2.0")),
+ ("/artifacts/0/module_version/group", json!("")),
+ ("/artifacts/0/module_version/name", json!("")),
+ ("/artifacts/0/module_version/version", json!("")),
+ ("/artifacts/0/module_version/group", json!("other")),
+ ("/artifacts/0/module_version/name", json!("other")),
+ ("/artifacts/0/module_version/version", json!("other")),
+ ("/artifacts/0/observed_byte_length", json!(0)),
+ (
+ "/artifacts/0/observed_byte_length",
+ json!(MAX_GRADLE_ARTIFACT_BYTES + 1),
+ ),
+ ("/artifacts/0/observed_byte_length", json!(1)),
+ ("/artifacts/0/observed_sha256", json!("invalid")),
+ ("/artifacts/0/observed_sha256", json!("0".repeat(64))),
+ ("/artifacts/0/artifact_name", json!("")),
+ ("/artifacts/0/artifact_name", json!("nested/unsafe.jar")),
+ ("/artifacts/0/artifact_name", json!("wrong.extension")),
+ ("/artifacts/0/logical_name", json!("")),
+ ("/artifacts/0/artifact_type", json!("")),
+ ("/artifacts/0/extension", json!("unknown")),
+ ("/artifacts/0/classifier", json!("")),
+ ("/artifacts/0/source_path", json!("")),
+ ("/artifacts/0/source_path", json!("relative/path")),
+ ("/artifacts/0/source_path", json!("/outside/root")),
+ (
+ "/edges/0/from",
+ serde_json::to_value(&raw.edges[0].to).unwrap(),
+ ),
+ ],
+ |changed| normalize(changed).is_ok(),
+ );
+ assert!(normalize_raw_gradle_graph(&raw, authority, &revision, 2, &roots).is_err());
+ assert!(normalize_raw_gradle_graph(&raw, &WORKLOADS[0], &revision, 1, &roots).is_err());
+ assert!(normalize_raw_gradle_graph(&raw, authority, &revision, 1, &[]).is_err());
+ for slot in ["components", "edges", "artifacts"] {
+ let mut changed = serde_json::to_value(&raw).unwrap();
+ let repeated = changed[slot][0].clone();
+ changed[slot].as_array_mut().unwrap().push(repeated);
+ assert!(normalize(&serde_json::from_value(changed).unwrap()).is_err());
+ }
+ let mut no_root = raw.clone();
+ for component in &mut no_root.components {
+ component.root = false;
+ }
+ assert!(normalize(&no_root).is_err());
+ let mut cycle = raw.clone();
+ cycle.edges[0].from = raw.edges[0].to.clone();
+ cycle.edges[0].to = raw.edges[0].from.clone();
+ assert!(normalize(&cycle).is_err());
+ }
+
+ #[test]
+ fn normalized_gradle_components_and_artifacts_keep_their_kind_specific_identity() {
+ let fixture = SyntheticFixture::new().unwrap();
+ let projection = &fixture.manifest.gradle_projection[0];
+ let artifact = &projection.artifacts[0];
+ let module = projection
+ .components
+ .iter()
+ .find(|row| row.kind == "module")
+ .unwrap();
+ let project = projection
+ .components
+ .iter()
+ .find(|row| row.kind == "project")
+ .unwrap();
+ rejects_typed_changes(
+ module,
+ &[
+ ("/group", json!(null)),
+ ("/name", json!(null)),
+ ("/version", json!(null)),
+ ("/build_root", json!(".")),
+ ("/project_path", json!(":app")),
+ ("/root", json!(true)),
+ ],
+ valid_gradle_component,
+ );
+ rejects_typed_changes(
+ project,
+ &[
+ ("/group", json!("extra")),
+ ("/name", json!("extra")),
+ ("/version", json!("extra")),
+ ("/build_root", json!(null)),
+ ("/project_path", json!(null)),
+ ],
+ valid_gradle_component,
+ );
+ rejects_typed_changes(
+ artifact,
+ &[
+ ("/component", json!("unknown")),
+ ("/package_ecosystem", json!("cargo")),
+ ("/package_namespace", json!("other")),
+ ("/package_name", json!("other")),
+ ("/package_version", json!("other")),
+ ],
+ |changed| artifact_matches_component(changed, module, &"1".repeat(40)),
+ );
+ for missing in ["group", "name", "version"] {
+ let mut changed = serde_json::to_value(module).unwrap();
+ changed[missing] = Value::Null;
+ assert!(!artifact_matches_component(
+ artifact,
+ &serde_json::from_value(changed).unwrap(),
+ &"1".repeat(40)
+ ));
+ }
+ let mut project_artifact = artifact.clone();
+ project_artifact.component = format!(
+ "{}:{}",
+ project.build_root.as_deref().unwrap(),
+ project.project_path.as_deref().unwrap()
+ );
+ project_artifact.package_namespace = "harvestcircle.app".to_owned();
+ project_artifact.package_name = "design_system".to_owned();
+ project_artifact.package_version = "unspecified".to_owned();
+ project_artifact.classifier = None;
+ assert!(artifact_matches_component(
+ &project_artifact,
+ project,
+ &"1".repeat(40)
+ ));
+ rejects_typed_changes(
+ &project_artifact,
+ &[
+ ("/component", json!("unknown")),
+ ("/package_ecosystem", json!("cargo")),
+ ("/package_namespace", json!("other")),
+ ("/classifier", json!("classified")),
+ ],
+ |changed| artifact_matches_component(changed, project, &"1".repeat(40)),
+ );
+ for missing in ["build_root", "project_path"] {
+ let mut changed = serde_json::to_value(project).unwrap();
+ changed[missing] = Value::Null;
+ assert!(!artifact_matches_component(
+ &project_artifact,
+ &serde_json::from_value(changed).unwrap(),
+ &"1".repeat(40)
+ ));
+ }
+ }
+
+ #[test]
+ fn request_requires_complete_ordered_sources_graphs_and_scanner_outputs() {
+ for case in 0..13 {
+ let mut fixture = SyntheticFixture::new().unwrap();
+ match case {
+ 0 => fixture.request.candidate.generation = 0,
+ 1 => fixture.request.candidate.digest = "invalid".to_owned(),
+ 2 => fixture.request.evaluation_epoch = 0,
+ 3 => {
+ fixture.request.inventory.pop();
+ }
+ 4 => {
+ fixture.request.admitted_gradle_graphs.pop();
+ }
+ 5 => {
+ fixture.request.admitted_scanner_outputs.pop();
+ }
+ 6 => {
+ fixture.request.sources.pop();
+ }
+ 7 => fixture.request.sources.swap(0, 1),
+ 8 => fixture.request.sources[0].revision = "invalid".to_owned(),
+ 9 => fixture.request.sources[0].tree = "invalid".to_owned(),
+ 10 => fixture.request.admitted_gradle_graphs.swap(0, 1),
+ 11 => fixture.request.admitted_scanner_outputs.swap(0, 1),
+ _ => fixture.request.sources.clear(),
+ }
+ assert!(
+ validate_request(&fixture.request).is_err(),
+ "request case {case}"
+ );
+ }
+ for (slot, maximum) in [
+ MAX_PRODUCER_REQUEST_BYTES,
+ MAX_TOOL_MANIFEST_BYTES,
+ MAX_TOOL_OBSERVATION_BYTES,
+ MAX_NVD_TRACE_BYTES,
+ MAX_PROVIDER_EVIDENCE_BYTES,
+ ]
+ .into_iter()
+ .enumerate()
+ {
+ let mut fixture = SyntheticFixture::new().unwrap();
+ let field = match slot {
+ 0 => &mut fixture.request.producer_request,
+ 1 => &mut fixture.request.step_297_tool_manifest,
+ 2 => &mut fixture.request.fresh_tool_observation,
+ 3 => &mut fixture.request.nvd_network_trace,
+ _ => &mut fixture.request.provider_execution_evidence,
+ };
+ field.resize(maximum + 1, b' ');
+ assert_eq!(
+ validate_trusted_authority(&fixture.request)
+ .unwrap_err()
+ .kind(),
+ AdvisoryFailureKind::InvalidSnapshot
+ );
+ }
+ }
+
+ #[test]
+ fn pinned_tools_and_scanner_arguments_reject_stale_or_substituted_inputs() {
+ let fixture = SyntheticFixture::new().unwrap();
+ let states = &fixture.manifest.tool_state;
+ rejects_typed_changes(
+ states,
+ &[
+ ("/0/id", json!("unknown")),
+ ("/0/normalized_version", json!("0")),
+ ("/0/executable_sha256", json!("f".repeat(64))),
+ ("/0/source_sha256", json!("f".repeat(64))),
+ ("/0/package_receipt_sha256", json!("f".repeat(64))),
+ ("/0/reviewed_at_epoch", json!(0)),
+ (
+ "/0/reviewed_at_epoch",
+ json!(fixture.request.evaluation_epoch + 1),
+ ),
+ (
+ "/0/reviewed_at_epoch",
+ json!(fixture.request.evaluation_epoch - TOOL_REVIEW_SECONDS - 1),
+ ),
+ ("/0/state", json!("unavailable")),
+ ],
+ |changed| validate_tool_states(changed, fixture.request.evaluation_epoch).is_ok(),
+ );
+ assert!(validate_tool_states(&[], fixture.request.evaluation_epoch).is_err());
+ let mut boundary = states.clone();
+ boundary[0].reviewed_at_epoch = fixture.request.evaluation_epoch - TOOL_REVIEW_SECONDS;
+ validate_tool_states(&boundary, fixture.request.evaluation_epoch).unwrap();
+ rejects_typed_changes(
+ &fixture.manifest.tool_acquisition,
+ &[("/0/id", json!("unknown")), ("/0/projection", json!({}))],
+ |changed| validate_tool_acquisitions(changed).is_ok(),
+ );
+ assert!(validate_tool_acquisitions(&[]).is_err());
+ for (provider, workload) in [
+ (ProviderId::Rustsec, "lib"),
+ (ProviderId::OwaspNvd, "app_design_system"),
+ ] {
+ let original = synthetic_scanner_arguments(provider, workload);
+ assert!(valid_scanner_arguments(provider, &original));
+ assert!(!valid_scanner_arguments(provider, &[]));
+ for index in 0..original.len() {
+ let mut changed = original.clone();
+ changed[index] = "untrusted".to_owned();
+ assert!(
+ !valid_scanner_arguments(provider, &changed),
+ "{provider:?} argument {index}"
+ );
+ }
+ }
+ for value in ["", "relative", "/contains\0nul"] {
+ assert!(!absolute_path(value));
+ }
+ assert!(absolute_path("/valid/path"));
+ }
+
+ #[test]
+ fn suppression_requires_unique_exact_finding_and_unexpired_owner_rationale() {
+ let suppression = Suppression {
+ id: "review-1".to_owned(),
+ provider: ProviderId::Rustsec,
+ advisory_id: "RUSTSEC-2099-0001".to_owned(),
+ workload_id: WORKLOADS[0].id.to_owned(),
+ package_ecosystem: "cargo".to_owned(),
+ package_namespace: String::new(),
+ package_name: "example".to_owned(),
+ package_version: "1.0.0".to_owned(),
+ owner: "security@example.invalid".to_owned(),
+ rationale: "Reviewed fixture".to_owned(),
+ created_at_epoch: 100,
+ expires_at_epoch: 200,
+ };
+ let finding = Finding {
+ provider: suppression.provider,
+ advisory_id: suppression.advisory_id.clone(),
+ package_ecosystem: suppression.package_ecosystem.clone(),
+ package_namespace: suppression.package_namespace.clone(),
+ package_name: suppression.package_name.clone(),
+ package_version: suppression.package_version.clone(),
+ workload_id: suppression.workload_id.clone(),
+ };
+ let mut findings = vec![finding.clone()];
+ apply_suppressions(&mut findings, std::slice::from_ref(&suppression), 150).unwrap();
+ assert!(findings.is_empty());
+ for mut findings in [vec![], vec![finding.clone(), finding.clone()]] {
+ assert!(
+ apply_suppressions(&mut findings, std::slice::from_ref(&suppression), 150).is_err()
+ );
+ }
+ rejects_typed_changes(
+ &suppression,
+ &[
+ ("/advisory_id", json!(NON_WAIVABLE_RUSTSEC)),
+ ("/advisory_id", json!("unknown")),
+ ("/id", json!("..")),
+ ("/package_ecosystem", json!("npm")),
+ ("/package_namespace", json!("unexpected")),
+ ("/package_name", json!("")),
+ ("/package_version", json!("")),
+ ("/workload_id", json!("unknown")),
+ ("/owner", json!("")),
+ ("/rationale", json!("")),
+ ("/created_at_epoch", json!(0)),
+ ("/created_at_epoch", json!(200)),
+ ("/created_at_epoch", json!(151)),
+ ("/expires_at_epoch", json!(150)),
+ ],
+ |changed| validate_suppression_inventory(std::slice::from_ref(changed), 150).is_ok(),
+ );
+ for field in [
+ "provider",
+ "advisory_id",
+ "package_ecosystem",
+ "package_namespace",
+ "package_name",
+ "package_version",
+ "workload_id",
+ ] {
+ let mut changed = serde_json::to_value(&suppression).unwrap();
+ changed[field] = if field == "provider" {
+ json!("owasp_nvd")
+ } else {
+ json!("mismatch")
+ };
+ let changed: Suppression = serde_json::from_value(changed).unwrap();
+ assert!(!suppression_matches(&changed, &finding), "{field}");
+ }
+ assert!(
+ validate_suppression_inventory(&[suppression.clone(), suppression.clone()], 150)
+ .is_err()
+ );
+ let mut second = suppression.clone();
+ second.id = "review-2".to_owned();
+ validate_suppression_inventory(&[suppression.clone(), second.clone()], 150).unwrap();
+ assert!(validate_suppression_inventory(&[second, suppression.clone()], 150).is_err());
+ let mut maven = suppression;
+ maven.provider = ProviderId::OwaspNvd;
+ maven.advisory_id = "CVE-2099-0001".to_owned();
+ maven.package_ecosystem = "maven".to_owned();
+ assert!(validate_suppression_inventory(&[maven.clone()], 150).is_err());
+ maven.package_namespace = "com.example".to_owned();
+ validate_suppression_inventory(&[maven], 150).unwrap();
+ }
+
+ #[test]
+ fn temporal_evidence_orders_review_acquisition_normalization_and_scans() {
+ let fixture = SyntheticFixture::new().unwrap();
+ let observation: TrustedToolObservation =
+ parse_canonical_authority(&fixture.request.fresh_tool_observation).unwrap();
+ let providers = &fixture.manifest.provider_snapshot;
+ let receipts = &fixture.process_receipts;
+ validate_temporal_order(&observation, providers, receipts).unwrap();
+ rejects_typed_changes(
+ &observation,
+ &[
+ ("/observed_at_epoch", json!(0)),
+ (
+ "/tool_state/0/reviewed_at_epoch",
+ json!(observation.observed_at_epoch + 1),
+ ),
+ ],
+ |changed| validate_temporal_order(changed, providers, receipts).is_ok(),
+ );
+ rejects_typed_changes(
+ providers,
+ &[
+ (
+ "/0/acquired_at_epoch",
+ json!(observation.observed_at_epoch - 1),
+ ),
+ ("/0/digest_time_epoch", json!(u64::MAX)),
+ ("/0/analyzed_at_epoch", json!(u64::MAX)),
+ (
+ "/1/acquired_at_epoch",
+ json!(providers[1].acquired_at_epoch + 1),
+ ),
+ ],
+ |changed| validate_temporal_order(&observation, changed, receipts).is_ok(),
+ );
+ assert!(validate_temporal_order(&observation, providers, &[]).is_err());
+ let update = receipts
+ .iter()
+ .position(|row| row.id == "owasp_nvd_update")
+ .unwrap();
+ let gradle = receipts
+ .iter()
+ .position(|row| row.id.starts_with("gradle:"))
+ .unwrap();
+ for (index, started) in [(update, true), (gradle, false)] {
+ let mut changed = receipts.clone();
+ if started {
+ changed[index].started_at_epoch = observation.observed_at_epoch - 1;
+ } else {
+ changed[index].completed_at_epoch = u64::MAX;
+ }
+ assert!(validate_temporal_order(&observation, providers, &changed).is_err());
+ }
+ }
+ use serde_json::json;
+
+ #[test]
+ fn complete_rustsec_reports_reject_nested_type_count_and_scanner_policy_drift() {
+ let mut fixture = SyntheticFixture::new().unwrap();
+ fixture
+ .set_rustsec_finding("RUSTSEC-2099-0001", "example", "1.0.0")
+ .unwrap();
+ let report = fixture.read_report_value(ProviderId::Rustsec).unwrap();
+ let result: WorkloadResult =
+ serde_json::from_value(report["workload_result"][0].clone()).unwrap();
+ let original: Value = serde_json::from_str(&result.raw_scanner_output).unwrap();
+ let provider = &fixture.manifest.provider_snapshot[0];
+ let parse = |value: &Value| {
+ parse_rustsec_output(
+ &result.workload_id,
+ value,
+ result.dependency_count,
+ &provider.database_identity_sha256,
+ &provider.materialized_tree_sha256,
+ provider.digest_time_epoch,
+ &mut Vec::new(),
+ )
+ };
+ rejects_wrong_node_types(&original, |value| parse(value).is_ok());
+ rejects_unknown_object_fields(
+ &original,
+ &[
+ "",
+ "/database",
+ "/lockfile",
+ "/settings",
+ "/vulnerabilities",
+ "/vulnerabilities/list/0",
+ "/vulnerabilities/list/0/advisory",
+ "/vulnerabilities/list/0/package",
+ "/vulnerabilities/list/0/versions",
+ ],
+ |value| parse(value).is_ok(),
+ );
+ let mut substituted_database = original.clone();
+ substituted_database["database"]["last-commit"] = json!("f".repeat(40));
+ assert!(parse(&substituted_database).is_err());
+ for (pointer, replacement) in [
+ ("/database/advisory-count", json!(0)),
+ ("/database/last-commit", json!("invalid")),
+ ("/database/last-updated", json!("invalid")),
+ ("/lockfile/dependency-count", json!(0)),
+ ("/settings/ignore", json!(["RUSTSEC-2099-0001"])),
+ ("/settings/target_arch", json!(["x86_64"])),
+ ("/settings/target_os", json!(["linux"])),
+ ("/settings/severity", json!("high")),
+ ("/settings/informational_warnings", json!([])),
+ ("/vulnerabilities/found", json!(false)),
+ ("/vulnerabilities/count", json!(0)),
+ ] {
+ let mut changed = original.clone();
+ *changed.pointer_mut(pointer).unwrap() = replacement;
+ assert!(parse(&changed).is_err(), "{pointer}");
+ }
+ for key in original.as_object().unwrap().keys() {
+ let mut changed = original.clone();
+ changed.as_object_mut().unwrap().remove(key);
+ assert!(parse(&changed).is_err(), "missing {key}");
+ }
+ for kind in ["notice", "unmaintained", "unsound"] {
+ let mut changed = original.clone();
+ let mut warning = original["vulnerabilities"]["list"][0].clone();
+ warning["kind"] = json!(kind);
+ changed["warnings"][kind] = json!([warning]);
+ rejects_wrong_node_types(&changed, |value| parse(value).is_ok());
+ rejects_unknown_object_fields(&changed, &[&format!("/warnings/{kind}/0")], |value| {
+ parse(value).is_ok()
+ });
+ changed["warnings"][kind][0]["kind"] = json!("other");
+ assert!(parse(&changed).is_err());
+ }
+ for (kind, rows) in [("unknown", json!([])), ("yanked", json!([{}]))] {
+ let mut changed = original.clone();
+ changed["warnings"][kind] = rows;
+ assert!(parse(&changed).is_err());
+ }
+ let mut empty_yanked = original.clone();
+ empty_yanked["warnings"]["yanked"] = json!([]);
+ assert!(parse(&empty_yanked).is_ok());
+ }
+
+ #[test]
+ fn complete_owasp_reports_bind_database_project_file_and_package_identity() {
+ let mut fixture = SyntheticFixture::new().unwrap();
+ let artifact = fixture.manifest.gradle_projection[0].artifacts[0].clone();
+ fixture
+ .set_owasp_finding(
+ "CVE-2099-0001",
+ &artifact.package_namespace,
+ &artifact.package_name,
+ &artifact.package_version,
+ )
+ .unwrap();
+ let report = fixture.read_report_value(ProviderId::OwaspNvd).unwrap();
+ let result: WorkloadResult =
+ serde_json::from_value(report["workload_result"][0].clone()).unwrap();
+ let mut original: Value = serde_json::from_str(&result.raw_scanner_output).unwrap();
+ for key in ["artifactID", "groupID", "version"] {
+ original["projectInfo"][key] = json!("fixture");
+ }
+ original["dependencies"][0]["description"] = json!("fixture");
+ original["dependencies"][0]["license"] = json!("Apache-2.0");
+ original["dependencies"][0]["projectReferences"] = json!(["fixture"]);
+ let provider = &fixture.manifest.provider_snapshot[1];
+ let projection = &fixture.manifest.gradle_projection[0];
+ let receipt = process_receipt(
+ &fixture.process_receipts,
+ &format!("owasp_analysis:{}", result.workload_id),
+ )
+ .unwrap();
+ let parse = |value: &Value| {
+ parse_owasp_output(
+ &result.workload_id,
+ value,
+ projection,
+ &provider.database_identity_sha256,
+ &provider.materialized_tree_sha256,
+ &result.arguments[4],
+ receipt.started_at_epoch,
+ receipt.completed_at_epoch,
+ provider.digest_time_epoch,
+ &mut Vec::new(),
+ )
+ };
+ rejects_wrong_node_types(&original, |value| parse(value).is_ok());
+ rejects_unknown_object_fields(
+ &original,
+ &[
+ "",
+ "/scanInfo",
+ "/scanInfo/dataSource/0",
+ "/projectInfo",
+ "/projectInfo/credits",
+ "/dependencies/0",
+ "/dependencies/0/evidenceCollected",
+ "/dependencies/0/packages/0",
+ "/dependencies/0/vulnerabilities/0",
+ ],
+ |value| parse(value).is_ok(),
+ );
+ for timestamp in [
+ provider.digest_time_epoch - 1,
+ provider.digest_time_epoch + 1,
+ ] {
+ let mut changed = original.clone();
+ changed["scanInfo"]["dataSource"][0]["timestamp"] =
+ json!(format_report_epoch(timestamp).unwrap());
+ assert!(parse(&changed).is_err());
+ }
+ for timestamp in [receipt.started_at_epoch - 1, receipt.completed_at_epoch + 1] {
+ let mut changed = original.clone();
+ changed["projectInfo"]["reportDate"] = json!(format_report_epoch(timestamp).unwrap());
+ assert!(parse(&changed).is_err());
+ }
+ for count in [2, 65] {
+ let mut changed = original.clone();
+ changed["scanInfo"]["dataSource"] =
+ json!(vec![original["scanInfo"]["dataSource"][0].clone(); count]);
+ assert!(parse(&changed).is_err());
+ }
+ let mut database_name = original.clone();
+ database_name["scanInfo"]["dataSource"][0]["name"] = json!("NVD CVE substituted");
+ assert!(parse(&database_name).is_err());
+ let mut unsorted_references = original.clone();
+ unsorted_references["dependencies"][0]["projectReferences"] = json!(["z", "a"]);
+ assert!(parse(&unsorted_references).is_err());
+ for (pointer, replacement) in [
+ ("/reportSchema", json!("2.0")),
+ ("/scanInfo/engineVersion", json!("unknown")),
+ ("/scanInfo/dataSource", json!([])),
+ ("/scanInfo/dataSource/0/name", json!("other")),
+ (
+ "/scanInfo/dataSource/0/timestamp",
+ json!("2099-01-01T00:00:00Z"),
+ ),
+ ("/projectInfo/name", json!("another-workload")),
+ ("/projectInfo/reportDate", json!("1970-01-01T00:00:00Z")),
+ ("/dependencies", json!([])),
+ ("/dependencies/0/fileName", json!("other.jar")),
+ ("/dependencies/0/filePath", json!("/other/path")),
+ ("/dependencies/0/md5", json!("invalid")),
+ ("/dependencies/0/sha1", json!("invalid")),
+ ("/dependencies/0/sha256", json!("invalid")),
+ ("/dependencies/0/sha256", json!("f".repeat(64))),
+ (
+ "/dependencies/0/packages/0/id",
+ json!("pkg:maven/other/package@1.0"),
+ ),
+ ("/dependencies/0/vulnerabilityIds/0/id", json!("invalid")),
+ (
+ "/dependencies/0/vulnerabilityIds/0/id",
+ json!("cpe:/a:other:package:1.0"),
+ ),
+ ] {
+ let mut changed = original.clone();
+ *changed.pointer_mut(pointer).unwrap() = replacement;
+ assert!(parse(&changed).is_err(), "{pointer}");
+ }
+ for key in [
+ "suppressedVulnerabilities",
+ "suppressedVulnerabilityIds",
+ "unknown",
+ ] {
+ let mut changed = original.clone();
+ changed["dependencies"][0][key] = json!([]);
+ assert!(parse(&changed).is_err());
+ }
+ let mut missing_findings = original.clone();
+ missing_findings["dependencies"][0]
+ .as_object_mut()
+ .unwrap()
+ .remove("vulnerabilities");
+ assert!(parse(&missing_findings).is_err());
+ let mut duplicated = original.clone();
+ duplicated["dependencies"]
+ .as_array_mut()
+ .unwrap()
+ .push(original["dependencies"][0].clone());
+ assert!(parse(&duplicated).is_err());
+ }
+
+ fn rejects_unknown_object_fields(
+ original: &Value,
+ pointers: &[&str],
+ validate: impl Fn(&Value) -> bool,
+ ) {
+ assert!(validate(original), "valid closed-schema fixture");
+ for pointer in pointers {
+ let mut changed = original.clone();
+ let object = changed
+ .pointer_mut(pointer)
+ .expect(pointer)
+ .as_object_mut()
+ .expect(pointer);
+ assert!(
+ object
+ .insert("unexpected_field".into(), json!(true))
+ .is_none()
+ );
+ assert!(!validate(&changed), "unknown field at {pointer}");
+ }
+ }
+
+ #[test]
+ fn virtual_owasp_dependencies_require_rooted_identity_and_emit_exact_findings() {
+ let mut fixture = SyntheticFixture::new().unwrap();
+ let artifact = fixture.manifest.gradle_projection[0].artifacts[0].clone();
+ fixture
+ .set_owasp_finding(
+ "CVE-2099-0001",
+ &artifact.package_namespace,
+ &artifact.package_name,
+ &artifact.package_version,
+ )
+ .unwrap();
+ let report = fixture.read_report_value(ProviderId::OwaspNvd).unwrap();
+ let result: WorkloadResult =
+ serde_json::from_value(report["workload_result"][0].clone()).unwrap();
+ let mut original: Value = serde_json::from_str(&result.raw_scanner_output).unwrap();
+ let provider = &fixture.manifest.provider_snapshot[1];
+ let projection = &fixture.manifest.gradle_projection[0];
+ let receipt = process_receipt(
+ &fixture.process_receipts,
+ &format!("owasp_analysis:{}", result.workload_id),
+ )
+ .unwrap();
+ let parse = |value: &Value, scan_root: &str| {
+ let mut findings = Vec::new();
+ parse_owasp_output(
+ &result.workload_id,
+ value,
+ projection,
+ &provider.database_identity_sha256,
+ &provider.materialized_tree_sha256,
+ scan_root,
+ receipt.started_at_epoch,
+ receipt.completed_at_epoch,
+ provider.digest_time_epoch,
+ &mut findings,
+ )?;
+ Ok::<_, AdvisoryError>(findings)
+ };
+ let mut virtual_row = original["dependencies"][0].clone();
+ for key in ["md5", "sha1", "sha256"] {
+ virtual_row.as_object_mut().unwrap().remove(key);
+ }
+ virtual_row["isVirtual"] = json!(true);
+ virtual_row["fileName"] = json!("embedded-module");
+ virtual_row["filePath"] = json!(format!("{}/embedded-module", result.arguments[4]));
+ virtual_row["packages"] = json!([{"id":"pkg:maven/embedded/library@2.0"}]);
+ virtual_row["includedBy"] =
+ json!([{"reference":format!("project:{}", result.workload_id)}]);
+ original["dependencies"]
+ .as_array_mut()
+ .unwrap()
+ .push(virtual_row);
+ let findings = parse(&original, &result.arguments[4]).unwrap();
+ assert_eq!(findings.len(), 2);
+ assert_eq!(findings[1].package_namespace, "embedded");
+ assert_eq!(findings[1].package_name, "library");
+ assert_eq!(findings[1].package_version, "2.0");
+ assert_eq!(findings[1].advisory_id, "CVE-2099-0001");
+ for (key, value) in [
+ ("md5", json!("1".repeat(32))),
+ ("sha1", json!("1".repeat(40))),
+ ("sha256", json!("1".repeat(64))),
+ ("filePath", json!("/outside/module")),
+ ("fileName", json!(artifact.materialized_name)),
+ ("packages", json!([])),
+ ("includedBy", json!([])),
+ ] {
+ let mut changed = original.clone();
+ changed["dependencies"][1][key] = value;
+ assert!(parse(&changed, &result.arguments[4]).is_err(), "{key}");
+ }
+ for key in ["packages", "includedBy"] {
+ let mut changed = original.clone();
+ changed["dependencies"][1]
+ .as_object_mut()
+ .unwrap()
+ .remove(key);
+ assert!(parse(&changed, &result.arguments[4]).is_err());
+ }
+ let mut missing_physical = original.clone();
+ missing_physical["dependencies"]
+ .as_array_mut()
+ .unwrap()
+ .remove(0);
+ assert!(parse(&missing_physical, &result.arguments[4]).is_err());
+ for root in ["relative", "/trailing/"] {
+ assert!(parse(&original, root).is_err());
+ }
+ }
+
+ #[test]
+ fn scanner_execution_rejects_independently_substituted_trusted_receipts() {
+ let mut fixture = SyntheticFixture::new().unwrap();
+ for provider in &fixture.manifest.provider_snapshot {
+ let original =
+ fixture.read_report_value(provider.provider).unwrap()["workload_result"][0].clone();
+ let original_result: WorkloadResult = serde_json::from_value(original.clone()).unwrap();
+ let inventory = fixture
+ .request
+ .inventory
+ .iter()
+ .find(|row| row.id == original_result.workload_id)
+ .unwrap()
+ .clone();
+ let id = format!(
+ "{}:{}",
+ if provider.provider == ProviderId::Rustsec {
+ "cargo_audit"
+ } else {
+ "owasp_analysis"
+ },
+ original_result.workload_id
+ );
+ let authority: TrustedProviderEvidence =
+ parse_canonical_authority(&fixture.request.provider_execution_evidence).unwrap();
+ validate_workload_execution(provider, &original_result, &inventory, &fixture.request)
+ .unwrap();
+ let original_bytes = fixture.request.provider_execution_evidence.clone();
+ let authority_value = serde_json::to_value(&authority).unwrap();
+ let index = authority
+ .process_receipt
+ .iter()
+ .position(|row| row.id == id)
+ .unwrap();
+ for (field, replacement) in [
+ ("program_sha256", json!("f".repeat(64))),
+ ("arguments_sha256", json!("f".repeat(64))),
+ ("input_sha256", json!("f".repeat(64))),
+ ("output_sha256", json!("f".repeat(64))),
+ ("stdout_byte_length", json!(1)),
+ ("stdout_sha256", json!("f".repeat(64))),
+ ("exit_code", json!(2)),
+ ("path_binding", json!([])),
+ ] {
+ let mut changed = authority_value.clone();
+ assert_ne!(
+ changed["process_receipt"][index][field], replacement,
+ "mutation {field}"
+ );
+ changed["process_receipt"][index][field] = replacement;
+ let changed: TrustedProviderEvidence = serde_json::from_value(changed).unwrap();
+ fixture.request.provider_execution_evidence =
+ canonical_authority_bytes(&changed).unwrap();
+ validate_trusted_authority(&fixture.request)
+ .expect("receipt remains structurally trusted");
+ let mut result = original.clone();
+ result["process_receipt_sha256"] =
+ json!(process_receipt_digest(&changed.process_receipt[index]).unwrap());
+ let result: WorkloadResult = serde_json::from_value(result).unwrap();
+ assert!(
+ validate_workload_execution(provider, &result, &inventory, &fixture.request)
+ .is_err(),
+ "{field}"
+ );
+ }
+ fixture.request.provider_execution_evidence = original_bytes;
+ }
+ }
+
+ fn rejects_typed_changes<T: for<'de> Deserialize<'de> + Serialize>(
+ original: &T,
+ changes: &[(&str, Value)],
+ validate: impl Fn(&T) -> bool,
+ ) {
+ assert!(validate(original), "accepted typed fixture");
+ let value = serde_json::to_value(original).unwrap();
+ for (pointer, replacement) in changes {
+ let mut changed = value.clone();
+ *changed.pointer_mut(pointer).expect(pointer) = replacement.clone();
+ assert_ne!(changed, value, "mutation must change {pointer}");
+ let changed: T =
+ serde_json::from_value(changed).expect("mutation preserves wire types");
+ assert!(
+ !validate(&changed),
+ "invalid typed field accepted: {pointer}"
+ );
+ }
+ }
+
+ #[test]
+ fn provider_admission_binds_artifacts_times_and_execution_receipts() {
+ let fixture = SyntheticFixture::new().unwrap();
+ let trace: NvdNetworkTrace =
+ parse_canonical_authority(&fixture.request.nvd_network_trace).unwrap();
+ for provider in &fixture.manifest.provider_snapshot {
+ let mut changes = vec![
+ ("/acquisition_state", json!("failed")),
+ ("/analysis_state", json!("unavailable")),
+ ("/acquisition_count", json!(0)),
+ ("/bounded_deadline_seconds", json!(0)),
+ ("/network_trace_sha256", json!("invalid")),
+ ("/producer_request_sha256", json!("invalid")),
+ ("/producer_request_sha256", json!("f".repeat(64))),
+ ("/database_identity_sha256", json!("invalid")),
+ ("/archive_format", json!("zip")),
+ ("/archive_expanded_bytes", json!(0)),
+ ("/archive_member_count", json!(0)),
+ ("/archive_payload_bytes", json!(0)),
+ ("/acquired_at_epoch", json!(0)),
+ ("/digest_time_epoch", json!(0)),
+ ("/analyzed_at_epoch", json!(0)),
+ ("/acquired_at_epoch", json!(u64::MAX)),
+ ("/digest_time_epoch", json!(u64::MAX)),
+ ("/analyzed_at_epoch", json!(u64::MAX)),
+ ("/acquisition_kind", json!("untrusted")),
+ ("/network_mode", json!("unrestricted")),
+ ("/archive/path", json!("other.gz")),
+ ("/report/path", json!("other.json")),
+ ("/analysis_environment", json!("ambient")),
+ ("/analysis_arguments", json!([])),
+ ("/acquisition_arguments", json!(["untrusted"])),
+ ("/archive/byte_length", json!(0)),
+ ("/report/byte_length", json!(0)),
+ ("/archive/byte_length", json!(2_147_483_649u64)),
+ ("/report/byte_length", json!(MAX_REPORT_BYTES + 1)),
+ ("/archive/sha256", json!("invalid")),
+ ("/report/sha256", json!("invalid")),
+ ("/archive/logical_uri", json!("file:///untrusted")),
+ ("/report/logical_uri", json!("file:///untrusted")),
+ ("/archive/media_type", json!("application/zip")),
+ ("/report/media_type", json!("text/plain")),
+ ("/archive/logical_role", json!("unknown")),
+ ("/report/logical_role", json!("unknown")),
+ ];
+ if provider.provider == ProviderId::Rustsec {
+ changes.push(("/network_trace_sha256", json!("a".repeat(64))));
+ }
+ rejects_typed_changes(provider, &changes, |changed| {
+ validate_provider(changed, &fixture.request, &trace, &fixture.process_receipts)
+ .is_ok()
+ });
+ }
+ let provider = &fixture.manifest.provider_snapshot[1];
+ let receipts = &fixture.process_receipts;
+ rejects_typed_changes(
+ receipts,
+ &[
+ ("/0/program_sha256", json!("a".repeat(64))),
+ ("/0/arguments_sha256", json!("a".repeat(64))),
+ ("/0/output_sha256", json!("a".repeat(64))),
+ ("/0/input_sha256", json!("a".repeat(64))),
+ ("/0/path_binding", json!([])),
+ ("/0/completed_at_epoch", json!(0)),
+ ("/0/exit_code", json!(1)),
+ ],
+ |changed| validate_provider(provider, &fixture.request, &trace, changed).is_ok(),
+ );
+ }
+
+ #[test]
+ fn request_inventory_and_projection_fields_are_independently_bound() {
+ let mut fixture = SyntheticFixture::new().unwrap();
+ let inventory = fixture.request.inventory.clone();
+ let original = serde_json::to_value(&inventory[0]).unwrap();
+ for (key, value) in original.as_object().unwrap() {
+ let mut changed = original.clone();
+ changed[key] = match value {
+ Value::Number(_) => json!(0),
+ Value::String(_) | Value::Null => json!("untrusted"),
+ _ => panic!("unexpected inventory field {key}"),
+ };
+ fixture.request.inventory[0] = serde_json::from_value(changed).unwrap();
+ assert!(validate_request(&fixture.request).is_err(), "{key}");
+ }
+ fixture.request.inventory = inventory;
+ validate_request(&fixture.request).unwrap();
+ let projections = &fixture.manifest.gradle_projection;
+ let original = serde_json::to_value(&projections[0]).unwrap();
+ for (key, value) in original.as_object().unwrap() {
+ let mut changed = original.clone();
+ changed[key] = match value {
+ Value::Number(_) => {
+ if key == "exit_code" {
+ json!(1)
+ } else {
+ json!(0)
+ }
+ }
+ Value::String(_) => {
+ if key == "state" {
+ json!("failed")
+ } else {
+ json!("untrusted")
+ }
+ }
+ Value::Array(_) => json!([]),
+ _ => panic!("unexpected projection field {key}"),
+ };
+ let mut candidate = projections.clone();
+ candidate[0] = serde_json::from_value(changed).unwrap();
+ assert!(
+ validate_gradle_projections(
+ &candidate,
+ &fixture.request,
+ &fixture.process_receipts
+ )
+ .is_err(),
+ "{key}"
+ );
+ }
+ assert!(
+ validate_gradle_projections(&[], &fixture.request, &fixture.process_receipts).is_err()
+ );
+ fixture.request.sources.clear();
+ assert!(validate_request(&fixture.request).is_err());
+ assert!(
+ validate_gradle_projections(projections, &fixture.request, &fixture.process_receipts)
+ .is_err()
+ );
+ }
+
+ #[test]
+ fn raw_gradle_variants_bound_attributes_capabilities_and_recursion() {
+ let variant: RawGradleVariant = serde_json::from_value(json!({
+ "attributes":[{"name":"usage", "value":"runtime"}],
+ "capabilities":[{"group":"example", "name":"library", "version":"1.0"}],
+ "external_variant":null,
+ }))
+ .unwrap();
+ rejects_typed_changes(
+ &variant,
+ &[
+ ("/attributes/0/name", json!("")),
+ ("/attributes/0/value", json!("\0")),
+ ("/capabilities/0/group", json!("\0")),
+ ("/capabilities/0/name", json!("")),
+ ("/capabilities/0/version", json!("\0")),
+ ],
+ |changed| validate_raw_variant(changed, 0).is_ok(),
+ );
+ let mut duplicate = variant.clone();
+ duplicate.attributes.push(duplicate.attributes[0].clone());
+ assert!(validate_raw_variant(&duplicate, 0).is_err());
+ duplicate.attributes[1].value = "z".into();
+ assert!(validate_raw_variant(&duplicate, 0).is_err());
+ let mut oversized = variant.clone();
+ oversized.attributes = vec![variant.attributes[0].clone(); MAX_GRADLE_ATTRIBUTES + 1];
+ assert!(validate_raw_variant(&oversized, 0).is_err());
+ oversized = variant.clone();
+ oversized.capabilities = vec![variant.capabilities[0].clone(); MAX_GRADLE_CAPABILITIES + 1];
+ assert!(validate_raw_variant(&oversized, 0).is_err());
+ let mut nested = variant.clone();
+ for _ in 0..MAX_GRADLE_EXTERNAL_VARIANT_DEPTH {
+ let mut parent = variant.clone();
+ parent.external_variant = Some(Box::new(nested));
+ nested = parent;
+ }
+ validate_raw_variant(&nested, 0).unwrap();
+ let mut too_deep = variant.clone();
+ too_deep.external_variant = Some(Box::new(nested));
+ assert!(validate_raw_variant(&too_deep, 0).is_err());
+ assert!(
+ validate_raw_variant_envelope(&RawGradleVariantEnvelope { selected: vec![] }).is_err()
+ );
+ assert!(
+ validate_raw_variant_envelope(&RawGradleVariantEnvelope {
+ selected: vec![variant.clone(); MAX_GRADLE_VARIANTS + 1]
+ })
+ .is_err()
+ );
+ assert!(
+ validate_raw_variant_envelope(&RawGradleVariantEnvelope {
+ selected: vec![variant.clone(), variant]
+ })
+ .is_err()
+ );
+ }
+
+ #[test]
+ fn raw_gradle_selectors_reject_mixed_identity_and_invalid_constraints() {
+ let module: RawGradleSelector = serde_json::from_value(json!({
+ "kind":"module", "group":"example", "name":"library", "version":"1.0",
+ "build_root":null, "project_path":null, "attributes":[], "capabilities":[],
+ "version_constraint":{"branch":null, "preferred":"", "required":"1.0", "strict":"", "rejected":["0.9"]}
+ })).unwrap();
+ rejects_typed_changes(
+ &module,
+ &[
+ ("/kind", json!("unknown")),
+ ("/group", json!(null)),
+ ("/name", json!(null)),
+ ("/version", json!(null)),
+ ("/group", json!("")),
+ ("/name", json!("")),
+ ("/version", json!("\0")),
+ ("/build_root", json!(".")),
+ ("/project_path", json!(":app")),
+ ("/version_constraint", json!(null)),
+ ("/version_constraint/branch", json!("\0")),
+ ("/version_constraint/preferred", json!("\0")),
+ ("/version_constraint/required", json!("\0")),
+ ("/version_constraint/strict", json!("\0")),
+ ("/version_constraint/rejected", json!([""])),
+ ("/version_constraint/rejected", json!(["b", "a"])),
+ ("/version_constraint/rejected", json!(["a", "a"])),
+ (
+ "/version_constraint/rejected",
+ json!(vec!["x"; MAX_GRADLE_REJECTED_VERSIONS + 1]),
+ ),
+ ],
+ |changed| validate_raw_selector(changed).is_ok(),
+ );
+ let project: RawGradleSelector = serde_json::from_value(json!({
+ "kind":"project", "group":null, "name":null, "version":null, "version_constraint":null,
+ "build_root":".", "project_path":":app:shared", "attributes":[], "capabilities":[]
+ }))
+ .unwrap();
+ rejects_typed_changes(
+ &project,
+ &[
+ ("/group", json!("example")),
+ ("/name", json!("library")),
+ ("/version", json!("1.0")),
+ (
+ "/version_constraint",
+ serde_json::to_value(&module.version_constraint).unwrap(),
+ ),
+ ("/build_root", json!(null)),
+ ("/project_path", json!(null)),
+ ("/build_root", json!("../other")),
+ ("/project_path", json!("app")),
+ ("/project_path", json!("::app")),
+ ],
+ |changed| validate_raw_selector(changed).is_ok(),
+ );
+ for selector in [&module, &project] {
+ let core = RawGradleComponentCore {
+ kind: selector.kind.clone(),
+ group: selector.group.clone(),
+ name: selector.name.clone(),
+ version: selector.version.clone(),
+ build_root: selector.build_root.clone(),
+ project_path: selector.project_path.clone(),
+ };
+ validate_raw_component_core(&core).unwrap();
+ let original = serde_json::to_value(&core).unwrap();
+ for (key, value) in original.as_object().unwrap() {
+ let mut changed = original.clone();
+ changed[key] = if value.is_null() {
+ json!("untrusted")
+ } else {
+ json!("")
+ };
+ assert!(
+ validate_raw_component_core(&serde_json::from_value(changed).unwrap()).is_err(),
+ "{key}"
+ );
+ }
+ }
+ }
+
+ #[test]
+ fn owasp_virtual_lineage_rejects_orphans_cycles_and_alias_collisions() {
+ fn dependency(alias: &str, parents: &[&str], is_virtual: bool) -> ParsedOwaspDependency {
+ ParsedOwaspDependency {
+ aliases: vec![alias.into()],
+ included_by: parents.iter().map(|value| (*value).into()).collect(),
+ is_virtual,
+ file_name: alias.into(),
+ package_ids: vec![],
+ vulnerabilities: vec![],
+ }
+ }
+ assert!(
+ validate_owasp_lineage(
+ "root",
+ &[
+ dependency("physical", &[], false),
+ dependency("parent", &["project:root"], true),
+ dependency("child", &["parent", "physical"], true),
+ dependency("sibling", &["parent"], true),
+ ]
+ )
+ .is_ok()
+ );
+ for rows in [
+ vec![dependency("orphan", &[], true)],
+ vec![dependency("child", &["absent"], true)],
+ vec![dependency("a", &["b"], true), dependency("b", &["a"], true)],
+ vec![
+ dependency("duplicate", &[], false),
+ dependency("duplicate", &[], false),
+ ],
+ vec![dependency("root", &[], false)],
+ ] {
+ assert!(validate_owasp_lineage("root", &rows).is_err());
+ }
+ }
+
+ #[test]
+ fn rustsec_affected_missing_required_fields_returns_a_typed_error() {
+ for field in ["arch", "functions", "os"] {
+ let mut value = json!({"arch":[], "functions":{}, "os":[]});
+ value.as_object_mut().unwrap().remove(field);
+ let error = validate_rustsec_affected(&value).expect_err(field);
+ assert_eq!(error.kind(), AdvisoryFailureKind::InvalidReport);
+ }
+ }
+
+ // Exercise a complete accepted wire object, then corrupt each node's JSON
+ // type independently. A malformed nested row must never be ignored merely
+ // because another part of the report remains valid.
+ fn rejects_wrong_node_types(value: &Value, validate: impl Fn(&Value) -> bool) {
+ fn paths(value: &Value, prefix: String, out: &mut Vec<String>) {
+ out.push(prefix.clone());
+ match value {
+ Value::Object(object) => {
+ for (key, child) in object {
+ let key = key.replace('~', "~0").replace('/', "~1");
+ paths(child, format!("{prefix}/{key}"), out);
+ }
+ }
+ Value::Array(rows) => {
+ for (index, child) in rows.iter().enumerate() {
+ paths(child, format!("{prefix}/{index}"), out);
+ }
+ }
+ _ => {}
+ }
+ }
+ assert!(validate(value), "positive wire fixture must be accepted");
+ let mut pointers = Vec::new();
+ paths(value, String::new(), &mut pointers);
+ for pointer in pointers {
+ let mut changed = value.clone();
+ let node = changed.pointer_mut(&pointer).expect("fixture pointer");
+ *node = match node {
+ Value::Object(_) | Value::Null => json!([]),
+ Value::Array(_) => json!({}),
+ Value::String(_) => json!(false),
+ Value::Number(_) => json!("invalid-number"),
+ Value::Bool(_) => json!(0),
+ };
+ assert!(!validate(&changed), "wrong type accepted at {pointer}");
+ }
+ }
+
+ #[test]
+ fn rustsec_nested_package_affected_and_version_rows_fail_closed() {
+ let dependency =
+ json!({"name":"dep", "source":"registry+https://example.test", "version":"1.2.3"});
+ rejects_wrong_node_types(&dependency, valid_rustsec_dependency);
+ let package = json!({
+ "name":"example", "version":"1.0.0", "source":"registry+https://example.test",
+ "checksum":"a".repeat(64), "replace":dependency, "dependencies":[dependency]
+ });
+ rejects_wrong_node_types(&package, |value| validate_rustsec_package(value).is_ok());
+ for (pointer, replacement) in [
+ ("/name", json!("")),
+ ("/version", json!("..")),
+ ("/checksum", json!("g".repeat(64))),
+ ("/dependencies", json!([])),
+ ] {
+ let mut invalid = package.clone();
+ *invalid.pointer_mut(pointer).unwrap() = replacement;
+ assert!(validate_rustsec_package(&invalid).is_err(), "{pointer}");
+ }
+ assert!(
+ validate_rustsec_package(&json!({
+ "name":"example", "version":"1.0.0", "source":null, "checksum":null, "replace":null
+ }))
+ .is_ok()
+ );
+ let affected = json!({"arch":["aarch64"], "os":["linux"], "functions":{"example::Thing<T,U>::_method":["<1.0.0"]}});
+ rejects_wrong_node_types(&affected, |value| validate_rustsec_affected(value).is_ok());
+ assert!(validate_rustsec_affected(&Value::Null).is_ok());
+ for path in [
+ "",
+ "example",
+ "::method",
+ "example::",
+ "0crate::method",
+ "crate::bad-name",
+ "crate::méthod",
+ ] {
+ assert!(!valid_rust_function_path(path), "{path}");
+ }
+ rejects_wrong_node_types(
+ &json!({"patched":[">=1.0.0"], "unaffected":["<0.5.0"]}),
+ |value| validate_rustsec_versions(value).is_ok(),
+ );
+ }
+
+ #[test]
+ fn rustsec_advisory_optional_metadata_is_validated_when_present() {
+ let mut fixture = SyntheticFixture::new().unwrap();
+ fixture
+ .set_rustsec_finding("RUSTSEC-2099-0001", "example", "1.0.0")
+ .unwrap();
+ let report = fixture.read_report_value(ProviderId::Rustsec).unwrap();
+ let raw: Value = serde_json::from_str(
+ report["workload_result"][0]["raw_scanner_output"]
+ .as_str()
+ .unwrap(),
+ )
+ .unwrap();
+ let mut advisory = raw["vulnerabilities"]["list"][0]["advisory"].clone();
+ for key in ["aliases", "categories", "keywords", "references", "related"] {
+ advisory[key] = json!(["example"]);
+ }
+ for key in ["cvss", "informational", "url"] {
+ advisory[key] = json!("example");
+ }
+ advisory["withdrawn"] = json!("2099-02-28");
+ rejects_wrong_node_types(&advisory, |value| {
+ validate_rustsec_advisory(value, "example").is_ok()
+ });
+ for (key, invalid) in [
+ ("package", "another"),
+ ("id", "CVE-2099-0001"),
+ ("license", ""),
+ ("date", "2099-02-29"),
+ ("withdrawn", "2099/01/01"),
+ ] {
+ let mut changed = advisory.clone();
+ changed[key] = json!(invalid);
+ assert!(
+ validate_rustsec_advisory(&changed, "example").is_err(),
+ "{key}"
+ );
+ }
+ for date in [
+ "",
+ "2099-01001",
+ "2099/01-01",
+ "2099-01/01",
+ "209x-01-01",
+ "2099-13-01",
+ "2099-01-32",
+ ] {
+ assert!(!valid_rustsec_date(&json!(date)), "{date}");
+ }
+ assert!(valid_rustsec_date(&json!("2000-02-29")));
+ }
+
+ #[test]
+ fn manifest_rejects_each_independently_changed_binding() {
+ let fixture = SyntheticFixture::new().unwrap();
+ validate_manifest(&fixture.manifest, &fixture.request).unwrap();
+ let original = serde_json::to_value(&fixture.manifest).unwrap();
+ for (key, value) in original.as_object().unwrap() {
+ let mut changed = original.clone();
+ changed[key] = match value {
+ Value::String(_) => json!("wrong-binding"),
+ Value::Array(_) => json!([]),
+ Value::Object(_) => {
+ let mut candidate = value.clone();
+ candidate["digest"] = json!("f".repeat(64));
+ candidate
+ }
+ _ => panic!("unexpected manifest field {key}"),
+ };
+ if changed == original {
+ continue;
+ }
+ let manifest: SnapshotManifest = serde_json::from_value(changed).unwrap();
+ assert!(
+ validate_manifest(&manifest, &fixture.request).is_err(),
+ "unbound manifest field {key}"
+ );
+ }
+ }
+
+ #[test]
+ fn trusted_authority_rejects_each_changed_envelope_field() {
+ let mut fixture = SyntheticFixture::new().unwrap();
+ for slot in 0..5 {
+ let original = match slot {
+ 0 => &fixture.request.producer_request,
+ 1 => &fixture.request.step_297_tool_manifest,
+ 2 => &fixture.request.fresh_tool_observation,
+ 3 => &fixture.request.nvd_network_trace,
+ _ => &fixture.request.provider_execution_evidence,
+ }
+ .clone();
+ let value: Value = serde_json::from_slice(&original).unwrap();
+ for (key, field) in value.as_object().unwrap() {
+ // These two values are bound by manifest admission after the
+ // trusted envelope has been admitted; that boundary is above.
+ if matches!(
+ key.as_str(),
+ "candidate_advisory_input_sha256" | "suppressions"
+ ) {
+ continue;
+ }
+ let mut changed = value.clone();
+ changed[key] = match field {
+ Value::String(_) => json!("wrong-binding"),
+ Value::Number(_) => json!(u64::MAX),
+ Value::Array(_) => json!([]),
+ _ => panic!("unexpected authority field {key}"),
+ };
+ let bytes = canonical_authority_bytes(&changed).unwrap();
+ match slot {
+ 0 => fixture.request.producer_request = bytes,
+ 1 => fixture.request.step_297_tool_manifest = bytes,
+ 2 => fixture.request.fresh_tool_observation = bytes,
+ 3 => fixture.request.nvd_network_trace = bytes,
+ _ => fixture.request.provider_execution_evidence = bytes,
+ }
+ assert!(
+ validate_trusted_authority(&fixture.request).is_err(),
+ "slot {slot} field {key}"
+ );
+ match slot {
+ 0 => fixture.request.producer_request = original.clone(),
+ 1 => fixture.request.step_297_tool_manifest = original.clone(),
+ 2 => fixture.request.fresh_tool_observation = original.clone(),
+ 3 => fixture.request.nvd_network_trace = original.clone(),
+ _ => fixture.request.provider_execution_evidence = original.clone(),
+ }
+ }
+ }
+ fixture.request.producer_request.push(b' ');
+ assert!(validate_trusted_authority(&fixture.request).is_err());
+ }
+
+ #[test]
+ fn process_receipts_reject_identity_environment_bounds_and_order_faults() {
+ let fixture = SyntheticFixture::new().unwrap();
+ let original = serde_json::to_value(&fixture.process_receipts).unwrap();
+ validate_process_receipts(&fixture.process_receipts, fixture.request.evaluation_epoch)
+ .unwrap();
+ for (pointer, replacement) in [
+ ("/0/id", json!("unknown")),
+ ("/0/state", json!("failed")),
+ ("/0/program_sha256", json!("invalid")),
+ ("/0/runtime_sha256", json!(["invalid"])),
+ ("/0/arguments_sha256", json!("invalid")),
+ ("/0/environment_sha256", json!("invalid")),
+ ("/0/working_directory_sha256", json!("invalid")),
+ ("/0/working_directory/logical_uri", json!("untrusted")),
+ ("/0/path_binding/0/logical_role", json!("..")),
+ ("/0/path_binding/0/identity_sha256", json!("invalid")),
+ ("/0/stdin_closed", json!(false)),
+ ("/0/deadline_seconds", json!(0)),
+ ("/0/started_at_epoch", json!(0)),
+ ("/0/completed_at_epoch", json!(0)),
+ ("/0/completed_at_epoch", json!(u64::MAX)),
+ ("/1/started_at_epoch", json!(1)),
+ ("/0/stdout_byte_length", json!(67_108_865u64)),
+ ("/0/stderr_byte_length", json!(67_108_865u64)),
+ ("/0/stdout_sha256", json!("invalid")),
+ ("/0/stderr_sha256", json!("invalid")),
+ ("/0/input_sha256", json!("invalid")),
+ ("/0/output_sha256", json!("invalid")),
+ ("/0/environment/0/name", json!("UNTRUSTED")),
+ ("/0/environment/0/logical_value", json!("untrusted")),
+ ("/0/environment/0/value_sha256", json!("invalid")),
+ ] {
+ let mut changed = original.clone();
+ *changed.pointer_mut(pointer).expect(pointer) = replacement;
+ let receipts: Vec<TrustedProcessReceipt> = serde_json::from_value(changed).unwrap();
+ assert!(
+ validate_process_receipts(&receipts, fixture.request.evaluation_epoch).is_err(),
+ "{pointer}"
+ );
+ }
+ let mut duplicate_binding = fixture.process_receipts.clone();
+ let repeated = duplicate_binding[0].path_binding[0].clone();
+ duplicate_binding[0].path_binding.push(repeated);
+ assert!(
+ validate_process_receipts(&duplicate_binding, fixture.request.evaluation_epoch)
+ .is_err()
+ );
+ for state in [
+ OperationState::Failed,
+ OperationState::TimedOut,
+ OperationState::Unavailable,
+ ] {
+ assert!(require_complete(state).is_err());
+ }
+ }
+
+ #[test]
+ fn bounded_tokens_dates_and_canonical_counters_accept_only_their_grammar() {
+ for value in [
+ "",
+ "01",
+ "-1",
+ "+1",
+ "1.0",
+ "1e2",
+ " 1",
+ "Ù¡",
+ "18446744073709551616",
+ ] {
+ assert_eq!(parse_canonical_u64(value), None, "{value}");
+ }
+ assert_eq!(parse_canonical_u64("0"), Some(0));
+ assert_eq!(parse_canonical_u64("18446744073709551615"), Some(u64::MAX));
+ for value in [
+ "",
+ "x",
+ "RUSTSEC-2026",
+ "RUSTSEC-26-0001",
+ "RUSTSEC-202x-0001",
+ "RUSTSEC-2026-abcd",
+ "CVE-2026-0001",
+ ] {
+ assert!(!valid_advisory_id(ProviderId::Rustsec, value), "{value}");
+ }
+ assert!(valid_advisory_id(ProviderId::OwaspNvd, "CVE-2026-12345"));
+ assert!(valid_hex("0123456789abcdef", 16));
+ for value in ["G", "g", "é", "\0"] {
+ assert!(!valid_hex(value, value.len()));
+ }
+ assert!(valid_identifier("relative/normal"));
+ for value in [
+ "",
+ "/absolute",
+ "../parent",
+ "a/../b",
+ "a\\b",
+ "a\0b",
+ "a\nb",
+ ] {
+ assert!(!valid_identifier(value), "{value:?}");
+ }
+ assert!(valid_exact_identity_token("a-Z_1./:@+", 32));
+ for value in ["..", "with space", "é", "\n", "long"] {
+ assert!(!valid_exact_identity_token(value, 3));
+ }
+ assert!(valid_ascii_text("", 0, true));
+ assert!(valid_ascii_text("abc", 3, false));
+ for value in ["", "abcd", "é", "a\nb"] {
+ assert!(!valid_ascii_text(value, 3, false));
+ }
+ assert!(valid_exact_string_array(&json!(["abc"]), 1, 3));
+ for value in [
+ json!(["a", "b"]),
+ json!(["abcd"]),
+ json!([""]),
+ json!([null]),
+ json!({}),
+ ] {
+ assert!(!valid_exact_string_array(&value, 1, 3));
+ }
+ assert!(require_strict_canonical_order(&[1, 2]).is_ok());
+ assert!(require_strict_canonical_order(&[2, 1]).is_err());
+ assert!(require_strict_canonical_order(&[1, 1]).is_err());
+ }
+
+ #[test]
+ fn owasp_optional_rows_are_validated_and_duplicates_are_rejected() {
+ let identifiers = json!([{"id":"cpe:/a:example:library:1.0", "notes":"note", "url":"https://example.test", "confidence":"HIGH"}]);
+ rejects_wrong_node_types(&identifiers, |value| {
+ parse_owasp_identifiers(Some(value), true).is_ok()
+ });
+ assert!(parse_owasp_identifiers(Some(&identifiers), false).is_err());
+ assert!(parse_owasp_identifiers(None, false).unwrap().is_empty());
+ let included = json!([{"reference":"project:root", "type":"DIRECT"}]);
+ rejects_wrong_node_types(&included, |value| {
+ parse_owasp_included_by(Some(value)).is_ok()
+ });
+ rejects_unknown_object_fields(&included, &["/0"], |value| {
+ parse_owasp_included_by(Some(value)).is_ok()
+ });
+ assert!(parse_owasp_included_by(None).unwrap().is_empty());
+ for value in [
+ json!([]),
+ json!([{"id":"b"},{"id":"a"}]),
+ json!([{"id":"a"},{"id":"a"}]),
+ ] {
+ assert!(parse_owasp_identifiers(Some(&value), false).is_err());
+ }
+ for value in [
+ json!([]),
+ json!([{"reference":"b"},{"reference":"a"}]),
+ json!([{"reference":"a"},{"reference":"a"}]),
+ ] {
+ assert!(parse_owasp_included_by(Some(&value)).is_err());
+ }
+ let mut evidence = json!({});
+ for (key, kind) in [
+ ("productEvidence", "product"),
+ ("vendorEvidence", "vendor"),
+ ("versionEvidence", "version"),
+ ] {
+ evidence[key] = json!([{"confidence":"HIGH", "name":"name", "source":"manifest", "type":kind, "value":"value"}]);
+ }
+ rejects_wrong_node_types(&evidence, valid_owasp_evidence);
+ rejects_unknown_object_fields(
+ &evidence,
+ &[
+ "",
+ "/productEvidence/0",
+ "/vendorEvidence/0",
+ "/versionEvidence/0",
+ ],
+ valid_owasp_evidence,
+ );
+ evidence["productEvidence"][0]["type"] = json!("vendor");
+ assert!(!valid_owasp_evidence(&evidence));
+ let references = json!([{"source":"NVD", "name":"advisory", "url":"https://example.test"}]);
+ rejects_wrong_node_types(&references, valid_owasp_references);
+ rejects_unknown_object_fields(&references, &["/0"], valid_owasp_references);
+ assert!(valid_owasp_references(&json!([])));
+ let software = json!([{"software":{
+ "id":"cpe:/a:example:library:1.0", "versionEndExcluding":"2.0", "versionEndIncluding":"1.9",
+ "versionStartExcluding":"0.1", "versionStartIncluding":"0.2", "vulnerabilityIdMatched":"true", "vulnerable":"false"
+ }}]);
+ rejects_wrong_node_types(&software, |value| {
+ parse_owasp_vulnerable_software(value).is_ok()
+ });
+ rejects_unknown_object_fields(&software, &["/0", "/0/software"], |value| {
+ parse_owasp_vulnerable_software(value).is_ok()
+ });
+ let mut duplicated = software.clone();
+ duplicated.as_array_mut().unwrap().push(software[0].clone());
+ assert!(parse_owasp_vulnerable_software(&duplicated).is_err());
+ for (key, value) in [
+ ("vulnerabilityIdMatched", "false"),
+ ("vulnerable", "true"),
+ ("id", "invalid"),
+ ] {
+ let mut changed = software.clone();
+ changed[0]["software"][key] = json!(value);
+ assert!(parse_owasp_vulnerable_software(&changed).is_err());
+ }
+ let vulnerabilities = json!([{
+ "name":"CVE-2099-0001", "description":"description", "notes":"notes", "references":references,
+ "source":"NVD", "severity":"HIGH", "unscored":"true", "cwes":["CWE-20"], "vulnerableSoftware":software,
+ "cvssv2": {"accessComplexity":"LOW", "accessVector":"NETWORK", "authenticationr":"NONE", "availabilityImpact":"PARTIAL", "confidentialityImpact":"PARTIAL", "integrityImpact":"PARTIAL", "score":7.5, "severity":"HIGH"},
+ "cvssv3": {"attackComplexity":"LOW", "attackVector":"NETWORK", "availabilityImpact":"HIGH", "baseScore":9.8, "baseSeverity":"CRITICAL", "confidentialityImpact":"HIGH", "integrityImpact":"HIGH", "privilegesRequired":"NONE", "scope":"UNCHANGED", "userInteraction":"NONE"},
+ "cvssv4": {"baseScore":9.3, "environmentalScore":9.0, "threatScore":8.0, "version":"4.0"}
+ }]);
+ rejects_wrong_node_types(&vulnerabilities, |value| {
+ parse_owasp_vulnerabilities(Some(value)).is_ok()
+ });
+ rejects_unknown_object_fields(
+ &vulnerabilities,
+ &["/0", "/0/cvssv2", "/0/cvssv3", "/0/cvssv4"],
+ |value| parse_owasp_vulnerabilities(Some(value)).is_ok(),
+ );
+ let mut unsorted_cwes = vulnerabilities.clone();
+ unsorted_cwes[0]["cwes"] = json!(["CWE-99", "CWE-20"]);
+ assert!(parse_owasp_vulnerabilities(Some(&unsorted_cwes)).is_err());
+ assert!(parse_owasp_vulnerabilities(None).unwrap().is_empty());
+ for (key, value) in [
+ ("name", "RUSTSEC-2099-0001"),
+ ("source", "OTHER"),
+ ("unscored", "false"),
+ ] {
+ let mut changed = vulnerabilities.clone();
+ changed[0][key] = json!(value);
+ assert!(parse_owasp_vulnerabilities(Some(&changed)).is_err());
+ }
+ let mut duplicated = vulnerabilities.clone();
+ duplicated
+ .as_array_mut()
+ .unwrap()
+ .push(vulnerabilities[0].clone());
+ assert!(parse_owasp_vulnerabilities(Some(&duplicated)).is_err());
+ }
+
+ #[test]
+ fn owasp_related_artifacts_bind_file_digests_and_virtual_lineage() {
+ let physical = json!([{"fileName":"example.jar", "filePath":"/fixture/example.jar", "isVirtual":false,
+ "md5":"1".repeat(32), "sha1":"2".repeat(40), "sha256":"3".repeat(64),
+ "packageIds":[{"id":"pkg:maven/example/library@1.0"}]}]);
+ let expected = BTreeMap::new();
+ rejects_wrong_node_types(&physical, |value| {
+ parse_owasp_related(Some(value), &expected).is_ok()
+ });
+ rejects_unknown_object_fields(&physical, &["/0"], |value| {
+ parse_owasp_related(Some(value), &expected).is_ok()
+ });
+ let virtual_row =
+ json!([{"fileName":"project", "filePath":"project:root", "isVirtual":true}]);
+ rejects_wrong_node_types(&virtual_row, |value| {
+ parse_owasp_related(Some(value), &expected).is_ok()
+ });
+ for key in ["md5", "sha1", "sha256"] {
+ let mut changed = physical.clone();
+ changed[0][key] = json!("invalid");
+ assert!(parse_owasp_related(Some(&changed), &expected).is_err());
+ let mut changed = virtual_row.clone();
+ changed[0][key] = json!("invalid");
+ assert!(parse_owasp_related(Some(&changed), &expected).is_err());
+ }
+ let mut repeated = physical.clone();
+ repeated.as_array_mut().unwrap().push(physical[0].clone());
+ assert!(parse_owasp_related(Some(&repeated), &expected).is_err());
+ assert!(
+ parse_owasp_related(None, &expected)
+ .unwrap()
+ .aliases
+ .is_empty()
+ );
+ for value in [
+ "",
+ "pkg:cargo/example@1.0",
+ "pkg:maven/example",
+ "pkg:maven/example@1.0",
+ "pkg:maven//example@1.0",
+ "pkg:maven/example/library@",
+ "pkg:maven/example/library@1.0?x",
+ "pkg:maven/example/library%20@1.0",
+ ] {
+ assert!(parse_exact_package_url(value).is_err(), "{value}");
+ }
+ assert_eq!(
+ parse_exact_package_url("pkg:maven/example/library@1.0").unwrap(),
+ (
+ "maven".into(),
+ "example".into(),
+ "library".into(),
+ "1.0".into()
+ )
+ );
+ }
+
+ #[test]
+ fn nvd_pagination_requires_complete_contiguous_pages_and_time_windows() {
+ let fixture = SyntheticFixture::new().unwrap();
+ let producer: TrustedProducerRequest =
+ parse_canonical_authority(&fixture.request.producer_request).unwrap();
+ let baseline: NvdNetworkTrace =
+ parse_canonical_authority(&fixture.request.nvd_network_trace).unwrap();
+ let receipts = &fixture.process_receipts;
+ validate_nvd_trace(&producer, &baseline, receipts).unwrap();
+ let original = serde_json::to_value(&baseline).unwrap();
+ for (pointer, replacement) in [
+ ("/request/0/sequence", json!(0)),
+ ("/request/0/method", json!("POST")),
+ ("/request/0/scheme", json!("http")),
+ ("/request/0/authority", json!("example.test")),
+ ("/request/0/path", json!("/other")),
+ ("/request/0/started_at_epoch", json!(0)),
+ ("/request/0/completed_at_epoch", json!(0)),
+ ("/request/0/completed_at_epoch", json!(u64::MAX)),
+ ("/request/0/response_status", json!(500)),
+ ("/request/0/response_byte_length", json!(0)),
+ (
+ "/request/0/response_byte_length",
+ json!(MAX_NVD_AGGREGATE_BYTES + 1),
+ ),
+ (
+ "/request/0/response_byte_length",
+ json!(MAX_NVD_RESPONSE_BYTES + 1),
+ ),
+ ("/request/0/response_sha256", json!("invalid")),
+ ("/request/0/response_start_index", json!(1)),
+ ("/request/0/response_results_per_page", json!(0)),
+ ("/request/0/response_total_results", json!(0)),
+ ] {
+ let mut changed = original.clone();
+ *changed.pointer_mut(pointer).unwrap() = replacement;
+ let trace: NvdNetworkTrace = serde_json::from_value(changed).unwrap();
+ assert!(
+ validate_nvd_trace(&producer, &trace, receipts).is_err(),
+ "{pointer}"
+ );
+ }
+ for key in [
+ "lastModStartDate",
+ "lastModEndDate",
+ "startIndex",
+ "resultsPerPage",
+ ] {
+ let mut changed = baseline.clone();
+ changed.request[0]
+ .query
+ .iter_mut()
+ .find(|row| row.name == key)
+ .unwrap()
+ .value = "invalid".into();
+ assert!(
+ validate_nvd_trace(&producer, &changed, receipts).is_err(),
+ "{key}"
+ );
+ }
+ let set_query = |row: &mut NvdRequestTrace, name: &str, value: String| {
+ row.query
+ .iter_mut()
+ .find(|query| query.name == name)
+ .unwrap()
+ .value = value;
+ };
+ let mut pages = baseline.clone();
+ assert_eq!(pages.request.len(), 1);
+ let page_size = pages.request[0].response_results_per_page;
+ pages.request[0].response_total_results = page_size + 1;
+ let mut second = pages.request[0].clone();
+ second.sequence = 2;
+ second.started_at_epoch = second.completed_at_epoch;
+ second.response_start_index = page_size;
+ set_query(&mut second, "startIndex", page_size.to_string());
+ pages.request.push(second);
+ validate_nvd_trace(&producer, &pages, receipts).unwrap();
+ let mut total_drift = pages.clone();
+ total_drift.request[1].response_total_results += 1;
+ assert!(validate_nvd_trace(&producer, &total_drift, receipts).is_err());
+ let mut backwards_time = pages.clone();
+ backwards_time.request[1].started_at_epoch = backwards_time.request[0].started_at_epoch;
+ assert!(validate_nvd_trace(&producer, &backwards_time, receipts).is_err());
+ let mut initial_offset = baseline.clone();
+ initial_offset.request[0].response_start_index = 1;
+ set_query(&mut initial_offset.request[0], "startIndex", "1".into());
+ assert!(validate_nvd_trace(&producer, &initial_offset, receipts).is_err());
+ let mut extra_query = baseline.clone();
+ let repeated_query = extra_query.request[0].query[0].clone();
+ extra_query.request[0].query.push(repeated_query);
+ assert!(validate_nvd_trace(&producer, &extra_query, receipts).is_err());
+ let mut reordered = baseline.clone();
+ reordered.request[0].query.swap(0, 1);
+ assert!(validate_nvd_trace(&producer, &reordered, receipts).is_err());
+ let mut truncated = pages.clone();
+ truncated.request.pop();
+ assert!(validate_nvd_trace(&producer, &truncated, receipts).is_err());
+ for gap in [0, page_size + 1] {
+ let mut changed = pages.clone();
+ changed.request[1].response_start_index = gap;
+ set_query(&mut changed.request[1], "startIndex", gap.to_string());
+ assert!(validate_nvd_trace(&producer, &changed, receipts).is_err());
+ }
+ let mut windows = baseline.clone();
+ let end = windows.request[0]
+ .query
+ .iter()
+ .find(|row| row.name == "lastModEndDate")
+ .unwrap()
+ .value
+ .clone();
+ let next_start = parse_report_epoch(&end).unwrap() + 1;
+ let mut second = windows.request[0].clone();
+ second.sequence = 2;
+ second.started_at_epoch = second.completed_at_epoch;
+ set_query(
+ &mut second,
+ "lastModStartDate",
+ format_report_epoch(next_start).unwrap(),
+ );
+ set_query(
+ &mut second,
+ "lastModEndDate",
+ format_report_epoch(next_start + 1).unwrap(),
+ );
+ windows.request.push(second);
+ validate_nvd_trace(&producer, &windows, receipts).unwrap();
+ let mut offset_window = windows.clone();
+ offset_window.request[1].response_start_index = 1;
+ set_query(&mut offset_window.request[1], "startIndex", "1".into());
+ assert!(validate_nvd_trace(&producer, &offset_window, receipts).is_err());
+ let mut reversed_window = windows.clone();
+ set_query(
+ &mut reversed_window.request[1],
+ "lastModStartDate",
+ format_report_epoch(next_start + 2).unwrap(),
+ );
+ assert!(validate_nvd_trace(&producer, &reversed_window, receipts).is_err());
+ set_query(
+ &mut windows.request[1],
+ "lastModStartDate",
+ format_report_epoch(next_start + 1).unwrap(),
+ );
+ assert!(validate_nvd_trace(&producer, &windows, receipts).is_err());
+ }
+}
diff --git a/tools/xtask/src/artifact_admission.rs b/tools/xtask/src/artifact_admission.rs
@@ -177,7 +177,9 @@ fn admit_binary_bytes(bytes: &[u8], target: &str) -> Result<(), AdmissionError>
Object::parse(bytes).map_err(|_| AdmissionError::InvalidBinary)?,
) {
(LINUX_TARGET, Object::Elf(binary)) => {
- if binary.header.e_machine != EM_X86_64
+ if !binary.is_64
+ || !binary.little_endian
+ || binary.header.e_machine != EM_X86_64
|| !matches!(
binary.header.e_type,
goblin::elf::header::ET_EXEC | goblin::elf::header::ET_DYN
@@ -827,6 +829,279 @@ fn sha256(bytes: &[u8]) -> String {
mod tests {
use super::*;
+ // Synthetic ELF headers with one executable PT_LOAD segment. These bytes
+ // exercise format admission only; no fixture is executed or released.
+ fn elf_fixture(is_64: bool, little_endian: bool) -> Vec<u8> {
+ fn put(bytes: &mut [u8], offset: usize, width: usize, value: u64, little: bool) {
+ let encoded = if little {
+ value.to_le_bytes()
+ } else {
+ value.to_be_bytes()
+ };
+ let source = if little {
+ &encoded[..width]
+ } else {
+ &encoded[8 - width..]
+ };
+ bytes[offset..offset + width].copy_from_slice(source);
+ }
+ let header = if is_64 { 64 } else { 52 };
+ let program = if is_64 { 56 } else { 32 };
+ let mut bytes = vec![0; header + program + 8];
+ bytes[..4].copy_from_slice(goblin::elf::header::ELFMAG);
+ bytes[4] = if is_64 { 2 } else { 1 };
+ bytes[5] = if little_endian { 1 } else { 2 };
+ bytes[6] = 1;
+ let length = bytes.len() as u64;
+ let word = if is_64 { 8 } else { 4 };
+ for (offset, width, value) in [
+ (16, 2, 2),
+ (18, 2, u64::from(EM_X86_64)),
+ (20, 4, 1),
+ (24, word, 0x400000 + (header + program) as u64),
+ (24 + word, word, header as u64),
+ (if is_64 { 52 } else { 40 }, 2, header as u64),
+ (if is_64 { 54 } else { 42 }, 2, program as u64),
+ (if is_64 { 56 } else { 44 }, 2, 1),
+ (header, 4, 1),
+ (header + if is_64 { 4 } else { 24 }, 4, 5),
+ (header + if is_64 { 16 } else { 8 }, word, 0x400000),
+ (header + if is_64 { 32 } else { 16 }, word, length),
+ (header + if is_64 { 40 } else { 20 }, word, length),
+ ] {
+ put(&mut bytes, offset, width, value, little_endian);
+ }
+ bytes
+ }
+
+ #[test]
+ fn linux_format_requires_both_64_bit_class_and_little_endian_encoding() {
+ let expected = expected_contract();
+ assert_eq!(
+ expected["binary"]["formats"][LINUX_TARGET],
+ "elf64_little_endian_x86_64_execute_or_pie"
+ );
+ assert!(admit_binary_bytes(&elf_fixture(true, true), LINUX_TARGET).is_ok());
+ let results = [(false, true), (true, false)].map(|(class, endian)| {
+ let bytes = elf_fixture(class, endian);
+ let Object::Elf(parsed) = Object::parse(&bytes).unwrap() else {
+ panic!("ELF fixture");
+ };
+ assert_eq!(parsed.is_64, class);
+ assert_eq!(parsed.little_endian, endian);
+ admit_binary_bytes(&bytes, LINUX_TARGET)
+ });
+ assert_eq!(results, [Err(AdmissionError::InvalidBinary); 2]);
+ }
+
+ #[test]
+ fn mach_executable_admission_binds_machine_type_entry_and_executable_segment() {
+ let mut original = vec![0u8; 136];
+ for (offset, value) in [
+ (0, 0xfeedfacfu32),
+ (4, CPU_TYPE_ARM64),
+ (12, MH_EXECUTE),
+ (16, 2),
+ (20, 96),
+ (32, 0x19),
+ (36, 72),
+ (88, 5),
+ (92, 5),
+ (104, 0x80000028),
+ (108, 24),
+ ] {
+ original[offset..offset + 4].copy_from_slice(&value.to_le_bytes());
+ }
+ original[40..46].copy_from_slice(b"__TEXT");
+ for (offset, value) in [(56, 0x100000000u64), (64, 136), (80, 136), (112, 128)] {
+ original[offset..offset + 8].copy_from_slice(&value.to_le_bytes());
+ }
+ assert!(admit_binary_bytes(&original, MACOS_TARGET).is_ok());
+ for (offset, bytes) in [
+ (4, 7u32.to_le_bytes().to_vec()),
+ (12, 6u32.to_le_bytes().to_vec()),
+ (92, 1u32.to_le_bytes().to_vec()),
+ (112, 136u64.to_le_bytes().to_vec()),
+ ] {
+ let mut changed = original.clone();
+ changed[offset..offset + bytes.len()].copy_from_slice(&bytes);
+ assert_eq!(
+ admit_binary_bytes(&changed, MACOS_TARGET),
+ Err(AdmissionError::InvalidBinary),
+ "offset {offset}"
+ );
+ }
+ let mut no_entry = original;
+ no_entry[56..64].fill(0);
+ no_entry[112..120].fill(0);
+ assert_eq!(
+ admit_binary_bytes(&no_entry, MACOS_TARGET),
+ Err(AdmissionError::InvalidBinary)
+ );
+ }
+
+ #[test]
+ fn linux_entrypoint_must_belong_to_an_executable_segment_of_the_exact_machine() {
+ let original = elf_fixture(true, true);
+ for (offset, replacement) in [
+ (16, 1u16.to_le_bytes().to_vec()),
+ (18, 183u16.to_le_bytes().to_vec()),
+ (24, 0u64.to_le_bytes().to_vec()),
+ (24, 0x3fffffu64.to_le_bytes().to_vec()),
+ (
+ 24,
+ (0x400000u64 + original.len() as u64).to_le_bytes().to_vec(),
+ ),
+ (68, 4u32.to_le_bytes().to_vec()),
+ ] {
+ let mut invalid = original.clone();
+ invalid[offset..offset + replacement.len()].copy_from_slice(&replacement);
+ assert_eq!(
+ admit_binary_bytes(&invalid, LINUX_TARGET),
+ Err(AdmissionError::InvalidBinary),
+ "header offset {offset}"
+ );
+ }
+ let mut pie = original;
+ pie[16..18].copy_from_slice(&goblin::elf::header::ET_DYN.to_le_bytes());
+ assert!(admit_binary_bytes(&pie, LINUX_TARGET).is_ok());
+ assert!(admit_binary_bytes(&pie, MACOS_TARGET).is_err());
+ assert!(admit_binary_inner(Path::new("/absent"), "unknown", false).is_err());
+ for libraries in [
+ vec![String::new()],
+ vec!["x".repeat(1025)],
+ vec!["lib\nname".into()],
+ vec!["SQLite.DLL".into()],
+ ] {
+ assert_eq!(
+ validate_dynamic_libraries(&libraries),
+ Err(AdmissionError::InvalidBinary)
+ );
+ }
+ assert!(validate_dynamic_libraries(&["libc.so.6".into()]).is_ok());
+ }
+
+ #[test]
+ fn archive_paths_and_links_enforce_byte_depth_and_traversal_bounds() {
+ let exact_depth = vec!["a"; MAX_DEPTH].join("/");
+ let excessive_depth = vec!["a"; MAX_DEPTH + 1].join("/");
+ assert!(validate_relative_path(exact_depth.as_bytes()).is_ok());
+ assert!(validate_relative_path(&vec![b'a'; MAX_PATH_BYTES]).is_ok());
+ for path in [
+ b"".to_vec(),
+ b"/absolute".to_vec(),
+ b"a\0b".to_vec(),
+ b"a\\b".to_vec(),
+ vec![0xff],
+ b"a//b".to_vec(),
+ b"a/./b".to_vec(),
+ b"a/../b".to_vec(),
+ vec![b'a'; MAX_PATH_BYTES + 1],
+ excessive_depth.into_bytes(),
+ ] {
+ assert!(validate_relative_path(&path).is_err(), "{path:?}");
+ }
+ assert!(validate_relative_path(b"directory/").is_ok());
+ assert!(validate_link_target(b"a/link", b"./../target").is_ok());
+ assert!(validate_link_target(b"a/link", b"target//leaf").is_ok());
+ for target in [
+ b"".to_vec(),
+ b"/absolute".to_vec(),
+ b"a\0b".to_vec(),
+ b"a\\b".to_vec(),
+ vec![0xff],
+ vec![b'a'; MAX_PATH_BYTES + 1],
+ vec!["a"; MAX_DEPTH + 1].join("/").into_bytes(),
+ ] {
+ assert!(
+ validate_link_target(b"a/link", &target).is_err(),
+ "{target:?}"
+ );
+ }
+ for path in [
+ "layer.tar".to_owned(),
+ "bad/layer.tar".to_owned(),
+ format!("{}/other.tar", "a".repeat(64)),
+ format!("{}/nested/layer.tar", "a".repeat(64)),
+ ] {
+ assert!(validate_layer_name(&path).is_err());
+ }
+ assert!(validate_layer_name(&format!("{}/layer.tar", "a".repeat(64))).is_ok());
+ }
+
+ #[test]
+ fn oci_runtime_config_binds_rootless_identity_entrypoint_and_every_label() {
+ let expected = OciExpectation {
+ service: "fixture_service",
+ binary_name: "fixture-service",
+ version: "0.1.0-alpha",
+ service_revision: "1111111111111111111111111111111111111111",
+ lib_revision: "2222222222222222222222222222222222222222",
+ license: AGPL_LICENSE,
+ contract_versions: ContractVersions {
+ admin: 3,
+ config: 1,
+ provider: 5,
+ state: 2,
+ status: 4,
+ },
+ };
+ let entrypoint = "/nix/store/fixture/bin/fixture-service";
+ let original = json!({"architecture":"amd64", "os":"linux", "created":"1970-01-01T00:00:01+00:00",
+ "config":{"User":"65532:65532", "WorkingDir":"/", "StopSignal":"SIGTERM",
+ "Env":["SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt"], "Entrypoint":[entrypoint], "Labels":fixture_labels(&expected)}});
+ assert_eq!(validate_config(&original, &expected).unwrap(), entrypoint);
+ for (pointer, replacement) in [
+ ("", json!(null)),
+ ("/architecture", json!("arm64")),
+ ("/os", json!("macos")),
+ ("/created", json!("now")),
+ ("/config", json!(null)),
+ ("/config/User", json!("root")),
+ ("/config/WorkingDir", json!("/tmp")),
+ ("/config/StopSignal", json!("SIGKILL")),
+ ("/config/Env", json!([])),
+ ("/config/Env", json!([null])),
+ ("/config/Entrypoint", json!([])),
+ ("/config/Entrypoint", json!([entrypoint, entrypoint])),
+ ("/config/Entrypoint", json!(["/bin/fixture-service"])),
+ (
+ "/config/Entrypoint",
+ json!(["/nix/store/fixture/bin/other"]),
+ ),
+ (
+ "/config/Entrypoint",
+ json!(["/nix/store/../bin/fixture-service"]),
+ ),
+ ("/config/Labels", json!(null)),
+ ("/config/Labels", json!({})),
+ ] {
+ let mut changed = original.clone();
+ *changed.pointer_mut(pointer).unwrap() = replacement;
+ assert!(validate_config(&changed, &expected).is_err(), "{pointer}");
+ }
+ for key in original["config"]["Labels"].as_object().unwrap().keys() {
+ let mut changed = original.clone();
+ changed["config"]["Labels"][key] = json!("unbound");
+ assert!(validate_config(&changed, &expected).is_err(), "label {key}");
+ }
+ for value in ["", "A", "0a", "a-b", "a/b"] {
+ assert!(!valid_identifier(value));
+ }
+ assert!(valid_identifier("service_1"));
+ assert!(valid_binary_name("service-1"));
+ for value in ["", "A", "0a", "a/b"] {
+ assert!(!valid_binary_name(value));
+ }
+ assert!(!valid_identifier(&"a".repeat(129)));
+ assert!(!valid_binary_name(&"a".repeat(129)));
+ assert!(!valid_absolute_path("relative"));
+ assert!(!valid_absolute_path(&format!(
+ "/{}",
+ "a".repeat(MAX_PATH_BYTES)
+ )));
+ }
+
#[test]
fn contract_is_exact() {
let root = Path::new(env!("CARGO_MANIFEST_DIR"))
diff --git a/tools/xtask/src/bounded_process.rs b/tools/xtask/src/bounded_process.rs
@@ -899,6 +899,79 @@ mod unix {
(false, false) => unreachable!("output stop requires an exceeded stream"),
}
}
+
+ #[cfg(test)]
+ mod stream_tests {
+ use super::*;
+ use std::collections::VecDeque;
+
+ #[test]
+ fn draining_preserves_interrupted_and_nonblocking_streams_but_closes_failures() {
+ struct ScriptedReader(VecDeque<Result<Vec<u8>, io::ErrorKind>>);
+ impl Read for ScriptedReader {
+ fn read(&mut self, output: &mut [u8]) -> io::Result<usize> {
+ match self.0.pop_front().unwrap_or(Ok(Vec::new())) {
+ Ok(bytes) => {
+ output[..bytes.len()].copy_from_slice(&bytes);
+ Ok(bytes.len())
+ }
+ Err(kind) => Err(io::Error::new(kind, "fixture read failure")),
+ }
+ }
+ }
+ let mut stream = Some(ScriptedReader(VecDeque::from([
+ Err(io::ErrorKind::Interrupted),
+ Ok(b"abc".to_vec()),
+ Err(io::ErrorKind::WouldBlock),
+ Err(io::ErrorKind::BrokenPipe),
+ ])));
+ let mut capture = Capture::new(8);
+ drain_stream(&mut stream, &mut capture).unwrap();
+ assert!(stream.is_some());
+ assert_eq!(capture.bytes, b"abc");
+ assert_eq!(
+ drain_stream(&mut stream, &mut capture).unwrap_err().kind(),
+ ProcessFailureKind::Read
+ );
+ assert!(stream.is_none());
+ drain_stream(&mut stream, &mut capture).unwrap();
+ assert_eq!(capture.bytes, b"abc");
+ }
+
+ #[test]
+ fn draining_has_a_fairness_budget_and_retains_only_the_configured_prefix() {
+ let mut stream = Some(io::Cursor::new(vec![b'x'; MAX_DRAIN_BYTES_PER_PASS + 1]));
+ let mut capture = Capture::new(MAX_DRAIN_BYTES_PER_PASS * 2);
+ drain_stream(&mut stream, &mut capture).unwrap();
+ assert_eq!(capture.bytes.len(), MAX_DRAIN_BYTES_PER_PASS);
+ assert!(stream.is_some());
+ drain_stream(&mut stream, &mut capture).unwrap();
+ assert!(stream.is_none());
+ assert_eq!(capture.bytes.len(), MAX_DRAIN_BYTES_PER_PASS + 1);
+ let mut stream = Some(io::Cursor::new(b"abcdef"));
+ let mut capture = Capture::new(3);
+ drain_stream(&mut stream, &mut capture).unwrap();
+ assert!(capture.exceeded);
+ assert_eq!(capture.bytes, b"abc");
+ drain_stream(&mut stream, &mut capture).unwrap();
+ assert!(stream.is_none());
+ assert_eq!(capture.bytes, b"abc");
+ }
+
+ #[test]
+ fn cleanup_keeps_the_original_error_when_later_cleanup_also_fails() {
+ let original = ProcessError::new(ProcessFailureKind::Read);
+ let mut first = None;
+ remember_first(&mut first, None);
+ assert!(first.is_none());
+ remember_first(&mut first, Some(original));
+ remember_first(
+ &mut first,
+ Some(ProcessError::new(ProcessFailureKind::InvalidConfiguration)),
+ );
+ assert_eq!(first.unwrap().kind(), ProcessFailureKind::Read);
+ }
+ }
}
pub(crate) fn self_test() -> Result<(), String> {
@@ -1232,6 +1305,26 @@ mod tests {
fn environment_boundaries_fail_closed() {
use std::os::unix::ffi::OsStringExt;
+ for name in [
+ "".to_owned(),
+ "A".repeat(MAX_ENVIRONMENT_NAME_BYTES + 1),
+ "A\0B".to_owned(),
+ "É".to_owned(),
+ "1INVALID".to_owned(),
+ "BAD-NAME".to_owned(),
+ ] {
+ assert_eq!(
+ environment_rejection(&name),
+ Some(EnvironmentRejection::InvalidNameOrNul)
+ );
+ }
+ assert_eq!(
+ environment_rejection("CARGO_TARGET_FIXTURE_RUSTFLAGS"),
+ Some(EnvironmentRejection::ForbiddenControl)
+ );
+ assert_eq!(environment_rejection("CARGO_TARGET_FIXTURE"), None);
+ assert_eq!(environment_rejection("_FIXTURE_1"), None);
+
let mut environment = ReplacementEnvironment::default();
environment
.insert("RSHR_VISIBLE", "first")
@@ -1283,6 +1376,8 @@ mod tests {
#[test]
fn hard_configuration_maximums_fail_before_spawn() {
for request in [
+ ProcessRequest::new(""),
+ ProcessRequest::new("/usr/bin/true").deadline(Duration::ZERO),
ProcessRequest::new("/usr/bin/true").deadline(MAX_DEADLINE + Duration::from_secs(1)),
ProcessRequest::new("/usr/bin/true")
.output_limits(MAX_STREAM_LIMIT + 1, MAX_STREAM_LIMIT),
diff --git a/tools/xtask/src/coverage.rs b/tools/xtask/src/coverage.rs
@@ -624,6 +624,9 @@ fn is_ignorable_detail_function(
}
fn scope_path_fragment(scope: &str) -> String {
+ if scope == "xtask" {
+ return "/tools/xtask/src/".to_owned();
+ }
let crate_dir = scope.strip_prefix("radroots_").unwrap_or(scope);
format!("/crates/{crate_dir}/src/")
}
@@ -1757,12 +1760,14 @@ fn run_crate_with_runner_at_root(
runner(
{
let mut cmd = coverage_llvm_cov_command();
- cmd.arg("clean")
- .arg("--workspace")
- .current_dir(workspace_root);
+ // Package cleanup leaves orphan executables under build output
+ // directories. LLVM also reads those coverage maps, so an earlier
+ // source revision can contaminate the current report even when its
+ // profile has been removed. Reset the owned coverage build tree.
+ cmd.arg("clean").current_dir(workspace_root);
cmd
},
- "cargo llvm-cov clean --workspace",
+ "cargo llvm-cov clean",
)?;
runner(
@@ -2627,6 +2632,67 @@ mod tests {
}
#[test]
+ fn detailed_xtask_report_measures_tool_source_instead_of_empty_perfect_totals() {
+ let root = temp_dir_path("details_xtask_scope");
+ let source_path = root.join("tools/xtask/src/main.rs");
+ write_file(
+ &source_path,
+ "fn used(flag: bool) { if flag { work(); } }\nfn missed() { work(); }\n",
+ );
+ let details = root.join("coverage-details.json");
+ let raw = serde_json::json!({"data": [{"functions": [
+ {
+ "count": 1,
+ "filenames": [source_path.display().to_string()],
+ "regions": [[1, 1, 1, 41, 1, 0, 0, 0]],
+ "branches": [[1, 25, 1, 29, 1, 0, 0, 0, 0]]
+ },
+ {
+ "count": 0,
+ "filenames": [source_path.display().to_string()],
+ "regions": [[2, 1, 2, 23, 0, 0, 0, 0]]
+ }
+ ]}]});
+ write_file(&details, &raw.to_string());
+ let measured = read_detailed_summary(&details, Some("xtask")).unwrap();
+ assert_eq!(measured.functions_percent, 50.0);
+ assert_eq!(measured.regions_percent, 50.0);
+ assert_eq!(measured.executable_lines.total, 2);
+ assert_eq!(measured.executable_lines.covered, 1);
+ assert_eq!(measured.branches.total, 2);
+ assert_eq!(measured.branches.covered, 1);
+ fs::remove_dir_all(root).unwrap();
+ }
+
+ #[test]
+ fn detailed_scope_separates_macro_files_and_preserves_existing_index_fallback() {
+ let root = temp_dir_path("details_mixed_source_maps");
+ let source = root.join("tools/xtask/src/main.rs");
+ let external = root.join("dependency/src/lib.rs");
+ write_file(
+ &source,
+ "fn measured() { work(); }\n#[cfg(test)]\nfn test_only() {}\n#[cfg(test)]\nconst TEST_ONLY: bool = true;\n",
+ );
+ write_file(&external, "fn external() {}\n");
+ let details = root.join("coverage-details.json");
+ let raw = serde_json::json!({"data":[{"functions":[
+ {"count":1,"filenames":[source,external],
+ "regions":[[1,1,1,25,1,0,0,0],[1,1,1,16,1,1,0,0],[1,1,1,2,1,99,0,0],[4,1,4,11,1,0,0,0],[1,1,1,2,1,0,0,1]],
+ "branches":[[1,17,1,21,1,1,0,0,0],[1,1,1,2,1,1,1,0,0],[1,1,1,2,1,1,99,0,0],[3,1,3,2,1,1,0,0,0]]},
+ {"count":1,"filenames":[source,external],"regions":[[1,1,1,16,1,1,0,0]],"branches":[]},
+ {"count":1,"filenames":[source],"regions":[[1,1,1,2,1,99,0,0]],"branches":[]}
+ ]}]});
+ write_file(&details, &raw.to_string());
+ let measured = read_detailed_summary(&details, Some("xtask")).unwrap();
+ assert_eq!(measured.functions_percent, 100.0);
+ assert_eq!(measured.executable_lines.covered, 1);
+ assert_eq!(measured.executable_lines.total, 1);
+ assert_eq!(measured.branches.covered, 4);
+ assert_eq!(measured.branches.total, 4);
+ fs::remove_dir_all(root).unwrap();
+ }
+
+ #[test]
fn read_summary_reports_read_and_parse_errors() {
let missing = temp_file_path("summary_missing");
let read_err = read_summary(&missing).expect_err("missing summary should fail");
@@ -2831,6 +2897,46 @@ pub fn production() {}
}
#[test]
+ fn lexical_brace_tracking_keeps_escaped_literals_and_pending_attributes_bounded() {
+ for literal in [r"'\n'", r"'\''", r"'\u{7d}'", "'é'"] {
+ assert_eq!(
+ char_literal_end(literal, 0),
+ Some(literal.len()),
+ "{literal}"
+ );
+ assert_eq!(source_brace_deltas(literal).collect::<Vec<_>>(), [0]);
+ }
+ for literal in ["'", r"'\", r"'\u{7d", r"'\uX'", "'ab'"] {
+ assert_eq!(char_literal_end(literal, 0), None, "{literal}");
+ }
+ let source = r####"fn boundary() {
+ let escaped = "\"}\\{";
+ let raw = r##"a"#} b"x { c"##;
+ let divided = 8 / 2;
+ /* / text * /* nested */ } */
+}
+"####;
+ assert_eq!(
+ source_brace_deltas(source).collect::<Vec<_>>(),
+ [1, 0, 0, 0, 0, -1]
+ );
+ for attribute in [
+ "#[cfg(test)]",
+ "#[cfg_attr(coverage_nightly, coverage(off))]",
+ ] {
+ let source = format!(
+ "{attribute}\n\n// reason\n#[allow(dead_code)]\nfn excluded\n() {{}}\nfn measured() {{}}\n"
+ );
+ let lines = if attribute == "#[cfg(test)]" {
+ cfg_test_source_lines(&source)
+ } else {
+ coverage_off_source_lines(&source)
+ };
+ assert_eq!(lines, [true, true, true, true, true, true, false]);
+ }
+ }
+
+ #[test]
fn coverage_off_source_lines_cover_only_the_annotated_item() {
let source = "#[cfg_attr(coverage_nightly, coverage(off))]\npub fn glue(\n enabled: bool,\n) -> bool {\n if enabled { true } else { false }\n}\npub fn policy() -> bool { true }\n";
let lines = coverage_off_source_lines(source);
@@ -4610,13 +4716,14 @@ test_threads = 0
assert_eq!(
names,
vec![
- "cargo llvm-cov clean --workspace".to_string(),
+ "cargo llvm-cov clean".to_string(),
"cargo llvm-cov --no-report".to_string(),
"cargo llvm-cov report --json --summary-only".to_string(),
"cargo llvm-cov report --json".to_string(),
"cargo llvm-cov report --lcov".to_string(),
]
);
+ assert!(rendered_commands[0].ends_with("llvm-cov clean"));
assert!(
rendered_commands
.iter()
diff --git a/tools/xtask/src/exact_tree_archive.rs b/tools/xtask/src/exact_tree_archive.rs
@@ -409,6 +409,133 @@ mod tests {
}
#[test]
+ fn archive_verification_rejects_metadata_inventory_and_payload_tampering() {
+ let payload = b"source\n";
+ let expected = || {
+ vec![WrittenMember {
+ path: "source.rs".to_owned(),
+ mode: 0o644,
+ byte_length: payload.len() as u64,
+ sha256: hex::encode(Sha256::digest(payload)),
+ }]
+ };
+ let bytes = |change: fn(&mut Header)| {
+ let mut header = Header::new_ustar();
+ header.set_path("source.rs").unwrap();
+ header.set_mode(0o644);
+ header.set_uid(0);
+ header.set_gid(0);
+ header.set_mtime(123);
+ header.set_size(payload.len() as u64);
+ header.set_entry_type(EntryType::Regular);
+ header.set_username("").unwrap();
+ header.set_groupname("").unwrap();
+ change(&mut header);
+ header.set_cksum();
+ let mut archive = Builder::new(Vec::new());
+ archive.append(&header, &payload[..]).unwrap();
+ archive.into_inner().unwrap()
+ };
+ let original = bytes(|_| {});
+ validate_archive_bytes(&original, &expected(), 123).unwrap();
+ let metadata_changes: [fn(&mut Header); 7] = [
+ |header| header.set_entry_type(EntryType::Directory),
+ |header| header.set_uid(1),
+ |header| header.set_gid(1),
+ |header| header.set_mtime(124),
+ |header| header.set_username("user").unwrap(),
+ |header| header.set_groupname("group").unwrap(),
+ |header| header.set_mode(0o755),
+ ];
+ for change in metadata_changes {
+ assert!(validate_archive_bytes(&bytes(change), &expected(), 123).is_err());
+ }
+ let inventory_changes: [fn(&mut WrittenMember); 4] = [
+ |member| member.path = "other.rs".to_owned(),
+ |member| member.mode = 0o755,
+ |member| member.byte_length += 1,
+ |member| member.sha256 = "0".repeat(64),
+ ];
+ for change in inventory_changes {
+ let mut altered = expected();
+ change(&mut altered[0]);
+ assert!(validate_archive_bytes(&original, &altered, 123).is_err());
+ }
+ let mut altered = original.clone();
+ altered[512] ^= 1;
+ assert!(validate_archive_bytes(&altered, &expected(), 123).is_err());
+ let mut altered = original.clone();
+ *altered.last_mut().unwrap() = 1;
+ assert!(validate_archive_bytes(&altered, &expected(), 123).is_err());
+ assert!(validate_archive_bytes(&original[..original.len() - 1], &expected(), 123).is_err());
+ assert!(validate_archive_bytes(&[], &expected(), 123).is_err());
+ assert!(validate_archive_bytes(&original, &[], 123).is_err());
+ let mut overflow = expected();
+ overflow[0].byte_length = u64::MAX;
+ assert!(validate_archive_bytes(&original, &overflow, 123).is_err());
+ }
+
+ #[test]
+ fn exact_blob_reads_bind_revision_path_and_output_limit() {
+ let (_fixture, root, revision) = fixture();
+ assert_eq!(
+ read_blob(&root, &revision, "alpha.txt", 6).unwrap(),
+ b"alpha\n"
+ );
+ assert!(read_blob(&root, &revision, "alpha.txt", 5).is_err());
+ assert!(read_blob(&root, &revision, "absent", 6).is_err());
+ for maximum in [0, MAX_SOURCE_MEMBER_BYTES + 1] {
+ assert!(read_blob(&root, &revision, "alpha.txt", maximum).is_err());
+ }
+ for path in [
+ "",
+ "/absolute",
+ "../escape",
+ "a\\b",
+ "a\nb",
+ "a\rb",
+ "a\0b",
+ ".",
+ &"x".repeat(MAX_USTAR_PATH_BYTES + 1),
+ ] {
+ assert!(read_blob(&root, &revision, path, 10).is_err(), "{path:?}");
+ }
+ for invalid in ["short", &"G".repeat(40), &"0".repeat(40)] {
+ assert!(read_blob(&root, invalid, "alpha.txt", 10).is_err());
+ }
+ assert!(read_blob(Path::new("relative"), &revision, "alpha.txt", 10).is_err());
+ assert!(read_blob(&root.join("absent"), &revision, "alpha.txt", 10).is_err());
+ assert!(git_output(&root, &["not-a-real-git-command"], 100).is_err());
+ assert!(git_output(&root.join("absent"), &["status"], 100).is_err());
+ assert_eq!(split_once(b"missing", b'\t'), None);
+ assert_eq!(
+ split_once(b"one\ttwo\tthree", b'\t'),
+ Some((&b"one"[..], &b"two\tthree"[..]))
+ );
+ }
+
+ #[test]
+ fn archive_creation_requires_fresh_canonical_output_and_nonempty_commit() {
+ let (_fixture, root, revision) = fixture();
+ let output = root.join("output.tar");
+ fs::write(&output, b"preserve").unwrap();
+ assert!(create(&root, &revision, &output, 1).is_err());
+ assert_eq!(fs::read(&output).unwrap(), b"preserve");
+ assert!(create(&root, &revision, &root.join("absent/output.tar"), 1).is_err());
+ git(&root, &["rm", "-r", "alpha.txt", "nested"]);
+ git(&root, &["commit", "--quiet", "-m", "empty source"]);
+ let empty = git(&root, &["rev-parse", "HEAD"]);
+ assert!(create(&root, &empty, &root.join("empty.tar"), 1).is_err());
+ assert!(!root.join("empty.tar").exists());
+ #[cfg(unix)]
+ {
+ let link = root.join("parent-link");
+ std::os::unix::fs::symlink(root.join(".git"), &link).unwrap();
+ assert!(create(&root, &revision, &link.join("archive.tar"), 1).is_err());
+ }
+ }
+
+ #[test]
fn exact_tree_archive_is_reproducible_and_canonical() {
let (fixture, root, revision) = fixture();
let first = fixture.path().join("first.tar");
diff --git a/tools/xtask/src/rshr_202_step_298_gate.rs b/tools/xtask/src/rshr_202_step_298_gate.rs
@@ -247,7 +247,7 @@ fn expected_contract(verifier_sha256: &str) -> Value {
})
}
-pub(crate) fn run(arguments: Arguments) -> Result<(), String> {
+fn validate_arguments(arguments: &Arguments) -> Result<String, String> {
let check_id = format!("gate-01-{GATE_DIGEST}");
if arguments.step != STEP
|| arguments.check_id != check_id
@@ -265,6 +265,11 @@ pub(crate) fn run(arguments: Arguments) -> Result<(), String> {
{
return Err("Step 298 gate arguments differ".to_owned());
}
+ Ok(check_id)
+}
+
+pub(crate) fn run(arguments: Arguments) -> Result<(), String> {
+ let check_id = validate_arguments(&arguments)?;
let root = root();
if root.join(".github").exists() {
return Err("forbidden .github surface is present".to_owned());
@@ -283,24 +288,7 @@ pub(crate) fn run(arguments: Arguments) -> Result<(), String> {
let authority_path = root.join("contracts/rshr-202-step-298-gates.v1.json");
let authority_bytes =
fs::read(authority_path).map_err(|_| "Step 298 gate authority is unreadable".to_owned())?;
- let authority: Value = serde_json::from_slice(&authority_bytes)
- .map_err(|_| "Step 298 gate authority is invalid".to_owned())?;
- let mut canonical_authority = canonical(&authority)?;
- canonical_authority.push(b'\n');
- let contracts = authority
- .get("gate_command_contract")
- .and_then(Value::as_array)
- .ok_or_else(|| "Step 298 gate contract is absent".to_owned())?;
- if authority_bytes != canonical_authority
- || authority.get("schema")
- != Some(&Value::String(
- "radroots.lib.rshr-202-step-298-gates.v1".to_owned(),
- ))
- || authority.get("step") != Some(&json!([STEP]))
- || contracts.as_slice() != [expected_contract(&verifier_sha256)]
- {
- return Err("Step 298 gate authority differs".to_owned());
- }
+ let contract = validate_authority(&authority_bytes, &verifier_sha256)?;
cargo(
&["+1.97.1", "fmt", "--all", "--", "--check"],
@@ -325,7 +313,39 @@ pub(crate) fn run(arguments: Arguments) -> Result<(), String> {
)?;
require_nix()?;
- let contract = &contracts[0];
+ let bytes = result_bytes(&arguments, &check_id, &verifier_sha256, &contract)?;
+ std::io::Write::write_all(&mut std::io::stdout().lock(), &bytes)
+ .map_err(|_| "Step 298 result write failed".to_owned())
+}
+
+fn validate_authority(authority_bytes: &[u8], verifier_sha256: &str) -> Result<Value, String> {
+ let authority: Value = serde_json::from_slice(authority_bytes)
+ .map_err(|_| "Step 298 gate authority is invalid".to_owned())?;
+ let mut canonical_authority = canonical(&authority)?;
+ canonical_authority.push(b'\n');
+ let contracts = authority
+ .get("gate_command_contract")
+ .and_then(Value::as_array)
+ .ok_or_else(|| "Step 298 gate contract is absent".to_owned())?;
+ if authority_bytes != canonical_authority
+ || authority.get("schema")
+ != Some(&Value::String(
+ "radroots.lib.rshr-202-step-298-gates.v1".to_owned(),
+ ))
+ || authority.get("step") != Some(&json!([STEP]))
+ || contracts.as_slice() != [expected_contract(verifier_sha256)]
+ {
+ return Err("Step 298 gate authority differs".to_owned());
+ }
+ Ok(contracts[0].clone())
+}
+
+fn result_bytes(
+ arguments: &Arguments,
+ check_id: &str,
+ verifier_sha256: &str,
+ contract: &Value,
+) -> Result<Vec<u8>, String> {
let assertion = json!([{
"id": format!("step_298_gate_01_{GATE_DIGEST}"),
"result": "pass"
@@ -351,6 +371,131 @@ pub(crate) fn run(arguments: Arguments) -> Result<(), String> {
});
let mut bytes = canonical(&result)?;
bytes.push(b'\n');
- std::io::Write::write_all(&mut std::io::stdout().lock(), &bytes)
- .map_err(|_| "Step 298 result write failed".to_owned())
+ Ok(bytes)
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn command_capture_preserves_status_streams_and_enforces_both_output_limits() {
+ let output = bounded(
+ Command::new("/bin/sh").args(["-c", "printf output; printf diagnostic >&2"]),
+ "fixture",
+ )
+ .unwrap();
+ assert_eq!(output.stdout, b"output");
+ assert_eq!(output.stderr, b"diagnostic");
+ assert_eq!(
+ bounded(Command::new("/bin/sh").args(["-c", "exit 7"]), "fixture").unwrap_err(),
+ "fixture failed"
+ );
+ let missing = tempfile::TempDir::new().unwrap();
+ assert_eq!(
+ bounded(&mut Command::new(missing.path().join("absent")), "fixture").unwrap_err(),
+ "fixture could not start"
+ );
+ for script in ["head -c \"$1\" /dev/zero", "head -c \"$1\" /dev/zero >&2"] {
+ let maximum = MAX_OUTPUT_BYTES.to_string();
+ let output = bounded(
+ Command::new("/bin/sh").args(["-c", script, "fixture", &maximum]),
+ "fixture",
+ )
+ .unwrap();
+ assert_eq!(output.stdout.len() + output.stderr.len(), MAX_OUTPUT_BYTES);
+ let oversized = (MAX_OUTPUT_BYTES + 1).to_string();
+ assert_eq!(
+ bounded(
+ Command::new("/bin/sh").args(["-c", script, "fixture", &oversized]),
+ "fixture"
+ )
+ .unwrap_err(),
+ "fixture exceeded its output bound"
+ );
+ }
+ }
+
+ fn arguments() -> Arguments {
+ Arguments {
+ step: STEP,
+ check_id: format!("gate-01-{GATE_DIGEST}"),
+ source_revision: "a".repeat(40),
+ source_tree: "0".repeat(40),
+ candidate_digest: "none".into(),
+ platform: "macos_aarch64".into(),
+ execution_request_sha256: "1".repeat(64),
+ }
+ }
+
+ #[test]
+ fn invalid_gate_arguments_are_rejected_before_external_work() {
+ assert_eq!(
+ validate_arguments(&arguments()).unwrap(),
+ format!("gate-01-{GATE_DIGEST}")
+ );
+ for field in 0..8 {
+ let mut invalid = arguments();
+ match field {
+ 0 => invalid.step = 0,
+ 1 => invalid.check_id.clear(),
+ 2 => invalid.candidate_digest = "unbound".into(),
+ 3 => invalid.platform = "linux".into(),
+ 4 => invalid.source_revision.clear(),
+ 5 => invalid.source_tree.clear(),
+ 6 => invalid.execution_request_sha256.clear(),
+ _ => invalid.source_revision = "A".repeat(40),
+ }
+ assert_eq!(run(invalid).unwrap_err(), "Step 298 gate arguments differ");
+ }
+ let mut invalid = arguments();
+ invalid.source_tree = "g".repeat(40);
+ assert!(validate_arguments(&invalid).is_err());
+ }
+
+ #[test]
+ fn authority_requires_canonical_bytes_and_exact_retained_bindings() {
+ let raw = include_bytes!("../../../contracts/rshr-202-step-298-gates.v1.json");
+ let authority: Value = serde_json::from_slice(raw).unwrap();
+ let verifier = authority["gate_command_contract"][0]["verifier_sha256"]
+ .as_str()
+ .unwrap();
+ let contract = validate_authority(raw, verifier).unwrap();
+ assert!(validate_authority(raw, &"f".repeat(64)).is_err());
+ assert!(validate_authority(b"invalid", verifier).is_err());
+ assert!(validate_authority(b"{}\n", verifier).is_err());
+ assert!(
+ validate_authority(&serde_json::to_vec_pretty(&authority).unwrap(), verifier).is_err()
+ );
+ for (pointer, value) in [
+ ("/schema", json!("other")),
+ ("/step", json!([0])),
+ ("/gate_command_contract", json!([])),
+ ] {
+ let mut changed = authority.clone();
+ *changed.pointer_mut(pointer).unwrap() = value;
+ let mut bytes = canonical(&changed).unwrap();
+ bytes.push(b'\n');
+ assert!(validate_authority(&bytes, verifier).is_err());
+ }
+ // Encoding fixtures is not a historical gate execution or qualification.
+ let args = arguments();
+ let bytes = result_bytes(&args, &args.check_id, verifier, &contract).unwrap();
+ let result: Value = serde_json::from_slice(&bytes).unwrap();
+ assert!(bytes.ends_with(b"\n"));
+ assert_eq!(result["source_revision"], args.source_revision);
+ assert_eq!(result["source_tree"], args.source_tree);
+ assert_eq!(
+ result["execution_request"][0]["sha256"],
+ args.execution_request_sha256
+ );
+ assert_eq!(
+ result["command_contract_sha256"],
+ sha256(&canonical(&contract).unwrap())
+ );
+ assert_eq!(
+ result["assertion_inventory_sha256"],
+ sha256(&canonical(&result["assertion"]).unwrap())
+ );
+ }
}
diff --git a/tools/xtask/src/rshr_202_step_298_platform.rs b/tools/xtask/src/rshr_202_step_298_platform.rs
@@ -36,7 +36,11 @@ fn uname(flag: &str) -> Result<String, String> {
if !output.status.success() || !output.stderr.is_empty() {
return Err("Step 298 platform probe uname failed".to_owned());
}
- let value = std::str::from_utf8(&output.stdout)
+ parse_uname(&output.stdout)
+}
+
+fn parse_uname(stdout: &[u8]) -> Result<String, String> {
+ let value = std::str::from_utf8(stdout)
.map_err(|_| "Step 298 platform probe uname output is not UTF-8".to_owned())?
.strip_suffix('\n')
.ok_or_else(|| "Step 298 platform probe uname output differs".to_owned())?;
@@ -49,7 +53,20 @@ fn uname(flag: &str) -> Result<String, String> {
pub(crate) fn run() -> Result<(), String> {
let request_bytes = fs::read(root().join(REQUEST_PATH))
.map_err(|_| "Step 298 platform execution request is unavailable".to_owned())?;
- let raw_request = std::str::from_utf8(&request_bytes)
+ let execution_request_sha256 = request_digest(&request_bytes)?;
+ let bytes = result_bytes(
+ execution_request_sha256,
+ &uname("-s")?,
+ &uname("-r")?,
+ &uname("-v")?,
+ std::env::consts::ARCH,
+ )?;
+ std::io::Write::write_all(&mut std::io::stdout().lock(), &bytes)
+ .map_err(|_| "Step 298 platform result write failed".to_owned())
+}
+
+fn request_digest(request_bytes: &[u8]) -> Result<&str, String> {
+ let raw_request = std::str::from_utf8(request_bytes)
.map_err(|_| "Step 298 platform execution request is not UTF-8".to_owned())?;
let execution_request_sha256 = raw_request.strip_suffix('\n').unwrap_or(raw_request);
if execution_request_sha256.len() != 64
@@ -59,11 +76,17 @@ pub(crate) fn run() -> Result<(), String> {
{
return Err("Step 298 platform execution request differs".to_owned());
}
+ Ok(execution_request_sha256)
+}
- let kernel_name = uname("-s")?;
- let kernel_release = uname("-r")?;
- let kernel_version = uname("-v")?;
- if kernel_name != "Darwin" || std::env::consts::ARCH != "aarch64" {
+fn result_bytes(
+ execution_request_sha256: &str,
+ kernel_name: &str,
+ kernel_release: &str,
+ kernel_version: &str,
+ architecture: &str,
+) -> Result<Vec<u8>, String> {
+ if kernel_name != "Darwin" || architecture != "aarch64" {
return Err("Step 298 platform identity differs".to_owned());
}
@@ -98,6 +121,66 @@ pub(crate) fn run() -> Result<(), String> {
});
let mut bytes = canonical(&result)?;
bytes.push(b'\n');
- std::io::Write::write_all(&mut std::io::stdout().lock(), &bytes)
- .map_err(|_| "Step 298 platform result write failed".to_owned())
+ Ok(bytes)
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn request_digest_rejects_noncanonical_or_unbound_values() {
+ let digest = "0123456789abcdef".repeat(4);
+ assert_eq!(request_digest(digest.as_bytes()).unwrap(), digest);
+ assert_eq!(
+ request_digest(format!("{digest}\n").as_bytes()).unwrap(),
+ digest
+ );
+ for bytes in [
+ vec![],
+ vec![0xff],
+ vec![b'0'; 63],
+ vec![b'0'; 65],
+ vec![b'G'; 64],
+ vec![b'A'; 64],
+ format!("{digest}\n\n").into_bytes(),
+ ] {
+ assert!(request_digest(&bytes).is_err());
+ }
+ }
+
+ #[test]
+ fn uname_output_requires_one_nonempty_utf8_line() {
+ assert_eq!(parse_uname(b"Darwin\n").unwrap(), "Darwin");
+ for bytes in [
+ b"".as_slice(),
+ b"\n",
+ b"Darwin",
+ b"Dar\nwin\n",
+ b"Darwin\r\n",
+ &[0xff],
+ ] {
+ assert!(parse_uname(bytes).is_err());
+ }
+ }
+
+ #[test]
+ fn encoded_platform_fixture_binds_request_and_kernel_without_qualifying_host() {
+ let request = "a".repeat(64);
+ let bytes = result_bytes(
+ &request,
+ "Darwin",
+ "fixture-release",
+ "fixture-version",
+ "aarch64",
+ )
+ .unwrap();
+ let result: Value = serde_json::from_slice(&bytes).unwrap();
+ assert_eq!(result["execution_request_sha256"], request);
+ assert_eq!(result["kernel_release"], "fixture-release");
+ assert_eq!(result["os_build_sha256"], sha256(&canonical(&json!({"kernel_name":"Darwin", "kernel_release":"fixture-release", "kernel_version":"fixture-version"})).unwrap()));
+ assert!(bytes.ends_with(b"\n"));
+ assert!(result_bytes(&request, "Linux", "fixture", "fixture", "aarch64").is_err());
+ assert!(result_bytes(&request, "Darwin", "fixture", "fixture", "x86_64").is_err());
+ }
}
diff --git a/tools/xtask/src/rshr_202_step_299_gate.rs b/tools/xtask/src/rshr_202_step_299_gate.rs
@@ -131,6 +131,45 @@ fn require_outputs(nix: &Path) -> Result<(), String> {
)?;
let inventory: Value = serde_json::from_slice(&show.stdout)
.map_err(|_| "Step 299 Nix output inventory is invalid".to_owned())?;
+ validate_output_inventory(&inventory)?;
+
+ let supported = bounded(
+ Command::new(nix).args(["--offline", "eval", "--json", ".#lib.supportedSystems"]),
+ "Step 299 shared-helper systems",
+ )?;
+ if supported.stdout != b"[\"aarch64-darwin\",\"x86_64-linux\"]\n" {
+ return Err("Step 299 shared-helper systems differ".to_owned());
+ }
+ let helper_type = bounded(
+ Command::new(nix).args([
+ "--offline",
+ "eval",
+ "--raw",
+ "--apply",
+ "f: builtins.typeOf f",
+ ".#lib.mkServiceHelpers",
+ ]),
+ "Step 299 shared-helper export",
+ )?;
+ if helper_type.stdout != b"lambda" {
+ return Err("Step 299 shared-helper export differs".to_owned());
+ }
+
+ for system in ["x86_64-darwin", "aarch64-linux", "x86_64-windows"] {
+ rejected(
+ Command::new(nix).args([
+ "--offline",
+ "eval",
+ "--raw",
+ &format!(".#packages.{system}.default.name"),
+ ]),
+ "Step 299 excluded-system evaluation",
+ )?;
+ }
+ Ok(())
+}
+
+fn validate_output_inventory(inventory: &Value) -> Result<(), String> {
let systems = ["aarch64-darwin", "x86_64-linux"];
for family in ["apps", "checks", "devShells", "formatter", "packages"] {
if object_keys(&inventory[family], family)? != systems {
@@ -175,39 +214,6 @@ fn require_outputs(nix: &Path) -> Result<(), String> {
}
}
- let supported = bounded(
- Command::new(nix).args(["--offline", "eval", "--json", ".#lib.supportedSystems"]),
- "Step 299 shared-helper systems",
- )?;
- if supported.stdout != b"[\"aarch64-darwin\",\"x86_64-linux\"]\n" {
- return Err("Step 299 shared-helper systems differ".to_owned());
- }
- let helper_type = bounded(
- Command::new(nix).args([
- "--offline",
- "eval",
- "--raw",
- "--apply",
- "f: builtins.typeOf f",
- ".#lib.mkServiceHelpers",
- ]),
- "Step 299 shared-helper export",
- )?;
- if helper_type.stdout != b"lambda" {
- return Err("Step 299 shared-helper export differs".to_owned());
- }
-
- for system in ["x86_64-darwin", "aarch64-linux", "x86_64-windows"] {
- rejected(
- Command::new(nix).args([
- "--offline",
- "eval",
- "--raw",
- &format!(".#packages.{system}.default.name"),
- ]),
- "Step 299 excluded-system evaluation",
- )?;
- }
Ok(())
}
@@ -278,7 +284,7 @@ fn expected_contract(verifier_sha256: &str) -> Value {
})
}
-pub(crate) fn run(arguments: Arguments) -> Result<(), String> {
+fn validate_arguments(arguments: &Arguments) -> Result<String, String> {
let check_id = format!("gate-01-{GATE_DIGEST}");
if arguments.step != STEP
|| arguments.check_id != check_id
@@ -296,6 +302,11 @@ pub(crate) fn run(arguments: Arguments) -> Result<(), String> {
{
return Err("Step 299 gate arguments differ".to_owned());
}
+ Ok(check_id)
+}
+
+pub(crate) fn run(arguments: Arguments) -> Result<(), String> {
+ let check_id = validate_arguments(&arguments)?;
let root = root();
if root.join(".github").exists() {
return Err("forbidden .github surface is present".to_owned());
@@ -314,7 +325,25 @@ pub(crate) fn run(arguments: Arguments) -> Result<(), String> {
let authority_path = root.join("contracts/rshr-202-step-299-gates.v1.json");
let authority_bytes =
fs::read(authority_path).map_err(|_| "Step 299 gate authority is unreadable".to_owned())?;
- let authority: Value = serde_json::from_slice(&authority_bytes)
+ let contract = validate_authority(&authority_bytes, &verifier_sha256)?;
+
+ bounded(
+ Command::new("cargo").args(["+1.97.1", "fmt", "--all", "--", "--check"]),
+ "Step 299 formatting",
+ )?;
+ bounded(
+ Command::new("cargo").args(["+1.97.1", "check", "--offline", "--locked", "-p", "xtask"]),
+ "Step 299 verifier check",
+ )?;
+ require_nix()?;
+
+ let bytes = result_bytes(&arguments, &check_id, &verifier_sha256, &contract)?;
+ std::io::Write::write_all(&mut std::io::stdout().lock(), &bytes)
+ .map_err(|_| "Step 299 result write failed".to_owned())
+}
+
+fn validate_authority(authority_bytes: &[u8], verifier_sha256: &str) -> Result<Value, String> {
+ let authority: Value = serde_json::from_slice(authority_bytes)
.map_err(|_| "Step 299 gate authority is invalid".to_owned())?;
let mut canonical_authority = canonical(&authority)?;
canonical_authority.push(b'\n');
@@ -328,22 +357,19 @@ pub(crate) fn run(arguments: Arguments) -> Result<(), String> {
"radroots.lib.rshr-202-step-299-gates.v1".to_owned(),
))
|| authority.get("step") != Some(&json!([STEP]))
- || contracts.as_slice() != [expected_contract(&verifier_sha256)]
+ || contracts.as_slice() != [expected_contract(verifier_sha256)]
{
return Err("Step 299 gate authority differs".to_owned());
}
+ Ok(contracts[0].clone())
+}
- bounded(
- Command::new("cargo").args(["+1.97.1", "fmt", "--all", "--", "--check"]),
- "Step 299 formatting",
- )?;
- bounded(
- Command::new("cargo").args(["+1.97.1", "check", "--offline", "--locked", "-p", "xtask"]),
- "Step 299 verifier check",
- )?;
- require_nix()?;
-
- let contract = &contracts[0];
+fn result_bytes(
+ arguments: &Arguments,
+ check_id: &str,
+ verifier_sha256: &str,
+ contract: &Value,
+) -> Result<Vec<u8>, String> {
let assertion = json!([{
"id": format!("step_299_gate_01_{GATE_DIGEST}"),
"result": "pass"
@@ -369,6 +395,185 @@ pub(crate) fn run(arguments: Arguments) -> Result<(), String> {
});
let mut bytes = canonical(&result)?;
bytes.push(b'\n');
- std::io::Write::write_all(&mut std::io::stdout().lock(), &bytes)
- .map_err(|_| "Step 299 result write failed".to_owned())
+ Ok(bytes)
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn command_capture_preserves_status_streams_and_enforces_both_output_limits() {
+ let output = bounded(
+ Command::new("/bin/sh").args(["-c", "printf output; printf diagnostic >&2"]),
+ "fixture",
+ )
+ .unwrap();
+ assert_eq!(output.stdout, b"output");
+ assert_eq!(output.stderr, b"diagnostic");
+ assert_eq!(
+ bounded(Command::new("/bin/sh").args(["-c", "exit 7"]), "fixture").unwrap_err(),
+ "fixture failed"
+ );
+ let missing = tempfile::TempDir::new().unwrap();
+ assert_eq!(
+ bounded(&mut Command::new(missing.path().join("absent")), "fixture").unwrap_err(),
+ "fixture could not start"
+ );
+ for script in ["head -c \"$1\" /dev/zero", "head -c \"$1\" /dev/zero >&2"] {
+ let maximum = MAX_OUTPUT_BYTES.to_string();
+ let output = bounded(
+ Command::new("/bin/sh").args(["-c", script, "fixture", &maximum]),
+ "fixture",
+ )
+ .unwrap();
+ assert_eq!(output.stdout.len() + output.stderr.len(), MAX_OUTPUT_BYTES);
+ let oversized = (MAX_OUTPUT_BYTES + 1).to_string();
+ assert_eq!(
+ bounded(
+ Command::new("/bin/sh").args(["-c", script, "fixture", &oversized]),
+ "fixture"
+ )
+ .unwrap_err(),
+ "fixture exceeded its output bound"
+ );
+ }
+ }
+
+ #[test]
+ fn expected_command_rejection_requires_a_nonzero_exit() {
+ rejected(Command::new("/bin/sh").args(["-c", "exit 7"]), "fixture").unwrap();
+ assert_eq!(
+ rejected(Command::new("/bin/sh").args(["-c", "exit 0"]), "fixture").unwrap_err(),
+ "fixture unexpectedly succeeded"
+ );
+ }
+
+ fn arguments() -> Arguments {
+ Arguments {
+ step: STEP,
+ check_id: format!("gate-01-{GATE_DIGEST}"),
+ source_revision: "a".repeat(40),
+ source_tree: "0".repeat(40),
+ candidate_digest: "none".into(),
+ platform: "macos_aarch64".into(),
+ execution_request_sha256: "1".repeat(64),
+ }
+ }
+
+ #[test]
+ fn invalid_gate_arguments_are_rejected_before_external_work() {
+ assert_eq!(
+ validate_arguments(&arguments()).unwrap(),
+ format!("gate-01-{GATE_DIGEST}")
+ );
+ for field in 0..8 {
+ let mut invalid = arguments();
+ match field {
+ 0 => invalid.step = 0,
+ 1 => invalid.check_id.clear(),
+ 2 => invalid.candidate_digest = "unbound".into(),
+ 3 => invalid.platform = "linux".into(),
+ 4 => invalid.source_revision.clear(),
+ 5 => invalid.source_tree.clear(),
+ 6 => invalid.execution_request_sha256.clear(),
+ _ => invalid.source_revision = "A".repeat(40),
+ }
+ assert_eq!(run(invalid).unwrap_err(), "Step 299 gate arguments differ");
+ }
+ let mut invalid = arguments();
+ invalid.source_tree = "g".repeat(40);
+ assert!(validate_arguments(&invalid).is_err());
+ }
+
+ #[test]
+ fn authority_requires_canonical_bytes_and_exact_retained_bindings() {
+ let raw = include_bytes!("../../../contracts/rshr-202-step-299-gates.v1.json");
+ let authority: Value = serde_json::from_slice(raw).unwrap();
+ let verifier = authority["gate_command_contract"][0]["verifier_sha256"]
+ .as_str()
+ .unwrap();
+ let contract = validate_authority(raw, verifier).unwrap();
+ assert!(validate_authority(raw, &"f".repeat(64)).is_err());
+ assert!(validate_authority(b"invalid", verifier).is_err());
+ assert!(validate_authority(b"{}\n", verifier).is_err());
+ assert!(
+ validate_authority(&serde_json::to_vec_pretty(&authority).unwrap(), verifier).is_err()
+ );
+ for (pointer, value) in [
+ ("/schema", json!("other")),
+ ("/step", json!([0])),
+ ("/gate_command_contract", json!([])),
+ ] {
+ let mut changed = authority.clone();
+ *changed.pointer_mut(pointer).unwrap() = value;
+ let mut bytes = canonical(&changed).unwrap();
+ bytes.push(b'\n');
+ assert!(validate_authority(&bytes, verifier).is_err());
+ }
+ // Encoding fixtures is not a historical gate execution or qualification.
+ let args = arguments();
+ let bytes = result_bytes(&args, &args.check_id, verifier, &contract).unwrap();
+ let result: Value = serde_json::from_slice(&bytes).unwrap();
+ assert!(bytes.ends_with(b"\n"));
+ assert_eq!(result["source_revision"], args.source_revision);
+ assert_eq!(result["source_tree"], args.source_tree);
+ assert_eq!(
+ result["execution_request"][0]["sha256"],
+ args.execution_request_sha256
+ );
+ assert_eq!(
+ result["command_contract_sha256"],
+ sha256(&canonical(&contract).unwrap())
+ );
+ assert_eq!(
+ result["assertion_inventory_sha256"],
+ sha256(&canonical(&result["assertion"]).unwrap())
+ );
+ }
+
+ #[test]
+ fn retained_inventory_rejects_platform_drift_and_fixture_escape_without_nix() {
+ let mut inventory = json!({"overlays":{"default":{"type":"nixpkgs-overlay"}}});
+ for system in ["aarch64-darwin", "x86_64-linux"] {
+ inventory["packages"][system] =
+ json!({"default":{"name":"radroots-lib-release-bundle-0.1.0-alpha"},"xtask":{}});
+ inventory["apps"][system] = json!({"default":{"description":"Inspect the installed Radroots Lib release bundle"}});
+ inventory["devShells"][system] = json!({"default":{}});
+ inventory["checks"][system] =
+ json!({"release-bundle":{}, "service-fixture-example":{}});
+ inventory["formatter"][system] = json!({});
+ }
+ validate_output_inventory(&inventory).unwrap();
+ for family in ["apps", "checks", "devShells", "formatter", "packages"] {
+ let mut changed = inventory.clone();
+ changed[family]
+ .as_object_mut()
+ .unwrap()
+ .remove("aarch64-darwin");
+ assert!(validate_output_inventory(&changed).is_err());
+ }
+ assert!(object_keys(&Value::Null, "test").is_err());
+ for (pointer, value) in [
+ ("/overlays/default/type", json!("wrong")),
+ ("/packages/aarch64-darwin/default/name", json!("wrong")),
+ ("/apps/aarch64-darwin/default/description", json!("wrong")),
+ ("/devShells/aarch64-darwin", json!({})),
+ ("/checks/aarch64-darwin", json!({})),
+ ("/checks/aarch64-darwin", json!({"release-bundle":{}})),
+ (
+ "/checks/aarch64-darwin",
+ json!({"release-bundle":{},"bad-fixture":{}}),
+ ),
+ ] {
+ let mut changed = inventory.clone();
+ *changed.pointer_mut(pointer).unwrap() = value;
+ assert!(validate_output_inventory(&changed).is_err());
+ }
+ for family in ["apps", "devShells", "packages"] {
+ let mut changed = inventory.clone();
+ changed[family]["aarch64-darwin"]["fixture-escape"] = json!({});
+ assert!(validate_output_inventory(&changed).is_err());
+ }
+ }
}
diff --git a/tools/xtask/src/rshr_202_step_304_gate.rs b/tools/xtask/src/rshr_202_step_304_gate.rs
@@ -190,7 +190,7 @@ fn expected_contract(verifier_sha256: &str) -> Value {
})
}
-pub(crate) fn run(arguments: Arguments) -> Result<(), String> {
+fn validate_arguments(arguments: &Arguments) -> Result<String, String> {
let check_id = format!("gate-01-{GATE_DIGEST}");
if arguments.step != STEP
|| arguments.check_id != check_id
@@ -208,6 +208,11 @@ pub(crate) fn run(arguments: Arguments) -> Result<(), String> {
{
return Err("Step 304 gate arguments differ".to_owned());
}
+ Ok(check_id)
+}
+
+pub(crate) fn run(arguments: Arguments) -> Result<(), String> {
+ let check_id = validate_arguments(&arguments)?;
let root = root();
if root.join(".github").exists() {
return Err("forbidden .github surface is present".to_owned());
@@ -225,21 +230,7 @@ pub(crate) fn run(arguments: Arguments) -> Result<(), String> {
sha256(&fs::read(verifier_path).map_err(|_| "Step 304 verifier is unreadable")?);
let authority_bytes = fs::read(root.join("contracts/rshr-202-step-304-gates.v1.json"))
.map_err(|_| "Step 304 gate authority is unreadable".to_owned())?;
- let authority: Value = serde_json::from_slice(&authority_bytes)
- .map_err(|_| "Step 304 gate authority is invalid".to_owned())?;
- let mut canonical_authority = canonical(&authority)?;
- canonical_authority.push(b'\n');
- let contracts = authority
- .get("gate_command_contract")
- .and_then(Value::as_array)
- .ok_or_else(|| "Step 304 gate contract is absent".to_owned())?;
- if authority_bytes != canonical_authority
- || authority.get("schema") != Some(&json!("radroots.lib.rshr-202-step-304-gates.v1"))
- || authority.get("step") != Some(&json!([STEP]))
- || contracts.as_slice() != [expected_contract(&verifier_sha256)]
- {
- return Err("Step 304 gate authority differs".to_owned());
- }
+ let contract = validate_authority(&authority_bytes, &verifier_sha256)?;
bounded(
Command::new("cargo").args(["+1.97.1", "fmt", "--all", "--", "--check"]),
"Step 304 formatting",
@@ -303,7 +294,36 @@ pub(crate) fn run(arguments: Arguments) -> Result<(), String> {
"Step 304 contracts",
)?;
require_nix()?;
- let contract = &contracts[0];
+ let bytes = result_bytes(&arguments, &check_id, &verifier_sha256, &contract)?;
+ std::io::Write::write_all(&mut std::io::stdout().lock(), &bytes)
+ .map_err(|_| "Step 304 result write failed".to_owned())
+}
+
+fn validate_authority(authority_bytes: &[u8], verifier_sha256: &str) -> Result<Value, String> {
+ let authority: Value = serde_json::from_slice(authority_bytes)
+ .map_err(|_| "Step 304 gate authority is invalid".to_owned())?;
+ let mut canonical_authority = canonical(&authority)?;
+ canonical_authority.push(b'\n');
+ let contracts = authority
+ .get("gate_command_contract")
+ .and_then(Value::as_array)
+ .ok_or_else(|| "Step 304 gate contract is absent".to_owned())?;
+ if authority_bytes != canonical_authority
+ || authority.get("schema") != Some(&json!("radroots.lib.rshr-202-step-304-gates.v1"))
+ || authority.get("step") != Some(&json!([STEP]))
+ || contracts.as_slice() != [expected_contract(verifier_sha256)]
+ {
+ return Err("Step 304 gate authority differs".to_owned());
+ }
+ Ok(contracts[0].clone())
+}
+
+fn result_bytes(
+ arguments: &Arguments,
+ check_id: &str,
+ verifier_sha256: &str,
+ contract: &Value,
+) -> Result<Vec<u8>, String> {
let assertion = json!([{ "id": format!("step_304_gate_01_{GATE_DIGEST}"), "result": "pass" }]);
let result = json!({
"schema": "radroots.services-hardening.rshr-200-step-check-result.v1", "step": STEP,
@@ -316,6 +336,131 @@ pub(crate) fn run(arguments: Arguments) -> Result<(), String> {
});
let mut bytes = canonical(&result)?;
bytes.push(b'\n');
- std::io::Write::write_all(&mut std::io::stdout().lock(), &bytes)
- .map_err(|_| "Step 304 result write failed".to_owned())
+ Ok(bytes)
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn command_capture_preserves_status_streams_and_enforces_both_output_limits() {
+ let output = bounded(
+ Command::new("/bin/sh").args(["-c", "printf output; printf diagnostic >&2"]),
+ "fixture",
+ )
+ .unwrap();
+ assert_eq!(output.stdout, b"output");
+ assert_eq!(output.stderr, b"diagnostic");
+ assert_eq!(
+ bounded(Command::new("/bin/sh").args(["-c", "exit 7"]), "fixture").unwrap_err(),
+ "fixture failed"
+ );
+ let missing = tempfile::TempDir::new().unwrap();
+ assert_eq!(
+ bounded(&mut Command::new(missing.path().join("absent")), "fixture").unwrap_err(),
+ "fixture could not start"
+ );
+ for script in ["head -c \"$1\" /dev/zero", "head -c \"$1\" /dev/zero >&2"] {
+ let maximum = MAX_OUTPUT_BYTES.to_string();
+ let output = bounded(
+ Command::new("/bin/sh").args(["-c", script, "fixture", &maximum]),
+ "fixture",
+ )
+ .unwrap();
+ assert_eq!(output.stdout.len() + output.stderr.len(), MAX_OUTPUT_BYTES);
+ let oversized = (MAX_OUTPUT_BYTES + 1).to_string();
+ assert_eq!(
+ bounded(
+ Command::new("/bin/sh").args(["-c", script, "fixture", &oversized]),
+ "fixture"
+ )
+ .unwrap_err(),
+ "fixture exceeded its output bound"
+ );
+ }
+ }
+
+ fn arguments() -> Arguments {
+ Arguments {
+ step: STEP,
+ check_id: format!("gate-01-{GATE_DIGEST}"),
+ source_revision: "a".repeat(40),
+ source_tree: "0".repeat(40),
+ candidate_digest: "none".into(),
+ platform: "macos_aarch64".into(),
+ execution_request_sha256: "1".repeat(64),
+ }
+ }
+
+ #[test]
+ fn invalid_gate_arguments_are_rejected_before_external_work() {
+ assert_eq!(
+ validate_arguments(&arguments()).unwrap(),
+ format!("gate-01-{GATE_DIGEST}")
+ );
+ for field in 0..8 {
+ let mut invalid = arguments();
+ match field {
+ 0 => invalid.step = 0,
+ 1 => invalid.check_id.clear(),
+ 2 => invalid.candidate_digest = "unbound".into(),
+ 3 => invalid.platform = "linux".into(),
+ 4 => invalid.source_revision.clear(),
+ 5 => invalid.source_tree.clear(),
+ 6 => invalid.execution_request_sha256.clear(),
+ _ => invalid.source_revision = "A".repeat(40),
+ }
+ assert_eq!(run(invalid).unwrap_err(), "Step 304 gate arguments differ");
+ }
+ let mut invalid = arguments();
+ invalid.source_tree = "g".repeat(40);
+ assert!(validate_arguments(&invalid).is_err());
+ }
+
+ #[test]
+ fn authority_requires_canonical_bytes_and_exact_retained_bindings() {
+ let raw = include_bytes!("../../../contracts/rshr-202-step-304-gates.v1.json");
+ let authority: Value = serde_json::from_slice(raw).unwrap();
+ let verifier = authority["gate_command_contract"][0]["verifier_sha256"]
+ .as_str()
+ .unwrap();
+ let contract = validate_authority(raw, verifier).unwrap();
+ assert!(validate_authority(raw, &"f".repeat(64)).is_err());
+ assert!(validate_authority(b"invalid", verifier).is_err());
+ assert!(validate_authority(b"{}\n", verifier).is_err());
+ assert!(
+ validate_authority(&serde_json::to_vec_pretty(&authority).unwrap(), verifier).is_err()
+ );
+ for (pointer, value) in [
+ ("/schema", json!("other")),
+ ("/step", json!([0])),
+ ("/gate_command_contract", json!([])),
+ ] {
+ let mut changed = authority.clone();
+ *changed.pointer_mut(pointer).unwrap() = value;
+ let mut bytes = canonical(&changed).unwrap();
+ bytes.push(b'\n');
+ assert!(validate_authority(&bytes, verifier).is_err());
+ }
+ // Encoding fixtures is not a historical gate execution or qualification.
+ let args = arguments();
+ let bytes = result_bytes(&args, &args.check_id, verifier, &contract).unwrap();
+ let result: Value = serde_json::from_slice(&bytes).unwrap();
+ assert!(bytes.ends_with(b"\n"));
+ assert_eq!(result["source_revision"], args.source_revision);
+ assert_eq!(result["source_tree"], args.source_tree);
+ assert_eq!(
+ result["execution_request"][0]["sha256"],
+ args.execution_request_sha256
+ );
+ assert_eq!(
+ result["command_contract_sha256"],
+ sha256(&canonical(&contract).unwrap())
+ );
+ assert_eq!(
+ result["assertion_inventory_sha256"],
+ sha256(&canonical(&result["assertion"]).unwrap())
+ );
+ }
}
diff --git a/tools/xtask/src/rshr_202_step_305_gate.rs b/tools/xtask/src/rshr_202_step_305_gate.rs
@@ -131,7 +131,7 @@ fn expected_contract(verifier_sha256: &str) -> Value {
})
}
-pub(crate) fn run(arguments: Arguments) -> Result<(), String> {
+fn validate_arguments(arguments: &Arguments) -> Result<String, String> {
let check_id = format!("gate-01-{GATE_DIGEST}");
if arguments.step != STEP
|| arguments.check_id != check_id
@@ -143,6 +143,11 @@ pub(crate) fn run(arguments: Arguments) -> Result<(), String> {
{
return Err("Step 305 gate arguments differ".to_owned());
}
+ Ok(check_id)
+}
+
+pub(crate) fn run(arguments: Arguments) -> Result<(), String> {
+ let check_id = validate_arguments(&arguments)?;
let root = root();
if root.join(".github").exists() {
return Err("forbidden .github surface is present".to_owned());
@@ -161,21 +166,7 @@ pub(crate) fn run(arguments: Arguments) -> Result<(), String> {
sha256(&fs::read(verifier_path).map_err(|_| "Step 305 verifier is unreadable".to_owned())?);
let authority_bytes = fs::read(root.join("contracts/rshr-202-step-305-gates.v1.json"))
.map_err(|_| "Step 305 gate authority is unreadable".to_owned())?;
- let authority: Value = serde_json::from_slice(&authority_bytes)
- .map_err(|_| "Step 305 gate authority is invalid".to_owned())?;
- let mut canonical_authority = canonical(&authority)?;
- canonical_authority.push(b'\n');
- let contracts = authority
- .get("gate_command_contract")
- .and_then(Value::as_array)
- .ok_or_else(|| "Step 305 gate contract is absent".to_owned())?;
- if authority_bytes != canonical_authority
- || authority.get("schema") != Some(&json!("radroots.lib.rshr-202-step-305-gates.v1"))
- || authority.get("step") != Some(&json!([STEP]))
- || contracts.as_slice() != [expected_contract(&verifier_sha256)]
- {
- return Err("Step 305 gate authority differs".to_owned());
- }
+ let contract = validate_authority(&authority_bytes, &verifier_sha256)?;
for (arguments, label) in [
(
@@ -256,7 +247,43 @@ pub(crate) fn run(arguments: Arguments) -> Result<(), String> {
"Step 305 contracts",
)?;
- let contract = &contracts[0];
+ let bytes = result_bytes(&arguments, &check_id, &verifier_sha256, &contract)?;
+ std::io::Write::write_all(&mut std::io::stdout().lock(), &bytes)
+ .map_err(|_| "Step 305 result write failed".to_owned())
+}
+
+fn valid_hex(value: &str, length: usize) -> bool {
+ value.len() == length
+ && value
+ .bytes()
+ .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
+}
+
+fn validate_authority(authority_bytes: &[u8], verifier_sha256: &str) -> Result<Value, String> {
+ let authority: Value = serde_json::from_slice(authority_bytes)
+ .map_err(|_| "Step 305 gate authority is invalid".to_owned())?;
+ let mut canonical_authority = canonical(&authority)?;
+ canonical_authority.push(b'\n');
+ let contracts = authority
+ .get("gate_command_contract")
+ .and_then(Value::as_array)
+ .ok_or_else(|| "Step 305 gate contract is absent".to_owned())?;
+ if authority_bytes != canonical_authority
+ || authority.get("schema") != Some(&json!("radroots.lib.rshr-202-step-305-gates.v1"))
+ || authority.get("step") != Some(&json!([STEP]))
+ || contracts.as_slice() != [expected_contract(verifier_sha256)]
+ {
+ return Err("Step 305 gate authority differs".to_owned());
+ }
+ Ok(contracts[0].clone())
+}
+
+fn result_bytes(
+ arguments: &Arguments,
+ check_id: &str,
+ verifier_sha256: &str,
+ contract: &Value,
+) -> Result<Vec<u8>, String> {
let assertion = json!([{ "id": format!("step_305_gate_01_{GATE_DIGEST}"), "result": "pass" }]);
let result = json!({
"schema": "radroots.services-hardening.rshr-200-step-check-result.v1",
@@ -276,13 +303,131 @@ pub(crate) fn run(arguments: Arguments) -> Result<(), String> {
});
let mut bytes = canonical(&result)?;
bytes.push(b'\n');
- std::io::Write::write_all(&mut std::io::stdout().lock(), &bytes)
- .map_err(|_| "Step 305 result write failed".to_owned())
+ Ok(bytes)
}
-fn valid_hex(value: &str, length: usize) -> bool {
- value.len() == length
- && value
- .bytes()
- .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn command_capture_preserves_status_streams_and_enforces_both_output_limits() {
+ let output = bounded(
+ Command::new("/bin/sh").args(["-c", "printf output; printf diagnostic >&2"]),
+ "fixture",
+ )
+ .unwrap();
+ assert_eq!(output.stdout, b"output");
+ assert_eq!(output.stderr, b"diagnostic");
+ assert_eq!(
+ bounded(Command::new("/bin/sh").args(["-c", "exit 7"]), "fixture").unwrap_err(),
+ "fixture failed"
+ );
+ let missing = tempfile::TempDir::new().unwrap();
+ assert_eq!(
+ bounded(&mut Command::new(missing.path().join("absent")), "fixture").unwrap_err(),
+ "fixture could not start"
+ );
+ for script in ["head -c \"$1\" /dev/zero", "head -c \"$1\" /dev/zero >&2"] {
+ let maximum = MAX_OUTPUT_BYTES.to_string();
+ let output = bounded(
+ Command::new("/bin/sh").args(["-c", script, "fixture", &maximum]),
+ "fixture",
+ )
+ .unwrap();
+ assert_eq!(output.stdout.len() + output.stderr.len(), MAX_OUTPUT_BYTES);
+ let oversized = (MAX_OUTPUT_BYTES + 1).to_string();
+ assert_eq!(
+ bounded(
+ Command::new("/bin/sh").args(["-c", script, "fixture", &oversized]),
+ "fixture"
+ )
+ .unwrap_err(),
+ "fixture exceeded its output bound"
+ );
+ }
+ }
+
+ fn arguments() -> Arguments {
+ Arguments {
+ step: STEP,
+ check_id: format!("gate-01-{GATE_DIGEST}"),
+ source_revision: "a".repeat(40),
+ source_tree: "0".repeat(40),
+ candidate_digest: "none".into(),
+ platform: "macos_aarch64".into(),
+ execution_request_sha256: "1".repeat(64),
+ }
+ }
+
+ #[test]
+ fn invalid_gate_arguments_are_rejected_before_external_work() {
+ assert_eq!(
+ validate_arguments(&arguments()).unwrap(),
+ format!("gate-01-{GATE_DIGEST}")
+ );
+ for field in 0..8 {
+ let mut invalid = arguments();
+ match field {
+ 0 => invalid.step = 0,
+ 1 => invalid.check_id.clear(),
+ 2 => invalid.candidate_digest = "unbound".into(),
+ 3 => invalid.platform = "linux".into(),
+ 4 => invalid.source_revision.clear(),
+ 5 => invalid.source_tree.clear(),
+ 6 => invalid.execution_request_sha256.clear(),
+ _ => invalid.source_revision = "A".repeat(40),
+ }
+ assert_eq!(run(invalid).unwrap_err(), "Step 305 gate arguments differ");
+ }
+ let mut invalid = arguments();
+ invalid.source_tree = "g".repeat(40);
+ assert!(validate_arguments(&invalid).is_err());
+ }
+
+ #[test]
+ fn authority_requires_canonical_bytes_and_exact_retained_bindings() {
+ let raw = include_bytes!("../../../contracts/rshr-202-step-305-gates.v1.json");
+ let authority: Value = serde_json::from_slice(raw).unwrap();
+ let verifier = authority["gate_command_contract"][0]["verifier_sha256"]
+ .as_str()
+ .unwrap();
+ let contract = validate_authority(raw, verifier).unwrap();
+ assert!(validate_authority(raw, &"f".repeat(64)).is_err());
+ assert!(validate_authority(b"invalid", verifier).is_err());
+ assert!(validate_authority(b"{}\n", verifier).is_err());
+ assert!(
+ validate_authority(&serde_json::to_vec_pretty(&authority).unwrap(), verifier).is_err()
+ );
+ for (pointer, value) in [
+ ("/schema", json!("other")),
+ ("/step", json!([0])),
+ ("/gate_command_contract", json!([])),
+ ] {
+ let mut changed = authority.clone();
+ *changed.pointer_mut(pointer).unwrap() = value;
+ let mut bytes = canonical(&changed).unwrap();
+ bytes.push(b'\n');
+ assert!(validate_authority(&bytes, verifier).is_err());
+ }
+ // Encoding fixtures is not a historical gate execution or qualification.
+ let args = arguments();
+ let bytes = result_bytes(&args, &args.check_id, verifier, &contract).unwrap();
+ let result: Value = serde_json::from_slice(&bytes).unwrap();
+ assert!(bytes.ends_with(b"\n"));
+ assert_eq!(result["source_revision"], args.source_revision);
+ assert_eq!(result["source_tree"], args.source_tree);
+ assert_eq!(
+ result["execution_request"][0]["sha256"],
+ args.execution_request_sha256
+ );
+ assert_eq!(
+ result["command_contract_sha256"],
+ sha256(&canonical(&contract).unwrap())
+ );
+ assert_eq!(
+ result["assertion_inventory_sha256"],
+ sha256(&canonical(&result["assertion"]).unwrap())
+ );
+ }
}
diff --git a/tools/xtask/src/rshr_202_step_306_gate.rs b/tools/xtask/src/rshr_202_step_306_gate.rs
@@ -119,7 +119,7 @@ fn expected_contract(verifier_sha256: &str) -> Value {
})
}
-pub(crate) fn run(arguments: Arguments) -> Result<(), String> {
+fn validate_arguments(arguments: &Arguments) -> Result<String, String> {
let check_id = format!("gate-01-{GATE_DIGEST}");
if arguments.step != STEP
|| arguments.check_id != check_id
@@ -131,6 +131,11 @@ pub(crate) fn run(arguments: Arguments) -> Result<(), String> {
{
return Err("Step 306 gate arguments differ".to_owned());
}
+ Ok(check_id)
+}
+
+pub(crate) fn run(arguments: Arguments) -> Result<(), String> {
+ let check_id = validate_arguments(&arguments)?;
let root = root();
if root.join(".github").exists() {
return Err("forbidden .github surface is present".to_owned());
@@ -149,21 +154,7 @@ pub(crate) fn run(arguments: Arguments) -> Result<(), String> {
sha256(&fs::read(verifier_path).map_err(|_| "Step 306 verifier is unreadable".to_owned())?);
let authority_bytes = fs::read(root.join("contracts/rshr-202-step-306-gates.v1.json"))
.map_err(|_| "Step 306 gate authority is unreadable".to_owned())?;
- let authority: Value = serde_json::from_slice(&authority_bytes)
- .map_err(|_| "Step 306 gate authority is invalid".to_owned())?;
- let mut canonical_authority = canonical(&authority)?;
- canonical_authority.push(b'\n');
- let contracts = authority
- .get("gate_command_contract")
- .and_then(Value::as_array)
- .ok_or_else(|| "Step 306 gate contract is absent".to_owned())?;
- if authority_bytes != canonical_authority
- || authority.get("schema") != Some(&json!("radroots.lib.rshr-202-step-306-gates.v1"))
- || authority.get("step") != Some(&json!([STEP]))
- || contracts.as_slice() != [expected_contract(&verifier_sha256)]
- {
- return Err("Step 306 gate authority differs".to_owned());
- }
+ let contract = validate_authority(&authority_bytes, &verifier_sha256)?;
for (arguments, label) in [
(
@@ -232,7 +223,43 @@ pub(crate) fn run(arguments: Arguments) -> Result<(), String> {
"Step 306 contracts",
)?;
- let contract = &contracts[0];
+ let bytes = result_bytes(&arguments, &check_id, &verifier_sha256, &contract)?;
+ std::io::Write::write_all(&mut std::io::stdout().lock(), &bytes)
+ .map_err(|_| "Step 306 result write failed".to_owned())
+}
+
+fn valid_hex(value: &str, length: usize) -> bool {
+ value.len() == length
+ && value
+ .bytes()
+ .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
+}
+
+fn validate_authority(authority_bytes: &[u8], verifier_sha256: &str) -> Result<Value, String> {
+ let authority: Value = serde_json::from_slice(authority_bytes)
+ .map_err(|_| "Step 306 gate authority is invalid".to_owned())?;
+ let mut canonical_authority = canonical(&authority)?;
+ canonical_authority.push(b'\n');
+ let contracts = authority
+ .get("gate_command_contract")
+ .and_then(Value::as_array)
+ .ok_or_else(|| "Step 306 gate contract is absent".to_owned())?;
+ if authority_bytes != canonical_authority
+ || authority.get("schema") != Some(&json!("radroots.lib.rshr-202-step-306-gates.v1"))
+ || authority.get("step") != Some(&json!([STEP]))
+ || contracts.as_slice() != [expected_contract(verifier_sha256)]
+ {
+ return Err("Step 306 gate authority differs".to_owned());
+ }
+ Ok(contracts[0].clone())
+}
+
+fn result_bytes(
+ arguments: &Arguments,
+ check_id: &str,
+ verifier_sha256: &str,
+ contract: &Value,
+) -> Result<Vec<u8>, String> {
let assertion = json!([{ "id": format!("step_306_gate_01_{GATE_DIGEST}"), "result": "pass" }]);
let result = json!({
"schema": "radroots.services-hardening.rshr-200-step-check-result.v1",
@@ -252,13 +279,131 @@ pub(crate) fn run(arguments: Arguments) -> Result<(), String> {
});
let mut bytes = canonical(&result)?;
bytes.push(b'\n');
- std::io::Write::write_all(&mut std::io::stdout().lock(), &bytes)
- .map_err(|_| "Step 306 result write failed".to_owned())
+ Ok(bytes)
}
-fn valid_hex(value: &str, length: usize) -> bool {
- value.len() == length
- && value
- .bytes()
- .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn command_capture_preserves_status_streams_and_enforces_both_output_limits() {
+ let output = bounded(
+ Command::new("/bin/sh").args(["-c", "printf output; printf diagnostic >&2"]),
+ "fixture",
+ )
+ .unwrap();
+ assert_eq!(output.stdout, b"output");
+ assert_eq!(output.stderr, b"diagnostic");
+ assert_eq!(
+ bounded(Command::new("/bin/sh").args(["-c", "exit 7"]), "fixture").unwrap_err(),
+ "fixture failed"
+ );
+ let missing = tempfile::TempDir::new().unwrap();
+ assert_eq!(
+ bounded(&mut Command::new(missing.path().join("absent")), "fixture").unwrap_err(),
+ "fixture could not start"
+ );
+ for script in ["head -c \"$1\" /dev/zero", "head -c \"$1\" /dev/zero >&2"] {
+ let maximum = MAX_OUTPUT_BYTES.to_string();
+ let output = bounded(
+ Command::new("/bin/sh").args(["-c", script, "fixture", &maximum]),
+ "fixture",
+ )
+ .unwrap();
+ assert_eq!(output.stdout.len() + output.stderr.len(), MAX_OUTPUT_BYTES);
+ let oversized = (MAX_OUTPUT_BYTES + 1).to_string();
+ assert_eq!(
+ bounded(
+ Command::new("/bin/sh").args(["-c", script, "fixture", &oversized]),
+ "fixture"
+ )
+ .unwrap_err(),
+ "fixture exceeded its output bound"
+ );
+ }
+ }
+
+ fn arguments() -> Arguments {
+ Arguments {
+ step: STEP,
+ check_id: format!("gate-01-{GATE_DIGEST}"),
+ source_revision: "a".repeat(40),
+ source_tree: "0".repeat(40),
+ candidate_digest: "none".into(),
+ platform: "macos_aarch64".into(),
+ execution_request_sha256: "1".repeat(64),
+ }
+ }
+
+ #[test]
+ fn invalid_gate_arguments_are_rejected_before_external_work() {
+ assert_eq!(
+ validate_arguments(&arguments()).unwrap(),
+ format!("gate-01-{GATE_DIGEST}")
+ );
+ for field in 0..8 {
+ let mut invalid = arguments();
+ match field {
+ 0 => invalid.step = 0,
+ 1 => invalid.check_id.clear(),
+ 2 => invalid.candidate_digest = "unbound".into(),
+ 3 => invalid.platform = "linux".into(),
+ 4 => invalid.source_revision.clear(),
+ 5 => invalid.source_tree.clear(),
+ 6 => invalid.execution_request_sha256.clear(),
+ _ => invalid.source_revision = "A".repeat(40),
+ }
+ assert_eq!(run(invalid).unwrap_err(), "Step 306 gate arguments differ");
+ }
+ let mut invalid = arguments();
+ invalid.source_tree = "g".repeat(40);
+ assert!(validate_arguments(&invalid).is_err());
+ }
+
+ #[test]
+ fn authority_requires_canonical_bytes_and_exact_retained_bindings() {
+ let raw = include_bytes!("../../../contracts/rshr-202-step-306-gates.v1.json");
+ let authority: Value = serde_json::from_slice(raw).unwrap();
+ let verifier = authority["gate_command_contract"][0]["verifier_sha256"]
+ .as_str()
+ .unwrap();
+ let contract = validate_authority(raw, verifier).unwrap();
+ assert!(validate_authority(raw, &"f".repeat(64)).is_err());
+ assert!(validate_authority(b"invalid", verifier).is_err());
+ assert!(validate_authority(b"{}\n", verifier).is_err());
+ assert!(
+ validate_authority(&serde_json::to_vec_pretty(&authority).unwrap(), verifier).is_err()
+ );
+ for (pointer, value) in [
+ ("/schema", json!("other")),
+ ("/step", json!([0])),
+ ("/gate_command_contract", json!([])),
+ ] {
+ let mut changed = authority.clone();
+ *changed.pointer_mut(pointer).unwrap() = value;
+ let mut bytes = canonical(&changed).unwrap();
+ bytes.push(b'\n');
+ assert!(validate_authority(&bytes, verifier).is_err());
+ }
+ // Encoding fixtures is not a historical gate execution or qualification.
+ let args = arguments();
+ let bytes = result_bytes(&args, &args.check_id, verifier, &contract).unwrap();
+ let result: Value = serde_json::from_slice(&bytes).unwrap();
+ assert!(bytes.ends_with(b"\n"));
+ assert_eq!(result["source_revision"], args.source_revision);
+ assert_eq!(result["source_tree"], args.source_tree);
+ assert_eq!(
+ result["execution_request"][0]["sha256"],
+ args.execution_request_sha256
+ );
+ assert_eq!(
+ result["command_contract_sha256"],
+ sha256(&canonical(&contract).unwrap())
+ );
+ assert_eq!(
+ result["assertion_inventory_sha256"],
+ sha256(&canonical(&result["assertion"]).unwrap())
+ );
+ }
}
diff --git a/tools/xtask/src/safe_artifact_io.rs b/tools/xtask/src/safe_artifact_io.rs
@@ -2385,6 +2385,161 @@ mod tests {
}
#[test]
+ fn zero_limits_and_unsafe_names_fail_before_filesystem_admission() {
+ for mutate in [
+ |limits: &mut TraversalLimits| limits.max_entries = 0,
+ |limits: &mut TraversalLimits| limits.max_files = 0,
+ |limits: &mut TraversalLimits| limits.max_total_bytes = 0,
+ |limits: &mut TraversalLimits| limits.max_file_bytes = 0,
+ |limits: &mut TraversalLimits| limits.max_depth = 0,
+ |limits: &mut TraversalLimits| limits.max_path_bytes = 0,
+ ] {
+ let mut limits = traversal_limits();
+ mutate(&mut limits);
+ assert_eq!(
+ validate_traversal_limits(limits).unwrap_err().kind(),
+ ArtifactIoFailureKind::InvalidRequest
+ );
+ }
+ for mutate in [
+ |limits: &mut TarGzipLimits| limits.max_compressed_bytes = 0,
+ |limits: &mut TarGzipLimits| limits.max_expanded_bytes = 0,
+ |limits: &mut TarGzipLimits| limits.max_members = 0,
+ |limits: &mut TarGzipLimits| limits.max_member_bytes = 0,
+ |limits: &mut TarGzipLimits| limits.max_payload_bytes = 0,
+ |limits: &mut TarGzipLimits| limits.max_depth = 0,
+ |limits: &mut TarGzipLimits| limits.max_path_bytes = 0,
+ ] {
+ let mut limits = archive_limits();
+ mutate(&mut limits);
+ assert_eq!(
+ validate_archive_limits(limits).unwrap_err().kind(),
+ ArtifactIoFailureKind::InvalidRequest
+ );
+ }
+ let directory = TempDir::new().unwrap();
+ let root = root(&directory);
+ fs::write(root.join("input"), b"x").unwrap();
+ assert_eq!(
+ read_regular(&root, Path::new("input"), 0)
+ .unwrap_err()
+ .kind(),
+ ArtifactIoFailureKind::InvalidRequest
+ );
+ assert_eq!(
+ hash_regular(&root, Path::new("input"), 0)
+ .unwrap_err()
+ .kind(),
+ ArtifactIoFailureKind::InvalidRequest
+ );
+ for excluded in ["", "a/b", "a\\b", ".", ".."] {
+ assert!(traverse_regular_files(&root, traversal_limits(), &[excluded]).is_err());
+ }
+ for path in ["", "/absolute", "../escape"] {
+ assert!(validate_relative(Path::new(path)).is_err());
+ }
+ for name in ["", "/absolute", "nested/file", ".", ".."] {
+ assert!(validate_single_component(OsStr::new(name)).is_err());
+ }
+ for path in [Path::new("relative"), Path::new("/")] {
+ assert!(copy_regular_to_new_path(&root.join("input"), path, 1).is_err());
+ }
+ assert!(split_absolute_file(Path::new("relative")).is_err());
+ assert_eq!(fs::read(root.join("input")).unwrap(), b"x");
+ assert!(open_absolute_directory(Path::new("relative")).is_err());
+ assert!(open_trusted_output_directory(Path::new("relative")).is_err());
+ assert!(open_relative(&root, Path::new("input/child"), ObjectKind::Regular).is_err());
+ }
+
+ #[test]
+ fn bounded_streams_keep_exact_prefixes_and_fail_on_the_next_byte() {
+ let mut reader = LimitReader::new(io::Cursor::new(b"abcd"), 3);
+ assert_eq!(reader.read(&mut []).unwrap(), 0);
+ let mut exact = [0; 3];
+ reader.read_exact(&mut exact).unwrap();
+ assert_eq!(&exact, b"abc");
+ assert!(!reader.exceeded());
+ assert!(reader.read(&mut [0; 1]).is_err());
+ assert!(reader.exceeded());
+ let mut writer = HashingLimitWriter::new(3);
+ writer.write_all(b"abc").unwrap();
+ assert_eq!(writer.write(&[]).unwrap(), 0);
+ writer.flush().unwrap();
+ assert!(writer.write_all(b"d").is_err());
+ assert!(writer.exceeded());
+ let evidence = writer.finalize();
+ assert_eq!(evidence.byte_length, 3);
+ assert_eq!(evidence.sha256, hex::encode(Sha256::digest(b"abc")));
+ let directory = TempDir::new().unwrap();
+ let path = directory.path().join("input");
+ fs::write(&path, b"abcd").unwrap();
+ let mut file = File::open(&path).unwrap();
+ let mut reader = HashingLimitReader::new(&mut file, 3);
+ assert_eq!(reader.read(&mut []).unwrap(), 0);
+ reader.read_exact(&mut exact).unwrap();
+ assert!(reader.read(&mut [0; 1]).is_err());
+ assert!(reader.exceeded());
+ assert_eq!(reader.finalize(), hex::encode(Sha256::digest(b"abc")));
+ assert_eq!(
+ stream_regular(&mut File::open(&path).unwrap(), 3, None)
+ .unwrap_err()
+ .kind(),
+ ArtifactIoFailureKind::LimitExceeded(LimitKind::FileBytes)
+ );
+ }
+
+ #[test]
+ fn archive_names_and_headers_reject_noncanonical_and_conflicting_members() {
+ for path in [
+ b"".as_slice(),
+ b"/root",
+ b"a\0b",
+ b"a\\b",
+ &[255],
+ b"a//b",
+ b"a/../b",
+ b"./a",
+ ] {
+ assert!(validate_archive_path(path, 4, 128).is_err());
+ }
+ validate_archive_path(b"dir/file", 2, 8).unwrap();
+ assert!(validate_archive_path(b"dir/file", 1, 8).is_err());
+ assert!(validate_archive_path(b"dir/file", 2, 7).is_err());
+ let mut names = BTreeMap::new();
+ admit_archive_name(&mut names, b"dir/", ArchiveMemberKind::Directory).unwrap();
+ admit_archive_name(&mut names, b"dir/file", ArchiveMemberKind::File).unwrap();
+ assert!(admit_archive_name(&mut names, b"dir/file", ArchiveMemberKind::File).is_err());
+ assert!(
+ admit_archive_name(&mut names, b"dir/file/child", ArchiveMemberKind::File).is_err()
+ );
+ let mut reverse = BTreeMap::new();
+ admit_archive_name(&mut reverse, b"dir/child", ArchiveMemberKind::File).unwrap();
+ assert!(admit_archive_name(&mut reverse, b"dir", ArchiveMemberKind::File).is_err());
+ let directory = TempDir::new().unwrap();
+ let path = directory.path().join("header");
+ let canonical = [0x1f, 0x8b, 8, 0, 0, 0, 0, 0, 0, 255];
+ for index in [0, 3, 4, 8, 9] {
+ let mut bytes = canonical;
+ bytes[index] ^= 1;
+ fs::write(&path, bytes).unwrap();
+ let mut file = File::open(&path).unwrap();
+ let mut reader = BufReader::new(HashingLimitReader::new(&mut file, 10));
+ assert!(
+ validate_gzip_header(&mut reader, TarGzipPolicy::DeterministicSnapshot).is_err()
+ );
+ }
+ fs::write(&path, canonical).unwrap();
+ let mut file = File::open(&path).unwrap();
+ let mut reader = BufReader::new(HashingLimitReader::new(&mut file, 9));
+ assert_eq!(
+ validate_gzip_header(&mut reader, TarGzipPolicy::DeterministicSnapshot)
+ .unwrap_err()
+ .kind(),
+ ArtifactIoFailureKind::LimitExceeded(LimitKind::ArchiveCompressedBytes)
+ );
+ }
+
+ #[test]
fn hard_maximums_reject_invalid_requests() {
assert_eq!(HARD_MAX_BUFFERED_READ_BYTES, 67_108_864);
assert_eq!(HARD_MAX_STREAM_FILE_BYTES, 17_179_869_184);
@@ -2864,6 +3019,53 @@ mod tests {
);
}
+ #[test]
+ fn retained_traversal_rejects_root_directory_member_and_file_replacement() {
+ for case in [
+ "root",
+ "directory",
+ "mode",
+ "file",
+ "members",
+ "member-name",
+ ] {
+ let directory = TempDir::new().unwrap();
+ let base = root(&directory);
+ let tree = base.join("tree");
+ fs::create_dir_all(tree.join("nested")).unwrap();
+ fs::write(tree.join("nested/file"), b"original").unwrap();
+ let snapshot = traverse_regular_files(&tree, traversal_limits(), &[]).unwrap();
+ snapshot.revalidate().unwrap();
+ match case {
+ "root" => {
+ fs::rename(&tree, base.join("old-tree")).unwrap();
+ fs::create_dir_all(tree.join("nested")).unwrap();
+ fs::write(tree.join("nested/file"), b"original").unwrap();
+ }
+ "directory" => {
+ fs::rename(tree.join("nested"), base.join("old-nested")).unwrap();
+ fs::create_dir(tree.join("nested")).unwrap();
+ fs::write(tree.join("nested/file"), b"original").unwrap();
+ }
+ "mode" => {
+ fs::set_permissions(tree.join("nested"), fs::Permissions::from_mode(0o700))
+ .unwrap()
+ }
+ "file" => fs::write(tree.join("nested/file"), b"different length").unwrap(),
+ "members" => fs::write(tree.join("nested/added"), b"new").unwrap(),
+ "member-name" => {
+ fs::rename(tree.join("nested/file"), tree.join("nested/renamed")).unwrap()
+ }
+ _ => unreachable!(),
+ }
+ assert_eq!(
+ snapshot.revalidate().unwrap_err().kind(),
+ ArtifactIoFailureKind::ChangedDuringRead,
+ "{case}"
+ );
+ }
+ }
+
fn write_archive(path: &Path, members: &[(&str, EntryType, &[u8])]) {
let output = File::create(path).expect("archive output");
let encoder = GzBuilder::new().write(output, Compression::fast());
@@ -3425,4 +3627,66 @@ mod step_294_tests {
ArtifactIoFailureKind::ChangedDuringRead
);
}
+
+ #[test]
+ fn materialization_rejects_parent_root_and_ancestor_rebinding() {
+ let source = trusted_tempdir("radroots-materialization-source-");
+ let source_root = root(&source);
+ write_deterministic_archive(&source_root.join("archive.tar.gz"), b"immutable", false);
+ for case in [
+ "parent-mode",
+ "root-mode",
+ "root-replacement",
+ "ancestor-replacement",
+ "ancestor-mode",
+ ] {
+ let parent = trusted_tempdir("radroots-materialization-parent-");
+ let base = root(&parent);
+ let ancestor = base.join("ancestor");
+ let output = ancestor.join("output");
+ fs::create_dir_all(&output).unwrap();
+ fs::set_permissions(&ancestor, fs::Permissions::from_mode(0o700)).unwrap();
+ fs::set_permissions(&output, fs::Permissions::from_mode(0o700)).unwrap();
+ let materialized = materialize_tar_gzip_relative(
+ &source_root,
+ Path::new("archive.tar.gz"),
+ &output,
+ archive_limits(),
+ None,
+ TarGzipPolicy::DeterministicSnapshot,
+ )
+ .unwrap();
+ materialized.revalidate().unwrap();
+ match case {
+ "parent-mode" => {
+ fs::set_permissions(&output, fs::Permissions::from_mode(0o750)).unwrap()
+ }
+ "root-mode" => {
+ fs::set_permissions(materialized.root(), fs::Permissions::from_mode(0o750))
+ .unwrap()
+ }
+ "root-replacement" => {
+ fs::rename(materialized.root(), base.join("retained-root")).unwrap();
+ fs::create_dir(materialized.root()).unwrap();
+ fs::set_permissions(materialized.root(), fs::Permissions::from_mode(0o700))
+ .unwrap();
+ }
+ "ancestor-replacement" => {
+ fs::rename(&ancestor, base.join("retained-ancestor")).unwrap();
+ fs::create_dir(&ancestor).unwrap();
+ fs::set_permissions(&ancestor, fs::Permissions::from_mode(0o700)).unwrap();
+ fs::rename(base.join("retained-ancestor/output"), &output).unwrap();
+ }
+ "ancestor-mode" => {
+ fs::set_permissions(&ancestor, fs::Permissions::from_mode(0o750)).unwrap()
+ }
+ _ => unreachable!(),
+ }
+ assert_eq!(
+ materialized.revalidate().unwrap_err().kind(),
+ ArtifactIoFailureKind::ChangedDuringRead,
+ "{case}"
+ );
+ }
+ }
}
diff --git a/tools/xtask/src/service_build_qualification.rs b/tools/xtask/src/service_build_qualification.rs
@@ -455,6 +455,67 @@ mod tests {
}
#[test]
+ fn canonical_fixture_locks_bind_service_and_each_positive_contract_version() {
+ for (from, to) in [
+ (
+ "service = \"fixture_service\"",
+ "service = \"other_service\"",
+ ),
+ ("config = 1", "config = 9"),
+ ("state = 2", "state = 9"),
+ ("admin = 3", "admin = 9"),
+ ("status = 4", "status = 9"),
+ ("provider = 5", "provider = 9"),
+ (
+ "material = \"absent\"",
+ "material = \"deferred\"\nlib_revision = \"1111111111111111111111111111111111111111\"\nflake_lock_sha256 = \"1111111111111111111111111111111111111111111111111111111111111111\"",
+ ),
+ ] {
+ let root = copied_fixture();
+ validate_fixture(root.path()).unwrap();
+ let path = root.path().join(FIXTURE_RELATIVE).join(LOCK_FILENAME);
+ let original = fs::read_to_string(&path).unwrap();
+ assert!(original.contains(from));
+ let changed = original.replacen(from, to, 1);
+ ServiceSourceLockV2::from_canonical_bytes(changed.as_bytes())
+ .expect("valid canonical lock with substituted identity");
+ fs::write(&path, changed).unwrap();
+ assert_eq!(
+ validate_fixture(root.path()),
+ Err(BuildQualificationError::InvalidFixture),
+ "{from}"
+ );
+ }
+ let root = copied_fixture();
+ let path = root.path().join(FIXTURE_RELATIVE).join("Cargo.toml");
+ let original = fs::read_to_string(&path).unwrap();
+ let changed = original.replace("nix_material = \"absent\"", "nix_material = \"deferred\"");
+ assert_ne!(changed, original);
+ fs::write(path, changed).unwrap();
+ assert_eq!(
+ validate_fixture(root.path()),
+ Err(BuildQualificationError::InvalidFixture)
+ );
+ }
+
+ #[test]
+ fn bound_contracts_reject_digest_substitution_and_unsafe_file_inputs() {
+ let root = TempDir::new().unwrap();
+ let path = root.path().join("input");
+ fs::write(&path, b"{}").unwrap();
+ validate_bound_contract(root.path(), "input", &digest(b"{}")).unwrap();
+ assert!(validate_bound_contract(root.path(), "input", &"1".repeat(64)).is_err());
+ assert!(read_bounded(&path, 1, BuildQualificationError::InvalidContract).is_err());
+ assert!(read_bounded(root.path(), 10, BuildQualificationError::InvalidContract).is_err());
+ #[cfg(unix)]
+ {
+ let link = root.path().join("link");
+ std::os::unix::fs::symlink(&path, &link).unwrap();
+ assert!(read_bounded(&link, 10, BuildQualificationError::InvalidContract).is_err());
+ }
+ }
+
+ #[test]
fn fixture_rejects_every_independent_metadata_drift() {
for (section, field, replacement) in [
(
diff --git a/tools/xtask/src/service_release_artifacts.rs b/tools/xtask/src/service_release_artifacts.rs
@@ -2758,6 +2758,7 @@ fn validate_decision(decision: &ReleaseDecision) -> Result<(), ReleaseArtifactEr
#[cfg(test)]
mod tests {
+ use serde_json::{Value, json};
use std::process::Command;
use crate::service_source_lock::ContractVersions;
@@ -3172,6 +3173,248 @@ version = "0.1.0-alpha"
}
#[test]
+ fn oci_admission_reconciles_manifest_config_and_layer_content() {
+ let temporary = TempDir::new().unwrap();
+ let root = temporary.path().canonicalize().unwrap();
+ let original = root.join("original.tar.gz");
+ let revision = "a".repeat(40);
+ create_oci_fixture(&original, &revision, &revision);
+ let expected = artifact_admission::OciExpectation {
+ service: "myc",
+ binary_name: "fixture-service",
+ version: "0.1.0-alpha",
+ service_revision: &revision,
+ lib_revision: &revision,
+ license: "AGPL-3.0-or-later",
+ contract_versions: artifact_admission::ContractVersions {
+ admin: 1,
+ config: 1,
+ provider: 1,
+ state: 1,
+ status: 1,
+ },
+ };
+ artifact_admission::admit_oci(&original, &root, &expected).unwrap();
+ let decoder = flate2::read::GzDecoder::new(fs::File::open(&original).unwrap());
+ let mut archive = tar::Archive::new(decoder);
+ let mut original_members = BTreeMap::new();
+ let mut directories = BTreeSet::new();
+ for entry in archive.entries().unwrap() {
+ let mut entry = entry.unwrap();
+ let name = entry.path().unwrap().to_str().unwrap().to_owned();
+ if entry.header().entry_type().is_dir() {
+ directories.insert(name.clone());
+ }
+ let mut bytes = Vec::new();
+ entry.read_to_end(&mut bytes).unwrap();
+ original_members.insert(name, bytes);
+ }
+ for case in 0..14 {
+ let mut members = original_members.clone();
+ let mut manifest: Value = serde_json::from_slice(&members["manifest.json"]).unwrap();
+ match case {
+ 0 => manifest = json!([]),
+ 1 => manifest[0]["Config"] = json!("wrong.extension"),
+ 2 => manifest[0]["Config"] = json!("invalid.json"),
+ 3 => manifest[0]["RepoTags"] = json!(["wrong:tag"]),
+ 4 => manifest[0]["Layers"] = json!([]),
+ 5 => manifest[0]["Layers"] = json!(["one", "two", "three"]),
+ 6 => {
+ manifest[0]["Layers"] = json!([
+ manifest[0]["Layers"][0].clone(),
+ manifest[0]["Layers"][0].clone()
+ ])
+ }
+ 7 => {
+ members
+ .get_mut(manifest[0]["Config"].as_str().unwrap())
+ .unwrap()
+ .push(b' ');
+ }
+ 8..=10 => {
+ let old_name = manifest[0]["Config"].as_str().unwrap().to_owned();
+ let mut config: Value =
+ serde_json::from_slice(&members.remove(&old_name).unwrap()).unwrap();
+ match case {
+ 8 => config["rootfs"]["type"] = json!("unknown"),
+ 9 => config["rootfs"]["diff_ids"] = json!([]),
+ _ => {
+ config["rootfs"]["diff_ids"] =
+ json!([format!("sha256:{}", "0".repeat(64))])
+ }
+ }
+ let bytes = serde_json::to_vec(&config).unwrap();
+ let name = format!("{}.json", sha256_bytes(&bytes));
+ members.insert(name.clone(), bytes);
+ manifest[0]["Config"] = json!(name);
+ }
+ 11 => {
+ members.remove(manifest[0]["Layers"][0].as_str().unwrap());
+ }
+ 12 => {
+ members.insert("unexpected".to_owned(), b"extra".to_vec());
+ }
+ _ => {}
+ }
+ members.insert(
+ "manifest.json".to_owned(),
+ serde_json::to_vec(&manifest).unwrap(),
+ );
+ let path = root.join(format!("invalid-{case}.tar.gz"));
+ let encoder = GzBuilder::new()
+ .mtime(0)
+ .operating_system(255)
+ .write(fs::File::create(&path).unwrap(), Compression::best());
+ let mut archive = TarBuilder::new(encoder);
+ for (name, bytes) in members {
+ let mut header = TarHeader::new_gnu();
+ let directory = directories.contains(&name);
+ header.set_entry_type(if directory {
+ tar::EntryType::Directory
+ } else {
+ tar::EntryType::Regular
+ });
+ header.set_size(bytes.len() as u64);
+ header.set_mode(if directory { 0o755 } else { 0o644 });
+ header.set_uid(0);
+ header.set_gid(0);
+ header.set_mtime(0);
+ header.set_cksum();
+ archive
+ .append_data(&mut header, name, bytes.as_slice())
+ .unwrap();
+ }
+ archive.into_inner().unwrap().finish().unwrap();
+ let admitted = artifact_admission::admit_oci(&path, &root, &expected);
+ if case == 13 {
+ admitted.expect("rewritten unmodified OCI fixture remains valid");
+ } else {
+ assert!(admitted.is_err(), "OCI case {case}");
+ }
+ }
+ for case in 0..5 {
+ let mut changed = artifact_admission::OciExpectation { ..expected };
+ match case {
+ 0 => changed.license = "MIT",
+ 1 => changed.service = "../escape",
+ 2 => changed.binary_name = "../escape",
+ 3 => changed.service_revision = "invalid",
+ _ => changed.lib_revision = "invalid",
+ }
+ assert!(artifact_admission::admit_oci(&original, &root, &changed).is_err());
+ }
+ }
+
+ #[test]
+ fn dependency_license_admission_preserves_exact_text_and_rejects_missing_or_unsafe_paths() {
+ let temporary = TempDir::new().unwrap();
+ let root = temporary.path().canonicalize().unwrap();
+ let mut dependency = package(
+ "fixture",
+ "fixture",
+ Some("registry+https://github.com/rust-lang/crates.io-index"),
+ Some(&"a".repeat(64)),
+ Some("MIT"),
+ false,
+ );
+ dependency.manifest_path = root.join("Cargo.toml").to_str().unwrap().to_owned();
+ fs::write(root.join("Cargo.toml"), b"fixture").unwrap();
+ assert!(dependency_license_texts(&dependency).is_err());
+ let text = "Fixture copyright\nPermission is granted.\n";
+ fs::write(root.join("LICENSE"), text).unwrap();
+ fs::write(root.join("COPYING-MIT"), "Copying terms\n").unwrap();
+ fs::write(root.join("NOTICE.txt"), "Notice terms\n").unwrap();
+ let texts = dependency_license_texts(&dependency).unwrap();
+ assert_eq!(
+ texts
+ .iter()
+ .map(|row| row.filename.as_str())
+ .collect::<Vec<_>>(),
+ ["COPYING-MIT", "LICENSE", "NOTICE.txt"]
+ );
+ assert_eq!(texts[1].text, text);
+ assert_eq!(texts[1].sha256, sha256_bytes(text.as_bytes()));
+ dependency.license_file = Some("LICENSE".to_owned());
+ assert_eq!(dependency_license_texts(&dependency).unwrap().len(), 1);
+ for path in ["../outside", "/absolute", "./LICENSE", "missing"] {
+ dependency.license_file = Some(path.to_owned());
+ assert!(dependency_license_texts(&dependency).is_err(), "{path}");
+ }
+ for path in [
+ "relative/Cargo.toml".to_owned(),
+ root.join("not-a-manifest").to_str().unwrap().to_owned(),
+ ] {
+ dependency.manifest_path = path;
+ assert!(dependency_license_texts(&dependency).is_err());
+ }
+ }
+
+ #[test]
+ fn dependency_license_content_and_inventory_are_bounded() {
+ let temporary = TempDir::new().unwrap();
+ let root = temporary.path().canonicalize().unwrap();
+ let mut dependency = package(
+ "fixture",
+ "fixture",
+ Some("registry+https://github.com/rust-lang/crates.io-index"),
+ Some(&"a".repeat(64)),
+ Some("MIT"),
+ false,
+ );
+ dependency.manifest_path = root.join("Cargo.toml").to_str().unwrap().to_owned();
+ dependency.license_file = Some("LICENSE".to_owned());
+ for bytes in [
+ b" \n\t".as_slice(),
+ &[255, 254],
+ b"-----BEGIN PRIVATE KEY-----\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n-----END PRIVATE KEY-----",
+ ] {
+ fs::write(root.join("LICENSE"), bytes).unwrap();
+ assert!(dependency_license_texts(&dependency).is_err());
+ }
+ let file = fs::File::create(root.join("LICENSE")).unwrap();
+ file.set_len(MAX_TEXT_INPUT_BYTES + 1).unwrap();
+ assert!(dependency_license_texts(&dependency).is_err());
+ drop(file);
+ fs::write(root.join("LICENSE"), b"Valid terms\n").unwrap();
+ dependency.license_file = None;
+ for index in 0..16 {
+ fs::write(root.join(format!("LICENSE-{index}")), b"Terms\n").unwrap();
+ }
+ assert!(dependency_license_texts(&dependency).is_err());
+ for index in 0..256 {
+ fs::write(root.join(format!("ordinary-{index}")), b"").unwrap();
+ }
+ assert!(dependency_license_texts(&dependency).is_err());
+ }
+
+ #[cfg(unix)]
+ #[test]
+ fn dependency_license_paths_cannot_escape_the_package_root() {
+ use std::os::unix::fs::symlink;
+ let temporary = TempDir::new().unwrap();
+ let root = temporary.path().canonicalize().unwrap();
+ let package_root = root.join("package");
+ fs::create_dir(&package_root).unwrap();
+ let mut dependency = package(
+ "fixture",
+ "fixture",
+ Some("registry+https://github.com/rust-lang/crates.io-index"),
+ Some(&"a".repeat(64)),
+ Some("MIT"),
+ false,
+ );
+ dependency.manifest_path = package_root.join("Cargo.toml").to_str().unwrap().to_owned();
+ fs::write(root.join("outside"), b"Outside terms\n").unwrap();
+ symlink(root.join("outside"), package_root.join("LICENSE")).unwrap();
+ assert!(dependency_license_texts(&dependency).is_err());
+ fs::create_dir(package_root.join("nested")).unwrap();
+ fs::write(package_root.join("nested/terms"), b"Nested terms\n").unwrap();
+ dependency.license_file = Some("nested/terms".to_owned());
+ assert!(dependency_license_texts(&dependency).is_err());
+ assert_eq!(fs::read(root.join("outside")).unwrap(), b"Outside terms\n");
+ }
+
+ #[test]
fn contract_matches_the_checked_in_decision() {
let root = Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
@@ -3325,6 +3568,42 @@ version = "0.1.0-alpha"
}
#[test]
+ fn license_documents_require_exact_bounded_nonempty_text_and_safe_filenames() {
+ build_supply_chain_documents(&sample_metadata(), sample_cargo_metadata()).unwrap();
+ for case in ["missing", "filename", "empty", "oversized"] {
+ let mut changed = sample_cargo_metadata();
+ let package = &mut changed.packages[1];
+ match case {
+ "missing" => package.license_texts.clear(),
+ "filename" => package.license_texts[0].filename = "../LICENSE".into(),
+ "empty" => package.license_texts[0].text = " \n".into(),
+ "oversized" => {
+ package.license_texts[0].text =
+ "x".repeat(MAX_GENERATED_DOCUMENT_BYTES as usize + 1)
+ }
+ _ => unreachable!(),
+ }
+ if let Some(text) = package.license_texts.first_mut() {
+ text.sha256 = sha256_bytes(text.text.as_bytes());
+ }
+ assert!(
+ matches!(
+ build_supply_chain_documents(&sample_metadata(), changed),
+ Err(ReleaseArtifactError::InvalidPackageInventory)
+ ),
+ "{case}"
+ );
+ }
+ let mut without_newline = sample_cargo_metadata();
+ let text = &mut without_newline.packages[1].license_texts[0];
+ text.text = "Exact license terms".into();
+ text.sha256 = sha256_bytes(text.text.as_bytes());
+ let (_, _, document) =
+ build_supply_chain_documents(&sample_metadata(), without_newline).unwrap();
+ assert!(document.ends_with("Exact license terms\n"));
+ }
+
+ #[test]
fn private_or_incomplete_dependency_evidence_is_rejected() {
let root_id = "root";
for dependency in [
@@ -4380,6 +4659,88 @@ version = "0.1.0-alpha"
}
#[test]
+ fn release_metadata_rejects_well_typed_but_invalid_identifiers_and_versions() {
+ let temporary = TempDir::new().unwrap();
+ let original: toml::Value = toml::from_str(
+ r#"
+ [workspace.package]
+ license = "AGPL-3.0-or-later"
+ [workspace.metadata.radroots.service_release]
+ service = "myc"
+ service_package = "fixture-service"
+ binary_name = "fixture-service"
+ version = "0.1.0-alpha"
+ "#,
+ )
+ .unwrap();
+ let manifest = temporary.path().join("Cargo.toml");
+ fs::write(&manifest, toml::to_string(&original).unwrap()).unwrap();
+ read_release_metadata(temporary.path()).unwrap();
+ for (field, replacement) in [
+ ("service", "Invalid".to_owned()),
+ ("service_package", "Invalid".to_owned()),
+ ("binary_name", "../escape".to_owned()),
+ ("version", "x".repeat(129)),
+ ("version", "invalid".to_owned()),
+ ("version", "01.0.0".to_owned()),
+ ] {
+ let mut value = original.clone();
+ value["workspace"]["metadata"]["radroots"]["service_release"][field] =
+ toml::Value::String(replacement);
+ fs::write(&manifest, toml::to_string(&value).unwrap()).unwrap();
+ assert!(matches!(
+ read_release_metadata(temporary.path()),
+ Err(ReleaseArtifactError::InvalidServiceMetadata)
+ ));
+ }
+ let mut value = original;
+ value["workspace"]["package"]["license"] = toml::Value::String("MIT".to_owned());
+ fs::write(&manifest, toml::to_string(&value).unwrap()).unwrap();
+ assert!(matches!(
+ read_release_metadata(temporary.path()),
+ Err(ReleaseArtifactError::InvalidServiceMetadata)
+ ));
+ }
+
+ #[test]
+ fn sbom_dependency_edges_composition_and_artifact_hashes_are_exact() {
+ let artifacts = vec![ArtifactRecord {
+ path: "binary.tar.gz".to_owned(),
+ byte_length: 42,
+ sha256: "a".repeat(64),
+ }];
+ for case in 0..7 {
+ let (mut sbom, _, _) =
+ build_supply_chain_documents(&sample_metadata(), sample_cargo_metadata()).unwrap();
+ reconcile_sbom_artifacts(&mut sbom, &artifacts);
+ validate_cyclonedx_profile(&sbom, &artifacts).unwrap();
+ match case {
+ 0 => sbom.components[0].bom_ref = sbom.metadata.component.bom_ref.clone(),
+ 1 => {
+ sbom.dependencies.pop();
+ }
+ 2 => sbom.dependencies[0].reference = "unknown".to_owned(),
+ 3 => sbom.dependencies[0].depends_on.push("unknown".to_owned()),
+ 4 => sbom.compositions[0].aggregate = "incomplete",
+ 5 => sbom.compositions[0].assemblies.clear(),
+ _ => {
+ let artifact = sbom
+ .components
+ .iter_mut()
+ .find(|row| row.bom_ref.starts_with("artifact:"))
+ .unwrap();
+ artifact.hashes[0].content = "b".repeat(64);
+ }
+ }
+ assert_eq!(
+ validate_cyclonedx_profile(&sbom, &artifacts),
+ Err(ReleaseArtifactError::GenerationFailure),
+ "SBOM case {case}"
+ );
+ }
+ }
+
+ #[test]
fn schema_reconciliation_attribution_and_subjects_fail_closed() {
let (mut sbom, _, _) =
build_supply_chain_documents(&sample_metadata(), sample_cargo_metadata())
@@ -4426,6 +4787,13 @@ version = "0.1.0-alpha"
);
validate_provenance_subjects(&provenance, &"a".repeat(64), &artifacts)
.expect("exact provenance subjects");
+ provenance.statement_type = "unknown";
+ assert!(validate_provenance_subjects(&provenance, &"a".repeat(64), &artifacts).is_err());
+ provenance.statement_type = "https://in-toto.io/Statement/v1";
+ provenance.predicate_type = "unknown";
+ assert!(validate_provenance_subjects(&provenance, &"a".repeat(64), &artifacts).is_err());
+ provenance.predicate_type = "https://slsa.dev/provenance/v1";
+ assert!(validate_provenance_subjects(&provenance, &"b".repeat(64), &artifacts).is_err());
provenance.subject.clear();
assert_eq!(
validate_provenance_subjects(&provenance, &"a".repeat(64), &artifacts),
@@ -4446,39 +4814,48 @@ version = "0.1.0-alpha"
nested_members_scanned: 1,
expanded_bytes_scanned: 42,
};
- let mut manifest = ArtifactManifestDocument {
- schema: "radroots.service.release-artifacts.v2",
- contract_version: 2,
- candidate_digest: "a".repeat(64),
- service: "myc".to_owned(),
- version: "0.1.0-alpha".to_owned(),
- target: native_fixture_target().to_owned(),
- source_date_epoch: 1_700_000_000,
- service_revision: "1".repeat(40),
- lib_revision: "2".repeat(40),
- rust_version: "1.97.1",
- host_feature_profile: "service-host",
- contract_versions: ContractVersionsDocument {
- config: 1,
- state: 1,
- admin: 1,
- status: 1,
- provider: 1,
- },
- confidentiality: ConfidentialityDocument {
- state: scan.state,
- derived_from: artifact_record("artifact-scan.v1.json", &scan_evidence),
- protected_material_included: false,
- },
- artifacts: artifacts.clone(),
- };
- validate_confidentiality_binding(&manifest, &scan, &scan_evidence, &artifacts)
- .expect("derived confidentiality");
- manifest.confidentiality.state = "invented_clean_state";
- assert_eq!(
- validate_confidentiality_binding(&manifest, &scan, &scan_evidence, &artifacts),
- Err(ReleaseArtifactError::GenerationFailure)
- );
+ for case in 0..6 {
+ let mut manifest = ArtifactManifestDocument {
+ schema: "radroots.service.release-artifacts.v2",
+ contract_version: 2,
+ candidate_digest: "a".repeat(64),
+ service: "myc".to_owned(),
+ version: "0.1.0-alpha".to_owned(),
+ target: native_fixture_target().to_owned(),
+ source_date_epoch: 1_700_000_000,
+ service_revision: "1".repeat(40),
+ lib_revision: "2".repeat(40),
+ rust_version: "1.97.1",
+ host_feature_profile: "service-host",
+ contract_versions: ContractVersionsDocument {
+ config: 1,
+ state: 1,
+ admin: 1,
+ status: 1,
+ provider: 1,
+ },
+ confidentiality: ConfidentialityDocument {
+ state: scan.state,
+ derived_from: artifact_record("artifact-scan.v1.json", &scan_evidence),
+ protected_material_included: false,
+ },
+ artifacts: artifacts.clone(),
+ };
+ validate_confidentiality_binding(&manifest, &scan, &scan_evidence, &artifacts)
+ .expect("derived confidentiality");
+ match case {
+ 0 => manifest.confidentiality.state = "invented_clean_state",
+ 1 => manifest.candidate_digest = "b".repeat(64),
+ 2 => manifest.confidentiality.protected_material_included = true,
+ 3 => manifest.confidentiality.derived_from.path = "unknown".to_owned(),
+ 4 => manifest.confidentiality.derived_from.sha256 = "b".repeat(64),
+ _ => manifest.artifacts.clear(),
+ }
+ assert_eq!(
+ validate_confidentiality_binding(&manifest, &scan, &scan_evidence, &artifacts),
+ Err(ReleaseArtifactError::GenerationFailure)
+ );
+ }
}
#[test]
@@ -4523,6 +4900,63 @@ version = "0.1.0-alpha"
);
}
+ #[test]
+ fn archive_scanning_enforces_member_types_counts_lengths_and_protected_paths() {
+ let root = TempDir::new().unwrap();
+ let path = root.path().join("input.tar");
+ for kind in [
+ tar::EntryType::Directory,
+ tar::EntryType::Symlink,
+ tar::EntryType::Link,
+ tar::EntryType::Fifo,
+ ] {
+ let mut builder = TarBuilder::new(Vec::new());
+ let mut header = TarHeader::new_gnu();
+ header.set_path("member").unwrap();
+ header.set_entry_type(kind);
+ header.set_size(0);
+ if kind.is_symlink() || kind.is_hard_link() {
+ header.set_link_name("target").unwrap();
+ }
+ header.set_cksum();
+ builder.append(&header, &[][..]).unwrap();
+ let bytes = builder.into_inner().unwrap();
+ fs::write(&path, &bytes).unwrap();
+ assert!(scan_tar_members(&path, false).is_err());
+ assert_eq!(
+ scan_tar_members(&path, true).is_ok(),
+ kind != tar::EntryType::Fifo
+ );
+ let gzip = root.path().join("binary.tar.gz");
+ let mut encoder = GzBuilder::new().write(Vec::new(), Compression::best());
+ encoder.write_all(&bytes).unwrap();
+ fs::write(&gzip, encoder.finish().unwrap()).unwrap();
+ assert_eq!(scan_tar_gzip_members(&gzip).is_ok(), kind.is_dir());
+ }
+ let empty = TarBuilder::new(Vec::new()).into_inner().unwrap();
+ fs::write(&path, &empty).unwrap();
+ assert!(scan_tar_members(&path, true).is_err());
+ let mut encoder = GzBuilder::new().write(Vec::new(), Compression::best());
+ encoder.write_all(&empty).unwrap();
+ fs::write(&path, encoder.finish().unwrap()).unwrap();
+ assert!(scan_tar_gzip_members(&path).is_err());
+ for length in [2, 4] {
+ assert!(scan_reader(&mut &b"abc"[..], length).is_err());
+ }
+ assert_eq!(scan_reader(&mut &b"abc"[..], 3).unwrap(), 3);
+ for path in [
+ "/absolute",
+ "../escape",
+ "src/.ssh/config",
+ &"a".repeat(MAX_ARCHIVE_PATH_BYTES + 1),
+ ] {
+ assert_eq!(
+ validate_scanned_path(path),
+ Err(ReleaseArtifactError::ProtectedMaterialDetected)
+ );
+ }
+ }
+
fn write_tar_fixture(path: &Path, member: &str, bytes: &[u8], gzip: bool) {
let mut header = TarHeader::new_gnu();
header.set_entry_type(tar::EntryType::Regular);
diff --git a/tools/xtask/src/service_repro_install.rs b/tools/xtask/src/service_repro_install.rs
@@ -1344,6 +1344,270 @@ printf '{"after_state_sha256":"%s","artifact_set_sha256":"%s","before_state_sha2
}
#[test]
+ fn plan_and_argument_admission_rejects_each_independent_policy_violation() {
+ for (pointer, replacement) in [
+ ("/schema", json!("unknown")),
+ ("/candidate_digest", json!("A".repeat(64))),
+ ("/target", json!("unknown")),
+ ("/source_revision", json!("invalid")),
+ ("/source_tree", json!("invalid")),
+ ("/root_preimage_revision", json!("invalid")),
+ ("/source_date_epoch", json!(0)),
+ ("/normalization/kind", json!("ignore_timestamps")),
+ ("/normalization/excluded_paths", json!(["secret"])),
+ ("/git_executable_sha256", json!("invalid")),
+ ("/adapter_executable_sha256", json!("invalid")),
+ ("/phase", json!([])),
+ ("/phase/0/id", json!("upgrade_candidate")),
+ ("/phase/0/argv", json!([])),
+ ] {
+ let mut value = sample_plan_value();
+ *value.pointer_mut(pointer).unwrap() = replacement;
+ assert_eq!(
+ validate_plan(&serde_json::from_value(value).unwrap()),
+ Err(ReproInstallError::InvalidPlan),
+ "{pointer}"
+ );
+ }
+ for arguments in [
+ vec![],
+ vec!["x".to_owned(); MAX_ARGV_TOKENS + 1],
+ vec![String::new()],
+ vec!["x".repeat(MAX_ARGV_TOKEN_BYTES + 1)],
+ vec!["a\nb".to_owned()],
+ vec!["a\0b".to_owned()],
+ vec!["{unknown}".to_owned()],
+ vec!["trailing}".to_owned()],
+ vec!["{checkout}".to_owned(), "{checkout}".to_owned()],
+ vec!["literal".to_owned()],
+ ] {
+ assert_eq!(
+ validate_argv_template(&arguments, &["{checkout}"]),
+ Err(ReproInstallError::InvalidPlan)
+ );
+ }
+ let values = BTreeMap::from([("{checkout}", OsString::from("/fixture"))]);
+ assert_eq!(
+ resolve_arguments(&["literal".to_owned(), "{checkout}".to_owned()], &values).unwrap(),
+ [OsString::from("literal"), OsString::from("/fixture")]
+ );
+ assert_eq!(
+ resolve_arguments(&["{unknown}".to_owned()], &values),
+ Err(ReproInstallError::InvalidPlan)
+ );
+ assert_eq!(
+ resolve_arguments(&["trailing}".to_owned()], &values),
+ Err(ReproInstallError::InvalidPlan)
+ );
+ }
+
+ #[test]
+ fn phase_chain_rejects_each_broken_binding_transition_and_health_mutation() {
+ let candidate = "a".repeat(64);
+ let artifacts = "1".repeat(64);
+ let predecessor = "2".repeat(64);
+ let validate = |rows: &[PhaseWitness]| {
+ validate_phase_chain(rows, &candidate, &artifacts, &predecessor)
+ };
+ validate(&valid_witnesses()).unwrap();
+ assert_eq!(validate(&[]), Err(ReproInstallError::InvalidWitness));
+ let mut reordered = valid_witnesses();
+ reordered.swap(0, 1);
+ assert_eq!(validate(&reordered), Err(ReproInstallError::InvalidWitness));
+ for index in 0..PHASE_IDS.len() {
+ for field in [
+ "schema",
+ "candidate_digest",
+ "artifact_set_sha256",
+ "before_state_sha256",
+ "result",
+ ] {
+ let mut value = serde_json::to_value(valid_witnesses()).unwrap();
+ value[index][field] = json!("mismatch");
+ let rows: Vec<PhaseWitness> = serde_json::from_value(value).unwrap();
+ assert_eq!(
+ validate(&rows),
+ Err(ReproInstallError::InvalidWitness),
+ "{index} {field}"
+ );
+ }
+ }
+ for index in [1, 3, 5, 7] {
+ let mut rows = valid_witnesses();
+ rows[index].after_state_sha256 = "7".repeat(64);
+ assert_eq!(validate(&rows), Err(ReproInstallError::InvalidWitness));
+ }
+ }
+
+ #[cfg(unix)]
+ #[test]
+ fn install_receipts_reject_noncanonical_bytes_and_unbound_adapter_claims() {
+ use std::os::unix::fs::PermissionsExt;
+ let temporary = TempDir::new().unwrap();
+ let root = temporary.path().canonicalize().unwrap();
+ let adapter = root.join("adapter");
+ let witness = serde_json::to_value(&valid_witnesses()[0]).unwrap();
+ let mut invalid_outputs = vec![
+ b"not json".to_vec(),
+ serde_json::to_vec_pretty(&witness).unwrap(),
+ b"{}\n".to_vec(),
+ ];
+ for field in [
+ "schema",
+ "phase",
+ "candidate_digest",
+ "artifact_set_sha256",
+ "result",
+ "before_state_sha256",
+ "after_state_sha256",
+ ] {
+ let mut changed = witness.clone();
+ changed[field] = json!("unbound");
+ invalid_outputs.push(canonical_json_line(&changed).unwrap());
+ }
+ for bytes in invalid_outputs {
+ let text = String::from_utf8(bytes).unwrap();
+ assert!(!text.contains('\''));
+ fs::write(&adapter, format!("#!/bin/sh\nprintf '%s' '{text}'\n")).unwrap();
+ fs::set_permissions(&adapter, fs::Permissions::from_mode(0o700)).unwrap();
+ assert!(matches!(
+ run_install_phases(
+ &adapter,
+ &sample_plan(),
+ &root,
+ &"1".repeat(64),
+ &root,
+ &"2".repeat(64),
+ &root,
+ &root,
+ &root
+ ),
+ Err(ReproInstallError::InvalidWitness)
+ ));
+ }
+ fs::write(&adapter, b"#!/bin/sh\nexit 3\n").unwrap();
+ assert!(matches!(
+ run_install_phases(
+ &adapter,
+ &sample_plan(),
+ &root,
+ &"1".repeat(64),
+ &root,
+ &"2".repeat(64),
+ &root,
+ &root,
+ &root
+ ),
+ Err(ReproInstallError::InstallFailure)
+ ));
+ }
+
+ #[cfg(unix)]
+ #[test]
+ fn filesystem_and_source_admission_preserves_existing_outputs() {
+ use std::os::unix::fs::{PermissionsExt, symlink};
+ let temporary = TempDir::new().unwrap();
+ let root = temporary.path().canonicalize().unwrap();
+ let file = root.join("file");
+ fs::write(&file, b"preserve").unwrap();
+ let dangling = root.join("dangling");
+ symlink(root.join("missing"), &dangling).unwrap();
+ let linked = root.join("linked");
+ symlink(&root, &linked).unwrap();
+ for path in [
+ Path::new("relative"),
+ &file,
+ &dangling,
+ &linked.join("result"),
+ &root.join("missing/result"),
+ ] {
+ assert_eq!(
+ validate_output_target(path),
+ Err(ReproInstallError::InvalidOutput)
+ );
+ }
+ assert_eq!(
+ write_result(&dangling, &json!({})),
+ Err(ReproInstallError::InvalidOutput)
+ );
+ assert_eq!(
+ write_result(&linked.join("result"), &json!({})),
+ Err(ReproInstallError::InvalidOutput)
+ );
+ assert_eq!(
+ write_result(
+ &root.join("too-large"),
+ &json!("x".repeat(MAX_RESULT_BYTES))
+ ),
+ Err(ReproInstallError::InvalidOutput)
+ );
+ for path in [Path::new("relative"), &file, &linked] {
+ assert!(canonical_directory(path, ReproInstallError::InvalidArtifacts).is_err());
+ }
+ assert_eq!(
+ validate_executable(Path::new("relative"), &"0".repeat(64)),
+ Err(ReproInstallError::InvalidTool)
+ );
+ assert_eq!(
+ validate_executable(&file, "invalid"),
+ Err(ReproInstallError::InvalidTool)
+ );
+ assert_eq!(
+ validate_executable(&root, &"0".repeat(64)),
+ Err(ReproInstallError::InvalidTool)
+ );
+ fs::set_permissions(&file, fs::Permissions::from_mode(0o600)).unwrap();
+ assert_eq!(
+ validate_executable(&file, &sha256_bytes(b"preserve")),
+ Err(ReproInstallError::InvalidTool)
+ );
+ for path in [&root, &linked, &file] {
+ assert_eq!(
+ read_regular_bounded(path, 2, ReproInstallError::InvalidPlan),
+ Err(ReproInstallError::InvalidPlan)
+ );
+ }
+ assert_eq!(
+ require_empty_directory(&root, ReproInstallError::BuildFailure),
+ Err(ReproInstallError::BuildFailure)
+ );
+ assert_eq!(
+ require_distinct(&root, &linked, ReproInstallError::BuildFailure),
+ Err(ReproInstallError::BuildFailure)
+ );
+ for path in ["", "/absolute", "../escape", ".", "a\\b", "a\nb", "a\0b"] {
+ assert_eq!(
+ portable_relative_path(Path::new(path)),
+ Err(ReproInstallError::InvalidArtifacts)
+ );
+ }
+ let empty = root.join("empty");
+ fs::create_dir(&empty).unwrap();
+ assert!(matches!(
+ artifact_inventory(&empty),
+ Err(ReproInstallError::InvalidArtifacts)
+ ));
+ let source = root.join("source");
+ create_repository(&source, "source.txt");
+ let git = program_path("git");
+ let revision = fixture_git(&source, &["rev-parse", "HEAD"]);
+ let tree = fixture_git(&source, &["rev-parse", "HEAD^{tree}"]);
+ assert_eq!(
+ verify_source(&git, &source, &root, &"0".repeat(40), &tree),
+ Err(ReproInstallError::InvalidSource)
+ );
+ assert_eq!(
+ verify_source(&git, &source, &root, &revision, &"0".repeat(40)),
+ Err(ReproInstallError::InvalidSource)
+ );
+ assert_eq!(
+ verify_root_preimage_epoch(&git, &source, &root, &revision, 1),
+ Err(ReproInstallError::InvalidSource)
+ );
+ assert_eq!(fs::read(&file).unwrap(), b"preserve");
+ }
+
+ #[test]
fn decision_contract_is_exact() {
validate_contract_inner(&Path::new(env!("CARGO_MANIFEST_DIR")).join("../.."))
.expect("decision contract");
diff --git a/tools/xtask/src/service_source_lock.rs b/tools/xtask/src/service_source_lock.rs
@@ -1037,6 +1037,90 @@ mod tests {
}
#[test]
+ fn deferred_material_parser_accepts_both_exact_layouts_and_rejects_ambiguous_selection() {
+ let revision = "1".repeat(40);
+ let mut original = serde_json::json!({"version":7,"root":"root","nodes":{
+ "root":{"inputs":{"lib":"lib"}},
+ "lib":{
+ "locked":{"lastModified":1,"narHash":format!("sha256-{}=", "A".repeat(43)),"owner":"radrootslabs","repo":"lib","rev":revision,"type":"github"},
+ "original":{"owner":"radrootslabs","repo":"lib","rev":revision,"type":"github"}
+ }
+ }});
+ let bytes = serde_json::to_vec(&original).unwrap();
+ assert_eq!(validate_deferred_nix_lock(&bytes).unwrap(), revision);
+ let direct = format!(
+ "inputs.lib = {{\nurl = \"github:radrootslabs/lib/{revision}\";\nflake = false;\n}};\n"
+ );
+ let nested = format!(
+ "inputs = {{\nlib = {{\nurl = \"github:radrootslabs/lib/{revision}\";\nflake = false;\n}};\n}};\n"
+ );
+ for expression in [&direct, &nested] {
+ assert_eq!(
+ validate_deferred_nix_material(expression.as_bytes(), &bytes)
+ .unwrap()
+ .lib_revision(),
+ revision
+ );
+ for changed in [
+ expression.replace("flake = false;", "flake = true;"),
+ expression.replace(&revision, &"2".repeat(40)),
+ format!("{expression}{direct}"),
+ ] {
+ assert_eq!(
+ validate_deferred_nix_material(changed.as_bytes(), &bytes),
+ Err(ServiceSourceLockError::InvalidNixMaterial)
+ );
+ }
+ }
+ assert!(validate_deferred_nix_material(&[255], &bytes).is_err());
+ for (pointer, value) in [
+ ("/root", serde_json::json!("")),
+ ("/root", serde_json::json!(null)),
+ ("/nodes", serde_json::json!([])),
+ ("/nodes/root/inputs/lib", serde_json::json!([])),
+ ("/nodes/lib/locked/owner", serde_json::json!("elsewhere")),
+ ("/nodes/lib/original/owner", serde_json::json!("elsewhere")),
+ ] {
+ let mut changed = original.clone();
+ *changed.pointer_mut(pointer).unwrap() = value;
+ assert!(
+ validate_deferred_nix_lock(&serde_json::to_vec(&changed).unwrap()).is_err(),
+ "{pointer}"
+ );
+ }
+ original["nodes"]["unrelated"] =
+ serde_json::json!({"values":[null,true,1,-1,"text",{},[]]});
+ assert_eq!(
+ validate_deferred_nix_lock(&serde_json::to_vec(&original).unwrap()).unwrap(),
+ revision
+ );
+ original["unknown"] = serde_json::json!(true);
+ assert!(validate_deferred_nix_lock(&serde_json::to_vec(&original).unwrap()).is_err());
+ for digest in [
+ "invalid".to_owned(),
+ format!("sha256-{}x", "A".repeat(43)),
+ format!("sha256-{}?=", "A".repeat(42)),
+ ] {
+ assert!(!valid_nix_sha256(&digest));
+ }
+ for service in ["a_", "a-b", "a.B"] {
+ assert!(!valid_service(service));
+ }
+ }
+
+ #[test]
+ fn source_lock_contract_input_must_be_a_bounded_regular_file() {
+ let root = tempfile::TempDir::new().unwrap();
+ let contract = root.path().join(CONTRACT_RELATIVE);
+ fs::create_dir_all(contract.parent().unwrap()).unwrap();
+ fs::create_dir(&contract).unwrap();
+ assert!(validate_contract_inner(root.path()).is_err());
+ fs::remove_dir(&contract).unwrap();
+ fs::write(&contract, vec![b' '; MAX_CONTRACT_BYTES + 1]).unwrap();
+ assert!(validate_contract_inner(root.path()).is_err());
+ }
+
+ #[test]
fn decision_rejects_every_independent_governed_field_drift() {
let bytes = fs::read(workspace_root().join(CONTRACT_RELATIVE)).expect("decision");
let canonical = serde_json::from_slice::<serde_json::Value>(&bytes).expect("decision json");
diff --git a/tools/xtask/tests/command_self_tests.rs b/tools/xtask/tests/command_self_tests.rs
@@ -0,0 +1,41 @@
+#![forbid(unsafe_code)]
+
+use std::process::{Command, Stdio};
+
+fn run_self_test(command: &str, expected_stdout: &[u8]) {
+ let directory = tempfile::tempdir().expect("isolated command working directory");
+ let output = Command::new(env!("CARGO_BIN_EXE_xtask"))
+ .arg(command)
+ .current_dir(directory.path())
+ .stdin(Stdio::null())
+ .output()
+ .expect("start the actual xtask binary");
+ assert!(
+ output.status.success(),
+ "{command} failed: {}",
+ String::from_utf8_lossy(&output.stderr)
+ );
+ assert_eq!(output.stdout, expected_stdout);
+ assert!(output.stderr.is_empty());
+}
+
+#[test]
+fn advisory_command_executes_the_complete_offline_snapshot_and_process_suite() {
+ run_self_test("advisory-snapshot-self-test", b"");
+}
+
+#[test]
+fn bounded_process_command_executes_deadline_tree_output_and_environment_faults() {
+ run_self_test(
+ "bounded-process-self-test",
+ b"bounded process self-test: ok\n",
+ );
+}
+
+#[test]
+fn artifact_command_executes_filesystem_archive_and_preflight_faults() {
+ run_self_test(
+ "safe-artifact-io-self-test",
+ b"safe artifact I/O self-test: ok\n",
+ );
+}