lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

advisory_snapshot.rs (411222B)


      1 //! Candidate-bound immutable advisory snapshot admission.
      2 
      3 use std::collections::{BTreeMap, BTreeSet};
      4 use std::ffi::OsString;
      5 use std::fmt;
      6 use std::path::{Component, Path, PathBuf};
      7 use std::time::Duration;
      8 
      9 use serde::{Deserialize, Serialize};
     10 use serde_json::Value;
     11 use sha2::{Digest as _, Sha256};
     12 
     13 use crate::safe_artifact_io::{
     14     self, ArchiveEvidence, FileEvidence, TarGzipLimits, TraversalLimits, TraversalSnapshot,
     15     TraversedFile,
     16 };
     17 
     18 const DECISION_PATH: &str =
     19     "contracts/architecture/decisions/services_hardening_advisory_snapshots.v1.json";
     20 const DECISION_BYTES: &[u8] = include_bytes!(concat!(
     21     env!("CARGO_MANIFEST_DIR"),
     22     "/../../contracts/architecture/decisions/services_hardening_advisory_snapshots.v1.json"
     23 ));
     24 const GRADLE_INIT_SCRIPT_BYTES: &[u8] = include_bytes!(concat!(
     25     env!("CARGO_MANIFEST_DIR"),
     26     "/assets/advisory_inventory.init.gradle"
     27 ));
     28 const GRADLE_INIT_SCRIPT_SHA256: &str =
     29     "4c51de29877813dd679c970507602940fdf8c38a586f8d2f6d101b6a93a8b598";
     30 const MANIFEST_NAME: &str = "manifest.json";
     31 const RUSTSEC_ARCHIVE_NAME: &str = "rustsec-db.tar.gz";
     32 const RUSTSEC_REPORT_NAME: &str = "rustsec-report.json";
     33 const NVD_ARCHIVE_NAME: &str = "nvd-data.tar.gz";
     34 const OWASP_REPORT_NAME: &str = "owasp-report.json";
     35 const GRADLE_RAW_GRAPH_NAME: &str = "inventory.json";
     36 const RAW_SCANNER_OUTPUT_NAME: &str = "scanner-output.json";
     37 const SNAPSHOT_FILE_NAMES: [&str; 5] = [
     38     MANIFEST_NAME,
     39     NVD_ARCHIVE_NAME,
     40     OWASP_REPORT_NAME,
     41     RUSTSEC_ARCHIVE_NAME,
     42     RUSTSEC_REPORT_NAME,
     43 ];
     44 const MAX_SNAPSHOT_MANIFEST_BYTES: u64 = 67_108_864;
     45 const MAX_PRODUCER_REQUEST_BYTES: usize = 1_048_576;
     46 const MAX_TOOL_MANIFEST_BYTES: usize = 16_777_216;
     47 const MAX_TOOL_OBSERVATION_BYTES: usize = 4_194_304;
     48 const MAX_NVD_TRACE_BYTES: usize = 16_777_216;
     49 const MAX_PROVIDER_EVIDENCE_BYTES: usize = 67_108_864;
     50 const MAX_REPORT_BYTES: u64 = 67_108_864;
     51 const MAX_RAW_WORKLOAD_REPORT_BYTES: u64 = 33_554_432;
     52 const MAX_GRADLE_GRAPH_BYTES: u64 = 8_388_608;
     53 const MAX_GRADLE_ARTIFACT_BYTES: u64 = 17_179_869_184;
     54 const MAX_GRADLE_COMPONENTS: usize = 65_536;
     55 const MAX_GRADLE_EDGES: usize = 262_144;
     56 const MAX_GRADLE_ARTIFACTS: usize = 65_536;
     57 const MAX_GRADLE_VARIANTS: usize = 4_096;
     58 const MAX_GRADLE_ATTRIBUTES: usize = 1_024;
     59 const MAX_GRADLE_CAPABILITIES: usize = 1_024;
     60 const MAX_GRADLE_REJECTED_VERSIONS: usize = 1_024;
     61 const MAX_GRADLE_EXTERNAL_VARIANT_DEPTH: usize = 4;
     62 const MAX_NVD_REQUESTS: usize = 1_024;
     63 const MAX_NVD_RESPONSE_BYTES: u64 = 67_108_864;
     64 const MAX_NVD_AGGREGATE_BYTES: u64 = 536_870_912;
     65 const QUALIFICATION_FRESHNESS_SECONDS: u64 = 86_400;
     66 const DEVELOPMENT_REUSE_SECONDS: u64 = 604_800;
     67 const TOOL_REVIEW_SECONDS: u64 = 86_400;
     68 const ANALYSIS_DEADLINE_SECONDS: u64 = 3_600;
     69 const NON_WAIVABLE_RUSTSEC: &str = "RUSTSEC-2026-0253";
     70 const BOUNDED_PROCESS_DECISION_SHA256: &str =
     71     "41e11fe9bfecfb946063bafcb4df7eb811abdf03b0eaf8df4bff0ccb382d9784";
     72 const BOUNDED_PROCESS_SOURCE_SHA256: &str =
     73     "56b8de0c3c34b7481f0f8c792e37dfb0d9fc12a5b990287c979e89fd6989202e";
     74 
     75 const TOOL_PINS: [ToolPin; 4] = [
     76     ToolPin {
     77         id: "cargo_audit",
     78         version: "0.22.1",
     79         executable_sha256: "c5cd7c0da8a9d0dff338aa1a2a30b0c723fde8201c23481f49e75be0bb77fe74",
     80         source_sha256: "2f4e27b0ab2d116c87c29db159ad42565cdcdccf77eb62ef0486ddd017a02da6",
     81         receipt_sha256: "74196dcade966311e535813d2e3bda2f9515186c700f3074acdc2849abce1303",
     82         projection_sha256: "c945e827744d62b46e46826c8c066e3f8f354d972fa7f069d963e287b8e7b595",
     83     },
     84     ToolPin {
     85         id: "owasp_dependency_check",
     86         version: "12.2.2",
     87         executable_sha256: "d683a49ec335eeca93d8707f3e8ce21d7ba63a1e619a325c6518f89c25efcdc4",
     88         source_sha256: "bf07fefd81af3094c5f6850423b014df44db62ce2dbad0f79079a90df675e44a",
     89         receipt_sha256: "83d9a668179fb5e9ad6fd5c625a9ab58b8371a6d32b59808fc8d2973957f54a2",
     90         projection_sha256: "1011327eeed9ed5c4835bdf8e08f5b1852cf118506563daa5ddb0a251e65fb9a",
     91     },
     92     ToolPin {
     93         id: "java",
     94         version: "21.0.12.1",
     95         executable_sha256: "9be1d0a740ff6502df1a762145e62860f5de4b7e17658d9cb9498da3acf9d16c",
     96         source_sha256: "575bb8d9d604821d8f350325b28a35e49bcffd7ec33727b41edc8d709537dada",
     97         receipt_sha256: "e7018157655e26443392ace45967e427aafa1dd98e2e67f94bdd04267d1d9296",
     98         projection_sha256: "2e45aabf8388778d521d51e1ade0d4403fae4d656773bf547e2105dbd79ae655",
     99     },
    100     ToolPin {
    101         id: "gradle_wrapper",
    102         version: "9.5.0",
    103         executable_sha256: "ab5c0cad16305af2e619c159c1f58dd68d07fab9c11e36701e109c0277407f7a",
    104         source_sha256: "553c78f50dafcd54d65b9a444649057857469edf836431389695608536d6b746",
    105         receipt_sha256: "a6538aaef9f5e2510cf4bbfe6d560e64ae4065e64a5c8ba0807d7448f6a89610",
    106         projection_sha256: "f84dc2cb5139d080b28eaee9bc67fcce9bb9acc5e6f609fc00f247b1dd282ef8",
    107     },
    108 ];
    109 
    110 const CANDIDATE_REPOSITORIES: [&str; 10] = [
    111     "_radroots",
    112     "oss/lib",
    113     "oss/sdk",
    114     "oss/myc",
    115     "oss/rhi",
    116     "oss/apple_kit",
    117     "oss/cli",
    118     "oss/radrootsd",
    119     "oss/ios_app",
    120     "oss/harvestcircle",
    121 ];
    122 
    123 const CARGO_AUDIT_ARGUMENTS: [&str; 7] = [
    124     "audit",
    125     "--db",
    126     "{absolute_rustsec_snapshot}",
    127     "--no-fetch",
    128     "--json",
    129     "--file",
    130     "{absolute_cargo_lock}",
    131 ];
    132 const OWASP_UPDATE_ARGUMENTS: [&str; 12] = [
    133     "--updateonly",
    134     "--data",
    135     "{nvd_staging}",
    136     "--disableKnownExploited",
    137     "--disableRetireJs",
    138     "--disableHostedSuppressions",
    139     "--disableVersionCheck",
    140     "--disableOssIndex",
    141     "--disableCentral",
    142     "--disableNodeAudit",
    143     "--disableYarnAudit",
    144     "--disablePnpmAudit",
    145 ];
    146 const OWASP_OFFLINE_ARGUMENTS: [&str; 20] = [
    147     "--noupdate",
    148     "--data",
    149     "{nvd_snapshot}",
    150     "--scan",
    151     "{gradle_inventory}",
    152     "--project",
    153     "{exact_workload_id}",
    154     "--format",
    155     "JSON",
    156     "--out",
    157     "{report}",
    158     "--disableKnownExploited",
    159     "--disableRetireJs",
    160     "--disableHostedSuppressions",
    161     "--disableVersionCheck",
    162     "--disableOssIndex",
    163     "--disableCentral",
    164     "--disableNodeAudit",
    165     "--disableYarnAudit",
    166     "--disablePnpmAudit",
    167 ];
    168 
    169 #[derive(Clone, Copy)]
    170 struct ToolPin {
    171     id: &'static str,
    172     version: &'static str,
    173     executable_sha256: &'static str,
    174     source_sha256: &'static str,
    175     receipt_sha256: &'static str,
    176     projection_sha256: &'static str,
    177 }
    178 
    179 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
    180 pub(crate) enum AdvisoryFailureKind {
    181     ArchiveRejected,
    182     BindingChanged,
    183     ExpiredSuppression,
    184     InventoryMismatch,
    185     InvalidContract,
    186     InvalidReport,
    187     InvalidSnapshot,
    188     KnownVulnerability,
    189     StaleSnapshot,
    190     TimedOut,
    191     Unavailable,
    192 }
    193 
    194 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
    195 pub(crate) struct AdvisoryError {
    196     kind: AdvisoryFailureKind,
    197 }
    198 
    199 impl AdvisoryError {
    200     const fn new(kind: AdvisoryFailureKind) -> Self {
    201         Self { kind }
    202     }
    203 
    204     pub(crate) const fn kind(self) -> AdvisoryFailureKind {
    205         self.kind
    206     }
    207 }
    208 
    209 impl fmt::Display for AdvisoryError {
    210     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    211         formatter.write_str(match self.kind {
    212             AdvisoryFailureKind::ArchiveRejected => "advisory archive admission failed",
    213             AdvisoryFailureKind::BindingChanged => "advisory snapshot binding changed",
    214             AdvisoryFailureKind::ExpiredSuppression => "advisory suppression is expired",
    215             AdvisoryFailureKind::InventoryMismatch => "advisory inventory differs",
    216             AdvisoryFailureKind::InvalidContract => "advisory contract differs",
    217             AdvisoryFailureKind::InvalidReport => "advisory scanner report is invalid",
    218             AdvisoryFailureKind::InvalidSnapshot => "advisory snapshot is invalid",
    219             AdvisoryFailureKind::KnownVulnerability => "advisory findings remain unsuppressed",
    220             AdvisoryFailureKind::StaleSnapshot => "advisory snapshot is stale",
    221             AdvisoryFailureKind::TimedOut => "advisory operation timed out",
    222             AdvisoryFailureKind::Unavailable => "mandatory advisory input is unavailable",
    223         })
    224     }
    225 }
    226 
    227 impl std::error::Error for AdvisoryError {}
    228 
    229 #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
    230 #[serde(deny_unknown_fields)]
    231 pub(crate) struct CandidateIdentity {
    232     pub(crate) generation: u64,
    233     pub(crate) digest: String,
    234 }
    235 
    236 #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Ord, PartialOrd, Serialize)]
    237 #[serde(deny_unknown_fields)]
    238 pub(crate) struct SourceIdentity {
    239     pub(crate) repository: String,
    240     pub(crate) revision: String,
    241     pub(crate) tree: String,
    242 }
    243 
    244 #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
    245 #[serde(deny_unknown_fields)]
    246 pub(crate) struct WorkloadInventory {
    247     pub(crate) id: String,
    248     pub(crate) repository: String,
    249     pub(crate) build_root: String,
    250     pub(crate) package_manager: String,
    251     pub(crate) language: String,
    252     pub(crate) manifest_path: Option<String>,
    253     pub(crate) lockfile_path: Option<String>,
    254     pub(crate) project_path: Option<String>,
    255     pub(crate) configuration: Option<String>,
    256     pub(crate) dependency_count: u64,
    257     pub(crate) input_sha256: String,
    258 }
    259 
    260 #[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
    261 #[serde(rename_all = "snake_case")]
    262 pub(crate) enum FreshnessMode {
    263     Development,
    264     Qualification,
    265 }
    266 
    267 pub(crate) struct AdmissionRequest {
    268     pub(crate) candidate: CandidateIdentity,
    269     pub(crate) sources: Vec<SourceIdentity>,
    270     pub(crate) inventory: Vec<WorkloadInventory>,
    271     pub(crate) evaluation_epoch: u64,
    272     pub(crate) freshness: FreshnessMode,
    273     pub(crate) producer_request: Vec<u8>,
    274     pub(crate) step_297_tool_manifest: Vec<u8>,
    275     pub(crate) fresh_tool_observation: Vec<u8>,
    276     pub(crate) nvd_network_trace: Vec<u8>,
    277     pub(crate) provider_execution_evidence: Vec<u8>,
    278     pub(crate) admitted_gradle_graphs: Vec<AdmittedGradleGraph>,
    279     pub(crate) admitted_scanner_outputs: Vec<AdmittedScannerOutput>,
    280 }
    281 
    282 #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
    283 #[serde(deny_unknown_fields)]
    284 struct TrustedProducerRequest {
    285     schema: String,
    286     candidate_generation: u64,
    287     candidate_digest: String,
    288     platform: String,
    289     process_contract_sha256: String,
    290     process_runner_source_sha256: String,
    291     nvd_endpoint: String,
    292     nvd_enforcement_program_sha256: String,
    293     nvd_enforcement_configuration_sha256: String,
    294     nvd_query_keys: Vec<String>,
    295     workload_ids: Vec<String>,
    296 }
    297 
    298 #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
    299 #[serde(deny_unknown_fields)]
    300 struct TrustedToolManifest {
    301     schema: String,
    302     candidate_generation: u64,
    303     candidate_digest: String,
    304     platform: String,
    305     producer_request_sha256: String,
    306     tool_acquisition: Vec<ToolAcquisition>,
    307 }
    308 
    309 #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
    310 #[serde(deny_unknown_fields)]
    311 struct TrustedToolObservation {
    312     schema: String,
    313     candidate_generation: u64,
    314     candidate_digest: String,
    315     platform: String,
    316     producer_request_sha256: String,
    317     tool_manifest_sha256: String,
    318     row_projection_sha256: String,
    319     observed_at_epoch: u64,
    320     tool_state: Vec<ToolState>,
    321 }
    322 
    323 #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
    324 #[serde(deny_unknown_fields)]
    325 struct NvdNetworkTrace {
    326     schema: String,
    327     candidate_generation: u64,
    328     candidate_digest: String,
    329     producer_request_sha256: String,
    330     enforcement: String,
    331     enforcement_program_sha256: String,
    332     enforcement_configuration_sha256: String,
    333     request: Vec<NvdRequestTrace>,
    334 }
    335 
    336 #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
    337 #[serde(deny_unknown_fields)]
    338 struct NvdRequestTrace {
    339     sequence: u64,
    340     method: String,
    341     scheme: String,
    342     authority: String,
    343     path: String,
    344     started_at_epoch: u64,
    345     completed_at_epoch: u64,
    346     query: Vec<NvdQueryTrace>,
    347     response_status: u16,
    348     response_byte_length: u64,
    349     response_sha256: String,
    350     response_start_index: u64,
    351     response_results_per_page: u64,
    352     response_total_results: u64,
    353 }
    354 
    355 #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
    356 #[serde(deny_unknown_fields)]
    357 struct NvdQueryTrace {
    358     name: String,
    359     value: String,
    360 }
    361 
    362 #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
    363 #[serde(deny_unknown_fields)]
    364 struct TrustedProviderEvidence {
    365     schema: String,
    366     candidate_generation: u64,
    367     candidate_digest: String,
    368     platform: String,
    369     producer_request_sha256: String,
    370     candidate_advisory_input_sha256: String,
    371     gradle_projection: Vec<GradleProjection>,
    372     provider_snapshot: Vec<ProviderSnapshot>,
    373     process_receipt: Vec<TrustedProcessReceipt>,
    374     suppressions: Vec<Suppression>,
    375 }
    376 
    377 #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
    378 #[serde(deny_unknown_fields)]
    379 struct TrustedProcessReceipt {
    380     id: String,
    381     state: OperationState,
    382     program_sha256: String,
    383     runtime_sha256: Vec<String>,
    384     arguments_sha256: String,
    385     environment_sha256: String,
    386     environment: Vec<ProcessEnvironmentRow>,
    387     working_directory_sha256: String,
    388     working_directory: ProcessWorkingDirectory,
    389     path_binding: Vec<ProcessPathBinding>,
    390     stdin_closed: bool,
    391     deadline_seconds: u64,
    392     started_at_epoch: u64,
    393     completed_at_epoch: u64,
    394     exit_code: i32,
    395     stdout_byte_length: u64,
    396     stdout_sha256: String,
    397     stderr_byte_length: u64,
    398     stderr_sha256: String,
    399     input_sha256: String,
    400     output_sha256: String,
    401 }
    402 
    403 #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
    404 #[serde(deny_unknown_fields)]
    405 struct ProcessEnvironmentRow {
    406     name: String,
    407     logical_value: String,
    408     value_sha256: String,
    409 }
    410 
    411 #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
    412 #[serde(deny_unknown_fields)]
    413 struct ProcessWorkingDirectory {
    414     logical_uri: String,
    415     kind: String,
    416     identity_sha256: String,
    417     pre_execution_entry_count: u64,
    418     pre_execution_tree_sha256: String,
    419     post_execution_entry_count: u64,
    420     post_execution_tree_sha256: String,
    421 }
    422 
    423 #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
    424 #[serde(deny_unknown_fields)]
    425 struct ProcessPathBinding {
    426     argument_index: u16,
    427     logical_role: String,
    428     identity_sha256: String,
    429 }
    430 
    431 #[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
    432 #[serde(rename_all = "snake_case")]
    433 enum ProviderId {
    434     OwaspNvd,
    435     Rustsec,
    436 }
    437 
    438 impl ProviderId {
    439     const fn as_str(self) -> &'static str {
    440         match self {
    441             Self::OwaspNvd => "owasp_nvd",
    442             Self::Rustsec => "rustsec",
    443         }
    444     }
    445 }
    446 
    447 #[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
    448 #[serde(rename_all = "snake_case")]
    449 enum OperationState {
    450     Complete,
    451     Failed,
    452     TimedOut,
    453     Unavailable,
    454 }
    455 
    456 #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
    457 #[serde(deny_unknown_fields)]
    458 struct ToolAcquisition {
    459     id: String,
    460     projection: Value,
    461 }
    462 
    463 #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
    464 #[serde(deny_unknown_fields)]
    465 struct ToolState {
    466     id: String,
    467     state: OperationState,
    468     reviewed_at_epoch: u64,
    469     normalized_version: String,
    470     executable_sha256: String,
    471     source_sha256: String,
    472     package_receipt_sha256: String,
    473 }
    474 
    475 #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
    476 #[serde(deny_unknown_fields)]
    477 struct BlobBinding {
    478     path: String,
    479     byte_length: u64,
    480     sha256: String,
    481     logical_uri: String,
    482     media_type: String,
    483     logical_role: String,
    484 }
    485 
    486 #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
    487 #[serde(deny_unknown_fields)]
    488 struct ProviderSnapshot {
    489     provider: ProviderId,
    490     acquisition_kind: String,
    491     acquisition_count: u8,
    492     acquisition_state: OperationState,
    493     acquired_at_epoch: u64,
    494     digest_time_epoch: u64,
    495     database_identity_sha256: String,
    496     materialized_tree_sha256: String,
    497     bounded_deadline_seconds: u64,
    498     network_mode: String,
    499     producer_request_sha256: String,
    500     network_trace_sha256: String,
    501     acquisition_arguments: Vec<String>,
    502     archive_format: String,
    503     archive: BlobBinding,
    504     archive_expanded_bytes: u64,
    505     archive_member_count: u64,
    506     archive_payload_bytes: u64,
    507     analysis_state: OperationState,
    508     analyzed_at_epoch: u64,
    509     analysis_environment: String,
    510     analysis_arguments: Vec<String>,
    511     report: BlobBinding,
    512 }
    513 
    514 #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
    515 #[serde(deny_unknown_fields)]
    516 struct GradleProjection {
    517     workload_id: String,
    518     state: OperationState,
    519     raw_graph_byte_length: u64,
    520     raw_graph_sha256: String,
    521     init_script_sha256: String,
    522     wrapper_arguments: Vec<String>,
    523     environment_keys: Vec<String>,
    524     environment_sha256: String,
    525     exit_code: i32,
    526     source_revision: String,
    527     source_tree: String,
    528     input_sha256: String,
    529     dependency_count: u64,
    530     component_count: u64,
    531     edge_count: u64,
    532     artifact_count: u64,
    533     components: Vec<GradleComponent>,
    534     edges: Vec<GradleEdge>,
    535     artifacts: Vec<GradleArtifact>,
    536     canonical_graph_sha256: String,
    537     materialized_tree_sha256: String,
    538     normalization_receipt_sha256: String,
    539     artifact_source_roots_sha256: String,
    540     seed_cache_inventory_sha256: String,
    541     wrapper_distribution_sha256: String,
    542     process_receipt_sha256: String,
    543 }
    544 
    545 #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
    546 #[serde(deny_unknown_fields)]
    547 struct GradleComponent {
    548     root: bool,
    549     kind: String,
    550     group: Option<String>,
    551     name: Option<String>,
    552     version: Option<String>,
    553     build_root: Option<String>,
    554     project_path: Option<String>,
    555     variant: Value,
    556     variant_sha256: String,
    557     identity_sha256: String,
    558 }
    559 
    560 #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
    561 #[serde(deny_unknown_fields)]
    562 struct GradleEdge {
    563     from_identity_sha256: String,
    564     to_identity_sha256: String,
    565     requested: Value,
    566     requested_sha256: String,
    567     constraint: bool,
    568     selected_variant: Value,
    569     selected_variant_sha256: String,
    570 }
    571 
    572 #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
    573 #[serde(deny_unknown_fields)]
    574 struct GradleArtifact {
    575     component_identity_sha256: String,
    576     component: String,
    577     package_ecosystem: String,
    578     package_namespace: String,
    579     package_name: String,
    580     package_version: String,
    581     artifact_sha256: String,
    582     artifact_name: String,
    583     logical_name: String,
    584     artifact_type: String,
    585     classifier: Option<String>,
    586     byte_length: u64,
    587     extension: String,
    588     materialized_name: String,
    589     variant: Value,
    590     variant_sha256: String,
    591 }
    592 
    593 #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
    594 #[serde(deny_unknown_fields)]
    595 struct Suppression {
    596     id: String,
    597     provider: ProviderId,
    598     advisory_id: String,
    599     workload_id: String,
    600     package_ecosystem: String,
    601     package_namespace: String,
    602     package_name: String,
    603     package_version: String,
    604     owner: String,
    605     rationale: String,
    606     created_at_epoch: u64,
    607     expires_at_epoch: u64,
    608 }
    609 
    610 #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
    611 #[serde(deny_unknown_fields)]
    612 struct SnapshotManifest {
    613     schema: String,
    614     candidate: CandidateIdentity,
    615     sources: Vec<SourceIdentity>,
    616     inventory_sha256: String,
    617     inventory: Vec<WorkloadInventory>,
    618     candidate_advisory_input_sha256: String,
    619     step_297_tool_manifest_sha256: String,
    620     fresh_tool_observation_sha256: String,
    621     tool_acquisition: Vec<ToolAcquisition>,
    622     tool_state: Vec<ToolState>,
    623     gradle_projection: Vec<GradleProjection>,
    624     provider_snapshot: Vec<ProviderSnapshot>,
    625     suppressions: Vec<Suppression>,
    626 }
    627 
    628 #[derive(Clone, Debug, Eq, PartialEq)]
    629 struct Finding {
    630     provider: ProviderId,
    631     advisory_id: String,
    632     package_ecosystem: String,
    633     package_namespace: String,
    634     package_name: String,
    635     package_version: String,
    636     workload_id: String,
    637 }
    638 
    639 #[derive(Clone, Debug, Eq, PartialEq)]
    640 pub(crate) struct AdvisorySnapshotEvidence {
    641     pub(crate) candidate: CandidateIdentity,
    642     pub(crate) manifest: FileEvidence,
    643     pub(crate) source_inventory_sha256: String,
    644     pub(crate) inventory_sha256: String,
    645     pub(crate) fresh_tool_observation_sha256: String,
    646     pub(crate) tool_row_projection_sha256: String,
    647     pub(crate) rustsec_archive: ArchiveEvidence,
    648     pub(crate) nvd_archive: ArchiveEvidence,
    649     pub(crate) rustsec_report: FileEvidence,
    650     pub(crate) owasp_report: FileEvidence,
    651     pub(crate) provider_time_trace_sha256: String,
    652     pub(crate) finding_inventory_sha256: String,
    653     pub(crate) suppression_inventory_sha256: String,
    654     pub(crate) candidate_advisory_input_sha256: String,
    655     pub(crate) gradle_projection_sha256: String,
    656     pub(crate) provider_freshness: Vec<ProviderFreshnessEvidence>,
    657     pub(crate) finding_count: usize,
    658     pub(crate) suppression_count: usize,
    659 }
    660 
    661 #[derive(Clone, Debug, Eq, PartialEq)]
    662 pub(crate) struct ProviderFreshnessEvidence {
    663     pub(crate) provider: String,
    664     pub(crate) acquired_at_epoch: u64,
    665     pub(crate) digest_time_epoch: u64,
    666     pub(crate) analyzed_at_epoch: u64,
    667     pub(crate) database_identity_sha256: String,
    668     pub(crate) archive_sha256: String,
    669     pub(crate) report_sha256: String,
    670     pub(crate) network_trace_sha256: String,
    671 }
    672 
    673 #[derive(Debug, Deserialize)]
    674 #[serde(deny_unknown_fields)]
    675 struct ReportEnvelope {
    676     schema: String,
    677     provider: ProviderId,
    678     workload_result: Vec<WorkloadResult>,
    679 }
    680 
    681 #[derive(Debug, Deserialize)]
    682 #[serde(deny_unknown_fields)]
    683 struct WorkloadResult {
    684     workload_id: String,
    685     input_sha256: String,
    686     dependency_count: u64,
    687     provider_archive_sha256: String,
    688     materialized_tree_sha256: String,
    689     tool_observation_sha256: String,
    690     arguments: Vec<String>,
    691     environment_sha256: String,
    692     process_receipt_sha256: String,
    693     database_copy: Option<ScannerDatabaseCopy>,
    694     exit_code: i32,
    695     raw_output_byte_length: u64,
    696     raw_output_sha256: String,
    697     raw_scanner_output: String,
    698 }
    699 
    700 #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
    701 #[serde(deny_unknown_fields)]
    702 struct ScannerDatabaseCopy {
    703     root_identity_sha256: String,
    704     source_tree_sha256: String,
    705     pre_scan_tree_sha256: String,
    706     post_scan_tree_sha256: String,
    707 }
    708 
    709 #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
    710 #[serde(deny_unknown_fields)]
    711 struct RawGradleGraph {
    712     schema: String,
    713     workload_id: String,
    714     build_root: String,
    715     project_path: String,
    716     configuration: String,
    717     components: Vec<RawGradleComponent>,
    718     edges: Vec<RawGradleEdge>,
    719     artifacts: Vec<RawGradleArtifact>,
    720 }
    721 
    722 #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
    723 #[serde(deny_unknown_fields)]
    724 struct RawGradleComponentCore {
    725     build_root: Option<String>,
    726     group: Option<String>,
    727     kind: String,
    728     name: Option<String>,
    729     project_path: Option<String>,
    730     version: Option<String>,
    731 }
    732 
    733 #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
    734 #[serde(deny_unknown_fields)]
    735 struct RawGradleComponent {
    736     build_root: Option<String>,
    737     group: Option<String>,
    738     kind: String,
    739     name: Option<String>,
    740     project_path: Option<String>,
    741     root: bool,
    742     variant: RawGradleVariantEnvelope,
    743     version: Option<String>,
    744 }
    745 
    746 #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
    747 #[serde(deny_unknown_fields)]
    748 struct RawGradleVariantEnvelope {
    749     selected: Vec<RawGradleVariant>,
    750 }
    751 
    752 #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
    753 #[serde(deny_unknown_fields)]
    754 struct RawGradleVariant {
    755     attributes: Vec<RawGradleAttribute>,
    756     capabilities: Vec<RawGradleCapability>,
    757     external_variant: Option<Box<RawGradleVariant>>,
    758 }
    759 
    760 #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
    761 #[serde(deny_unknown_fields)]
    762 struct RawGradleAttribute {
    763     name: String,
    764     value: String,
    765 }
    766 
    767 #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
    768 #[serde(deny_unknown_fields)]
    769 struct RawGradleCapability {
    770     group: String,
    771     name: String,
    772     version: Option<String>,
    773 }
    774 
    775 #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
    776 #[serde(deny_unknown_fields)]
    777 struct RawGradleEdge {
    778     constraint: bool,
    779     from: RawGradleComponentCore,
    780     requested: RawGradleSelector,
    781     selected_variant: RawGradleVariant,
    782     to: RawGradleComponentCore,
    783 }
    784 
    785 #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
    786 #[serde(deny_unknown_fields)]
    787 struct RawGradleSelector {
    788     attributes: Vec<RawGradleAttribute>,
    789     build_root: Option<String>,
    790     capabilities: Vec<RawGradleCapability>,
    791     group: Option<String>,
    792     kind: String,
    793     name: Option<String>,
    794     project_path: Option<String>,
    795     version: Option<String>,
    796     version_constraint: Option<RawGradleVersionConstraint>,
    797 }
    798 
    799 #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
    800 #[serde(deny_unknown_fields)]
    801 struct RawGradleVersionConstraint {
    802     branch: Option<String>,
    803     preferred: String,
    804     rejected: Vec<String>,
    805     required: String,
    806     strict: String,
    807 }
    808 
    809 #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
    810 #[serde(deny_unknown_fields)]
    811 struct RawGradleArtifact {
    812     artifact_name: String,
    813     artifact_type: String,
    814     classifier: Option<String>,
    815     component: RawGradleComponentCore,
    816     extension: String,
    817     group: Option<String>,
    818     logical_name: String,
    819     module_version: RawGradleModuleVersion,
    820     name: Option<String>,
    821     observed_byte_length: u64,
    822     observed_sha256: String,
    823     source_path: String,
    824     variant: RawGradleVariant,
    825     version: Option<String>,
    826 }
    827 
    828 #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
    829 #[serde(deny_unknown_fields)]
    830 struct RawGradleModuleVersion {
    831     group: String,
    832     name: String,
    833     version: String,
    834 }
    835 
    836 pub(crate) struct AdmittedGradleGraph {
    837     raw: FileEvidence,
    838     workload_id: String,
    839     components: Vec<GradleComponent>,
    840     edges: Vec<GradleEdge>,
    841     artifacts: Vec<GradleArtifact>,
    842     canonical_graph_sha256: String,
    843     materialized_tree_sha256: String,
    844     normalization_receipt_sha256: String,
    845     artifact_source_roots_sha256: String,
    846     source_snapshot: TraversalSnapshot,
    847     materialized_snapshot: TraversalSnapshot,
    848     _materialization: tempfile::TempDir,
    849 }
    850 
    851 impl AdmittedGradleGraph {
    852     pub(crate) fn revalidate(&self) -> Result<(), AdvisoryError> {
    853         self.source_snapshot
    854             .revalidate()
    855             .map_err(map_binding_error)?;
    856         self.materialized_snapshot
    857             .revalidate()
    858             .map_err(map_binding_error)
    859     }
    860 
    861     fn raw_evidence(&self) -> &FileEvidence {
    862         &self.raw
    863     }
    864 
    865     fn matches_projection(&self, projection: &GradleProjection) -> bool {
    866         projection.workload_id == self.workload_id
    867             && projection.raw_graph_byte_length == self.raw.byte_length
    868             && projection.raw_graph_sha256 == self.raw.sha256
    869             && projection.components == self.components
    870             && projection.edges == self.edges
    871             && projection.artifacts == self.artifacts
    872             && projection.canonical_graph_sha256 == self.canonical_graph_sha256
    873             && projection.materialized_tree_sha256 == self.materialized_tree_sha256
    874             && projection.normalization_receipt_sha256 == self.normalization_receipt_sha256
    875             && projection.artifact_source_roots_sha256 == self.artifact_source_roots_sha256
    876     }
    877 }
    878 
    879 pub(crate) struct AdmittedScannerOutput {
    880     provider: ProviderId,
    881     workload_id: String,
    882     bytes: Vec<u8>,
    883     evidence: FileEvidence,
    884     snapshot: TraversalSnapshot,
    885 }
    886 
    887 impl AdmittedScannerOutput {
    888     fn revalidate(&self) -> Result<(), AdvisoryError> {
    889         self.snapshot.revalidate().map_err(map_binding_error)
    890     }
    891 }
    892 
    893 fn admit_raw_scanner_output(
    894     root: &Path,
    895     provider: ProviderId,
    896     workload_id: &str,
    897 ) -> Result<AdmittedScannerOutput, AdvisoryError> {
    898     let snapshot = safe_artifact_io::traverse_regular_files(
    899         root,
    900         TraversalLimits {
    901             max_entries: 1,
    902             max_files: 1,
    903             max_total_bytes: MAX_RAW_WORKLOAD_REPORT_BYTES,
    904             max_file_bytes: MAX_RAW_WORKLOAD_REPORT_BYTES,
    905             max_depth: 1,
    906             max_path_bytes: 64,
    907         },
    908         &[],
    909     )
    910     .map_err(map_binding_error)?;
    911     let file = snapshot
    912         .files()
    913         .first()
    914         .filter(|file| {
    915             snapshot.entry_count() == 1
    916                 && snapshot.files().len() == 1
    917                 && file.relative_path() == Path::new(RAW_SCANNER_OUTPUT_NAME)
    918         })
    919         .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InvalidReport))?;
    920     let (bytes, evidence) = snapshot
    921         .read_evidenced(file, MAX_RAW_WORKLOAD_REPORT_BYTES)
    922         .map_err(map_binding_error)?;
    923     if bytes.is_empty() || serde_json::from_slice::<Value>(&bytes).is_err() {
    924         return Err(AdvisoryError::new(AdvisoryFailureKind::InvalidReport));
    925     }
    926     snapshot.revalidate().map_err(map_binding_error)?;
    927     Ok(AdmittedScannerOutput {
    928         provider,
    929         workload_id: workload_id.to_owned(),
    930         bytes,
    931         evidence,
    932         snapshot,
    933     })
    934 }
    935 
    936 struct GradleArtifactSourceRoot<'a> {
    937     path: &'a Path,
    938     logical_role: &'static str,
    939     identity_sha256: &'a str,
    940 }
    941 
    942 struct NormalizedRawGradleGraph {
    943     components: Vec<GradleComponent>,
    944     edges: Vec<GradleEdge>,
    945     artifacts: Vec<GradleArtifact>,
    946     source_paths: Vec<PathBuf>,
    947 }
    948 
    949 fn admit_raw_gradle_graph(
    950     raw_root: &Path,
    951     materialization_parent: &Path,
    952     authority: &WorkloadAuthority,
    953     source_revision: &str,
    954     expected_dependency_count: u64,
    955     artifact_source_roots: &[GradleArtifactSourceRoot<'_>],
    956 ) -> Result<AdmittedGradleGraph, AdvisoryError> {
    957     let source_snapshot = safe_artifact_io::traverse_regular_files(
    958         raw_root,
    959         TraversalLimits {
    960             max_entries: 1,
    961             max_files: 1,
    962             max_total_bytes: MAX_GRADLE_GRAPH_BYTES,
    963             max_file_bytes: MAX_GRADLE_GRAPH_BYTES,
    964             max_depth: 1,
    965             max_path_bytes: 64,
    966         },
    967         &[],
    968     )
    969     .map_err(map_binding_error)?;
    970     let raw_file = source_snapshot
    971         .files()
    972         .first()
    973         .filter(|file| {
    974             source_snapshot.entry_count() == 1
    975                 && source_snapshot.files().len() == 1
    976                 && file.relative_path() == Path::new(GRADLE_RAW_GRAPH_NAME)
    977         })
    978         .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InventoryMismatch))?;
    979     let (raw_bytes, raw_evidence) = source_snapshot
    980         .read_evidenced(raw_file, MAX_GRADLE_GRAPH_BYTES)
    981         .map_err(map_binding_error)?;
    982     let raw_value: Value = serde_json::from_slice(&raw_bytes)
    983         .map_err(|_| AdvisoryError::new(AdvisoryFailureKind::InventoryMismatch))?;
    984     if canonical_json_without_lf(&raw_value) != raw_bytes {
    985         return Err(AdvisoryError::new(AdvisoryFailureKind::InventoryMismatch));
    986     }
    987     let raw: RawGradleGraph = serde_json::from_value(raw_value)
    988         .map_err(|_| AdvisoryError::new(AdvisoryFailureKind::InventoryMismatch))?;
    989     let artifact_source_roots_sha256 = artifact_source_roots_digest(artifact_source_roots)?;
    990     let normalized = normalize_raw_gradle_graph(
    991         &raw,
    992         authority,
    993         source_revision,
    994         expected_dependency_count,
    995         artifact_source_roots,
    996     )?;
    997 
    998     if !materialization_parent.is_absolute() {
    999         return Err(AdvisoryError::new(AdvisoryFailureKind::InvalidSnapshot));
   1000     }
   1001     safe_artifact_io::validate_trusted_output_directory(materialization_parent)
   1002         .map_err(map_binding_error)?;
   1003     let materialization = tempfile::Builder::new()
   1004         .prefix(".radroots-advisory-scan-")
   1005         .tempdir_in(materialization_parent)
   1006         .map_err(|_| AdvisoryError::new(AdvisoryFailureKind::Unavailable))?;
   1007     #[cfg(unix)]
   1008     {
   1009         use std::os::unix::fs::PermissionsExt as _;
   1010         std::fs::set_permissions(
   1011             materialization.path(),
   1012             std::fs::Permissions::from_mode(0o700),
   1013         )
   1014         .map_err(|_| AdvisoryError::new(AdvisoryFailureKind::Unavailable))?;
   1015     }
   1016     #[cfg(not(unix))]
   1017     {
   1018         return Err(AdvisoryError::new(AdvisoryFailureKind::Unavailable));
   1019     }
   1020 
   1021     let mut materialized = BTreeMap::<(String, String), FileEvidence>::new();
   1022     for (artifact, source) in normalized.artifacts.iter().zip(&normalized.source_paths) {
   1023         let key = (artifact.artifact_sha256.clone(), artifact.extension.clone());
   1024         let evidence = if let Some(existing) = materialized.get(&key) {
   1025             let observed = safe_artifact_io::hash_regular_path(source, MAX_GRADLE_ARTIFACT_BYTES)
   1026                 .map_err(map_binding_error)?;
   1027             if &observed != existing {
   1028                 return Err(AdvisoryError::new(AdvisoryFailureKind::BindingChanged));
   1029             }
   1030             observed
   1031         } else {
   1032             let observed = safe_artifact_io::copy_regular_to_new_path(
   1033                 source,
   1034                 &materialization.path().join(&artifact.materialized_name),
   1035                 MAX_GRADLE_ARTIFACT_BYTES,
   1036             )
   1037             .map_err(map_binding_error)?;
   1038             materialized.insert(key, observed.clone());
   1039             observed
   1040         };
   1041         if evidence.byte_length != artifact.byte_length
   1042             || evidence.sha256 != artifact.artifact_sha256
   1043         {
   1044             return Err(AdvisoryError::new(AdvisoryFailureKind::BindingChanged));
   1045         }
   1046     }
   1047     let maximum_files = u64::try_from(materialized.len().max(1))
   1048         .map_err(|_| AdvisoryError::new(AdvisoryFailureKind::InventoryMismatch))?;
   1049     let materialized_snapshot = safe_artifact_io::traverse_regular_files(
   1050         materialization.path(),
   1051         TraversalLimits {
   1052             max_entries: maximum_files,
   1053             max_files: maximum_files,
   1054             max_total_bytes: MAX_GRADLE_ARTIFACT_BYTES,
   1055             max_file_bytes: MAX_GRADLE_ARTIFACT_BYTES,
   1056             max_depth: 1,
   1057             max_path_bytes: 128,
   1058         },
   1059         &[],
   1060     )
   1061     .map_err(map_binding_error)?;
   1062     if materialized_snapshot.files().len() != materialized.len()
   1063         || materialized_snapshot.root_permission_mode() != 0o700
   1064         || materialized_snapshot
   1065             .files()
   1066             .iter()
   1067             .any(|file| file.permission_mode() != 0o600)
   1068     {
   1069         return Err(AdvisoryError::new(AdvisoryFailureKind::BindingChanged));
   1070     }
   1071     let materialized_tree_sha256 = materialized_tree_digest(&materialized_snapshot)?;
   1072     let canonical_graph_sha256 = gradle_graph_digest_parts(
   1073         authority.id,
   1074         &normalized.components,
   1075         &normalized.edges,
   1076         &normalized.artifacts,
   1077     )?;
   1078     let normalization_receipt_sha256 = gradle_normalization_receipt_digest(
   1079         authority.id,
   1080         raw_evidence.byte_length,
   1081         &raw_evidence.sha256,
   1082         &canonical_graph_sha256,
   1083         &materialized_tree_sha256,
   1084         &artifact_source_roots_sha256,
   1085     )?;
   1086     source_snapshot.revalidate().map_err(map_binding_error)?;
   1087     materialized_snapshot
   1088         .revalidate()
   1089         .map_err(map_binding_error)?;
   1090     Ok(AdmittedGradleGraph {
   1091         raw: raw_evidence,
   1092         workload_id: authority.id.to_owned(),
   1093         components: normalized.components,
   1094         edges: normalized.edges,
   1095         artifacts: normalized.artifacts,
   1096         canonical_graph_sha256,
   1097         materialized_tree_sha256,
   1098         normalization_receipt_sha256,
   1099         artifact_source_roots_sha256,
   1100         source_snapshot,
   1101         materialized_snapshot,
   1102         _materialization: materialization,
   1103     })
   1104 }
   1105 
   1106 fn normalize_raw_gradle_graph(
   1107     raw: &RawGradleGraph,
   1108     authority: &WorkloadAuthority,
   1109     source_revision: &str,
   1110     expected_dependency_count: u64,
   1111     artifact_source_roots: &[GradleArtifactSourceRoot<'_>],
   1112 ) -> Result<NormalizedRawGradleGraph, AdvisoryError> {
   1113     if raw.schema != "radroots.gradle-advisory-graph.v1"
   1114         || authority.package_manager != "gradle"
   1115         || raw.workload_id != authority.id
   1116         || raw.build_root != authority.build_root
   1117         || raw.project_path.as_str()
   1118             != authority
   1119                 .project_path
   1120                 .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InvalidContract))?
   1121         || raw.configuration.as_str()
   1122             != authority
   1123                 .configuration
   1124                 .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InvalidContract))?
   1125         || raw.components.is_empty()
   1126         || raw.components.len() > MAX_GRADLE_COMPONENTS
   1127         || raw.edges.len() > MAX_GRADLE_EDGES
   1128         || raw.artifacts.len() > MAX_GRADLE_ARTIFACTS
   1129         || raw.edges.len() as u64 != expected_dependency_count
   1130     {
   1131         return Err(AdvisoryError::new(AdvisoryFailureKind::InventoryMismatch));
   1132     }
   1133     require_strict_canonical_order(&raw.components)?;
   1134     require_strict_canonical_order(&raw.edges)?;
   1135     require_strict_canonical_order(&raw.artifacts)?;
   1136 
   1137     let mut components = Vec::with_capacity(raw.components.len());
   1138     let mut component_by_core = BTreeMap::<Vec<u8>, (String, RawGradleVariantEnvelope)>::new();
   1139     for component in &raw.components {
   1140         let core = raw_component_core(component);
   1141         validate_raw_component_core(&core)?;
   1142         validate_raw_variant_envelope(&component.variant)?;
   1143         let variant = serde_json::to_value(&component.variant)
   1144             .map_err(|_| AdvisoryError::new(AdvisoryFailureKind::InventoryMismatch))?;
   1145         let mut normalized = GradleComponent {
   1146             root: component.root,
   1147             kind: component.kind.clone(),
   1148             group: component.group.clone(),
   1149             name: component.name.clone(),
   1150             version: component.version.clone(),
   1151             build_root: component.build_root.clone(),
   1152             project_path: component.project_path.clone(),
   1153             variant,
   1154             variant_sha256: String::new(),
   1155             identity_sha256: String::new(),
   1156         };
   1157         normalized.variant_sha256 = gradle_variant_digest(&normalized.variant)?;
   1158         normalized.identity_sha256 = gradle_component_digest(&normalized)?;
   1159         let core_key = canonical_row_key(&core)?;
   1160         if component_by_core
   1161             .insert(
   1162                 core_key,
   1163                 (
   1164                     normalized.identity_sha256.clone(),
   1165                     component.variant.clone(),
   1166                 ),
   1167             )
   1168             .is_some()
   1169         {
   1170             return Err(AdvisoryError::new(AdvisoryFailureKind::InventoryMismatch));
   1171         }
   1172         components.push(normalized);
   1173     }
   1174     components.sort_by(|left, right| left.identity_sha256.cmp(&right.identity_sha256));
   1175     if components
   1176         .windows(2)
   1177         .any(|window| window[0].identity_sha256 >= window[1].identity_sha256)
   1178     {
   1179         return Err(AdvisoryError::new(AdvisoryFailureKind::InventoryMismatch));
   1180     }
   1181     let roots = components
   1182         .iter()
   1183         .filter(|component| component.root)
   1184         .collect::<Vec<_>>();
   1185     if roots.len() != 1
   1186         || roots[0].kind != "project"
   1187         || roots[0].build_root.as_deref() != Some(authority.build_root)
   1188         || roots[0].project_path.as_deref() != authority.project_path
   1189     {
   1190         return Err(AdvisoryError::new(AdvisoryFailureKind::InventoryMismatch));
   1191     }
   1192 
   1193     let mut edges = Vec::with_capacity(raw.edges.len());
   1194     for edge in &raw.edges {
   1195         validate_raw_component_core(&edge.from)?;
   1196         validate_raw_component_core(&edge.to)?;
   1197         validate_raw_selector(&edge.requested)?;
   1198         validate_raw_variant(&edge.selected_variant, 0)?;
   1199         let (from_identity, _) = component_by_core
   1200             .get(&canonical_row_key(&edge.from)?)
   1201             .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InventoryMismatch))?;
   1202         let (to_identity, to_variants) = component_by_core
   1203             .get(&canonical_row_key(&edge.to)?)
   1204             .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InventoryMismatch))?;
   1205         if from_identity == to_identity || !to_variants.selected.contains(&edge.selected_variant) {
   1206             return Err(AdvisoryError::new(AdvisoryFailureKind::InventoryMismatch));
   1207         }
   1208         let requested = serde_json::to_value(&edge.requested)
   1209             .map_err(|_| AdvisoryError::new(AdvisoryFailureKind::InventoryMismatch))?;
   1210         let selected_variant = serde_json::to_value(&edge.selected_variant)
   1211             .map_err(|_| AdvisoryError::new(AdvisoryFailureKind::InventoryMismatch))?;
   1212         edges.push(GradleEdge {
   1213             from_identity_sha256: from_identity.clone(),
   1214             to_identity_sha256: to_identity.clone(),
   1215             requested_sha256: gradle_request_digest(&requested)?,
   1216             requested,
   1217             constraint: edge.constraint,
   1218             selected_variant_sha256: gradle_variant_digest(&selected_variant)?,
   1219             selected_variant,
   1220         });
   1221     }
   1222     edges.sort_by(|left, right| {
   1223         canonical_row_key(left)
   1224             .unwrap_or_default()
   1225             .cmp(&canonical_row_key(right).unwrap_or_default())
   1226     });
   1227     require_strict_canonical_order(&edges)?;
   1228     let mut reachable = BTreeSet::from([roots[0].identity_sha256.as_str()]);
   1229     loop {
   1230         let before = reachable.len();
   1231         for edge in &edges {
   1232             if reachable.contains(edge.from_identity_sha256.as_str()) {
   1233                 reachable.insert(edge.to_identity_sha256.as_str());
   1234             }
   1235         }
   1236         if reachable.len() == before {
   1237             break;
   1238         }
   1239     }
   1240     if reachable.len() != components.len() {
   1241         return Err(AdvisoryError::new(AdvisoryFailureKind::InventoryMismatch));
   1242     }
   1243 
   1244     let mut artifacts = Vec::with_capacity(raw.artifacts.len());
   1245     let mut source_paths = Vec::with_capacity(raw.artifacts.len());
   1246     let mut total_artifact_bytes = 0_u64;
   1247     for artifact in &raw.artifacts {
   1248         validate_raw_component_core(&artifact.component)?;
   1249         validate_raw_variant(&artifact.variant, 0)?;
   1250         let (component_identity, variants) = component_by_core
   1251             .get(&canonical_row_key(&artifact.component)?)
   1252             .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InventoryMismatch))?;
   1253         if !variants.selected.contains(&artifact.variant)
   1254             || artifact.group != artifact.component.group
   1255             || artifact.name != artifact.component.name
   1256             || artifact.version != artifact.component.version
   1257             || !valid_ascii_text(&artifact.module_version.group, 256, false)
   1258             || !valid_ascii_text(&artifact.module_version.name, 256, false)
   1259             || !valid_ascii_text(&artifact.module_version.version, 128, false)
   1260             || artifact.observed_byte_length == 0
   1261             || artifact.observed_byte_length > MAX_GRADLE_ARTIFACT_BYTES
   1262             || !valid_hex(&artifact.observed_sha256, 64)
   1263             || !valid_ascii_text(&artifact.artifact_name, 256, false)
   1264             || artifact.artifact_name.contains(['/', '\\'])
   1265             || !valid_ascii_text(&artifact.logical_name, 256, false)
   1266             || !valid_ascii_text(&artifact.artifact_type, 64, false)
   1267             || !valid_artifact_extension(&artifact.extension)
   1268             || !artifact
   1269                 .artifact_name
   1270                 .ends_with(&format!(".{}", artifact.extension))
   1271             || artifact
   1272                 .classifier
   1273                 .as_deref()
   1274                 .is_some_and(|value| !valid_ascii_text(value, 128, false))
   1275             || !valid_ascii_text(&artifact.source_path, 4_096, false)
   1276         {
   1277             return Err(AdvisoryError::new(AdvisoryFailureKind::InventoryMismatch));
   1278         }
   1279         let source_path = PathBuf::from(&artifact.source_path);
   1280         let source_roots = artifact_source_roots
   1281             .iter()
   1282             .filter_map(|root| {
   1283                 source_path
   1284                     .strip_prefix(root.path)
   1285                     .ok()
   1286                     .filter(|relative| {
   1287                         !relative.as_os_str().is_empty()
   1288                             && relative
   1289                                 .components()
   1290                                 .all(|component| matches!(component, Component::Normal(_)))
   1291                     })
   1292                     .map(|relative| (root, relative))
   1293             })
   1294             .collect::<Vec<_>>();
   1295         if !valid_absolute_normal_path(&source_path) || source_roots.len() != 1 {
   1296             return Err(AdvisoryError::new(AdvisoryFailureKind::InventoryMismatch));
   1297         }
   1298         let (source_root, relative) = source_roots[0];
   1299         let source_evidence =
   1300             safe_artifact_io::hash_regular(source_root.path, relative, MAX_GRADLE_ARTIFACT_BYTES)
   1301                 .map_err(map_binding_error)?;
   1302         if source_evidence.byte_length != artifact.observed_byte_length
   1303             || source_evidence.sha256 != artifact.observed_sha256
   1304         {
   1305             return Err(AdvisoryError::new(AdvisoryFailureKind::BindingChanged));
   1306         }
   1307         total_artifact_bytes = total_artifact_bytes
   1308             .checked_add(artifact.observed_byte_length)
   1309             .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InventoryMismatch))?;
   1310         if total_artifact_bytes > MAX_GRADLE_ARTIFACT_BYTES {
   1311             return Err(AdvisoryError::new(AdvisoryFailureKind::InventoryMismatch));
   1312         }
   1313         let component = components
   1314             .iter()
   1315             .find(|component| &component.identity_sha256 == component_identity)
   1316             .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InventoryMismatch))?;
   1317         let component_label =
   1318             match component.kind.as_str() {
   1319                 "module" => {
   1320                     let group = component.group.as_deref().ok_or_else(|| {
   1321                         AdvisoryError::new(AdvisoryFailureKind::InventoryMismatch)
   1322                     })?;
   1323                     let name = component.name.as_deref().ok_or_else(|| {
   1324                         AdvisoryError::new(AdvisoryFailureKind::InventoryMismatch)
   1325                     })?;
   1326                     let version = component.version.as_deref().ok_or_else(|| {
   1327                         AdvisoryError::new(AdvisoryFailureKind::InventoryMismatch)
   1328                     })?;
   1329                     if artifact.module_version.group != group
   1330                         || artifact.module_version.name != name
   1331                         || artifact.module_version.version != version
   1332                     {
   1333                         return Err(AdvisoryError::new(AdvisoryFailureKind::InventoryMismatch));
   1334                     }
   1335                     format!("{group}:{name}:{version}")
   1336                 }
   1337                 "project" => {
   1338                     let root = component.build_root.as_deref().ok_or_else(|| {
   1339                         AdvisoryError::new(AdvisoryFailureKind::InventoryMismatch)
   1340                     })?;
   1341                     let project = component.project_path.as_deref().ok_or_else(|| {
   1342                         AdvisoryError::new(AdvisoryFailureKind::InventoryMismatch)
   1343                     })?;
   1344                     if !valid_project_module_version(
   1345                         root,
   1346                         project,
   1347                         &artifact.module_version,
   1348                         source_revision,
   1349                     ) {
   1350                         return Err(AdvisoryError::new(AdvisoryFailureKind::InventoryMismatch));
   1351                     }
   1352                     format!("{root}:{project}")
   1353                 }
   1354                 _ => {
   1355                     return Err(AdvisoryError::new(AdvisoryFailureKind::InventoryMismatch));
   1356                 }
   1357             };
   1358         let variant = serde_json::to_value(&artifact.variant)
   1359             .map_err(|_| AdvisoryError::new(AdvisoryFailureKind::InventoryMismatch))?;
   1360         artifacts.push(GradleArtifact {
   1361             component_identity_sha256: component_identity.clone(),
   1362             component: component_label,
   1363             package_ecosystem: "maven".to_owned(),
   1364             package_namespace: artifact.module_version.group.clone(),
   1365             package_name: artifact.module_version.name.clone(),
   1366             package_version: artifact.module_version.version.clone(),
   1367             artifact_sha256: artifact.observed_sha256.clone(),
   1368             artifact_name: artifact.artifact_name.clone(),
   1369             logical_name: artifact.logical_name.clone(),
   1370             artifact_type: artifact.artifact_type.clone(),
   1371             classifier: artifact.classifier.clone(),
   1372             byte_length: artifact.observed_byte_length,
   1373             extension: artifact.extension.clone(),
   1374             materialized_name: format!("{}.{}", artifact.observed_sha256, artifact.extension),
   1375             variant_sha256: gradle_variant_digest(&variant)?,
   1376             variant,
   1377         });
   1378         source_paths.push(source_path);
   1379     }
   1380     let mut paired = artifacts.into_iter().zip(source_paths).collect::<Vec<_>>();
   1381     paired.sort_by(|left, right| {
   1382         canonical_row_key(&left.0)
   1383             .unwrap_or_default()
   1384             .cmp(&canonical_row_key(&right.0).unwrap_or_default())
   1385     });
   1386     if paired.windows(2).any(|window| window[0].0 == window[1].0) {
   1387         return Err(AdvisoryError::new(AdvisoryFailureKind::InventoryMismatch));
   1388     }
   1389     let (artifacts, source_paths): (Vec<_>, Vec<_>) = paired.into_iter().unzip();
   1390     Ok(NormalizedRawGradleGraph {
   1391         components,
   1392         edges,
   1393         artifacts,
   1394         source_paths,
   1395     })
   1396 }
   1397 
   1398 fn raw_component_core(component: &RawGradleComponent) -> RawGradleComponentCore {
   1399     RawGradleComponentCore {
   1400         build_root: component.build_root.clone(),
   1401         group: component.group.clone(),
   1402         kind: component.kind.clone(),
   1403         name: component.name.clone(),
   1404         project_path: component.project_path.clone(),
   1405         version: component.version.clone(),
   1406     }
   1407 }
   1408 
   1409 fn valid_absolute_normal_path(path: &Path) -> bool {
   1410     path.is_absolute()
   1411         && path
   1412             .components()
   1413             .all(|component| matches!(component, Component::RootDir | Component::Normal(_)))
   1414 }
   1415 
   1416 fn artifact_source_roots_digest(
   1417     roots: &[GradleArtifactSourceRoot<'_>],
   1418 ) -> Result<String, AdvisoryError> {
   1419     if roots.is_empty() || roots.len() > 2 {
   1420         return Err(AdvisoryError::new(AdvisoryFailureKind::InventoryMismatch));
   1421     }
   1422     let mut rows = Vec::with_capacity(roots.len());
   1423     let mut roles = BTreeSet::new();
   1424     for root in roots {
   1425         if !valid_absolute_normal_path(root.path)
   1426             || !matches!(
   1427                 root.logical_role,
   1428                 "candidate_build_output" | "governed_seed_cache"
   1429             )
   1430             || !roles.insert(root.logical_role)
   1431             || !valid_hex(root.identity_sha256, 64)
   1432         {
   1433             return Err(AdvisoryError::new(AdvisoryFailureKind::InventoryMismatch));
   1434         }
   1435         rows.push(serde_json::json!({
   1436             "identity_sha256": root.identity_sha256,
   1437             "logical_role": root.logical_role,
   1438         }));
   1439     }
   1440     rows.sort_by(|left, right| {
   1441         canonical_json_without_lf(left).cmp(&canonical_json_without_lf(right))
   1442     });
   1443     domain_json_digest(
   1444         b"radroots-advisory-gradle-artifact-source-roots-v1\0",
   1445         &rows,
   1446     )
   1447 }
   1448 
   1449 #[derive(Clone, Copy)]
   1450 struct WorkloadAuthority {
   1451     id: &'static str,
   1452     repository: &'static str,
   1453     build_root: &'static str,
   1454     package_manager: &'static str,
   1455     language: &'static str,
   1456     manifest_path: Option<&'static str>,
   1457     lockfile_path: Option<&'static str>,
   1458     project_path: Option<&'static str>,
   1459     configuration: Option<&'static str>,
   1460 }
   1461 
   1462 const WORKLOADS: [WorkloadAuthority; 16] = [
   1463     cargo_workload("cli", "oss/cli", ".", "Cargo.toml", "Cargo.lock"),
   1464     cargo_workload(
   1465         "harvest_core",
   1466         "oss/harvestcircle",
   1467         "core",
   1468         "core/Cargo.toml",
   1469         "core/Cargo.lock",
   1470     ),
   1471     cargo_workload(
   1472         "harvest_xtask",
   1473         "oss/harvestcircle",
   1474         "tools/xtask",
   1475         "tools/xtask/Cargo.toml",
   1476         "tools/xtask/Cargo.lock",
   1477     ),
   1478     cargo_workload(
   1479         "ios_ffi",
   1480         "oss/ios_app",
   1481         "RadrootsFFI",
   1482         "Cargo.toml",
   1483         "Cargo.lock",
   1484     ),
   1485     cargo_workload("lib", "oss/lib", ".", "Cargo.toml", "Cargo.lock"),
   1486     cargo_workload("myc", "oss/myc", ".", "Cargo.toml", "Cargo.lock"),
   1487     cargo_workload(
   1488         "radrootsd",
   1489         "oss/radrootsd",
   1490         ".",
   1491         "Cargo.toml",
   1492         "Cargo.lock",
   1493     ),
   1494     cargo_workload("rhi", "oss/rhi", ".", "Cargo.toml", "Cargo.lock"),
   1495     cargo_workload("root", "_radroots", ".", "Cargo.toml", "Cargo.lock"),
   1496     cargo_workload("sdk", "oss/sdk", ".", "Cargo.toml", "Cargo.lock"),
   1497     gradle_workload(
   1498         "app_design_system",
   1499         ".",
   1500         ":app:design_system",
   1501         "desktopRuntimeClasspath",
   1502     ),
   1503     gradle_workload("app_desktop", ".", ":app:desktop", "runtimeClasspath"),
   1504     gradle_workload("app_shared", ".", ":app:shared", "desktopRuntimeClasspath"),
   1505     gradle_workload(
   1506         "tools_design_catalog",
   1507         ".",
   1508         ":tools:design_catalog",
   1509         "desktopRuntimeClasspath",
   1510     ),
   1511     gradle_workload(
   1512         "build_logic_contracts",
   1513         "build-logic",
   1514         ":contracts",
   1515         "runtimeClasspath",
   1516     ),
   1517     gradle_workload(
   1518         "build_logic_plugins",
   1519         "build-logic",
   1520         ":plugins",
   1521         "runtimeClasspath",
   1522     ),
   1523 ];
   1524 
   1525 const fn cargo_workload(
   1526     id: &'static str,
   1527     repository: &'static str,
   1528     build_root: &'static str,
   1529     manifest_path: &'static str,
   1530     lockfile_path: &'static str,
   1531 ) -> WorkloadAuthority {
   1532     WorkloadAuthority {
   1533         id,
   1534         repository,
   1535         build_root,
   1536         package_manager: "cargo",
   1537         language: "rust",
   1538         manifest_path: Some(manifest_path),
   1539         lockfile_path: Some(lockfile_path),
   1540         project_path: None,
   1541         configuration: None,
   1542     }
   1543 }
   1544 
   1545 const fn gradle_workload(
   1546     id: &'static str,
   1547     build_root: &'static str,
   1548     project_path: &'static str,
   1549     configuration: &'static str,
   1550 ) -> WorkloadAuthority {
   1551     WorkloadAuthority {
   1552         id,
   1553         repository: "oss/harvestcircle",
   1554         build_root,
   1555         package_manager: "gradle",
   1556         language: "kotlin",
   1557         manifest_path: None,
   1558         lockfile_path: None,
   1559         project_path: Some(project_path),
   1560         configuration: Some(configuration),
   1561     }
   1562 }
   1563 
   1564 pub(crate) fn validate_decision(root: &Path) -> Result<(), String> {
   1565     let bytes = safe_artifact_io::read_regular_path(&root.join(DECISION_PATH), 1_048_576)
   1566         .map_err(|_| "advisory snapshot decision admission failed".to_owned())?;
   1567     if bytes != DECISION_BYTES {
   1568         return Err("advisory snapshot decision differs from compiled authority".to_owned());
   1569     }
   1570     let _: Value = serde_json::from_slice(&bytes)
   1571         .map_err(|_| "advisory snapshot decision is invalid".to_owned())?;
   1572     // Exact equality with the compiled authority above already binds whitespace
   1573     // and object-member order. Re-serializing through `Value` would instead sort
   1574     // object keys and reject the repository's intentional semantic key order.
   1575     Ok(())
   1576 }
   1577 
   1578 pub(crate) fn admit_snapshot(
   1579     root: &Path,
   1580     materialization_parent: &Path,
   1581     request: &AdmissionRequest,
   1582 ) -> Result<AdvisorySnapshotEvidence, AdvisoryError> {
   1583     validate_request(request)?;
   1584     let snapshot = safe_artifact_io::traverse_regular_files(
   1585         root,
   1586         TraversalLimits {
   1587             max_entries: SNAPSHOT_FILE_NAMES.len() as u64,
   1588             max_files: SNAPSHOT_FILE_NAMES.len() as u64,
   1589             max_total_bytes: 4_430_233_600,
   1590             max_file_bytes: 2_147_483_648,
   1591             max_depth: 1,
   1592             max_path_bytes: 128,
   1593         },
   1594         &[],
   1595     )
   1596     .map_err(map_binding_error)?;
   1597     let files = exact_snapshot_files(&snapshot)?;
   1598     let manifest_file = files
   1599         .get(MANIFEST_NAME)
   1600         .copied()
   1601         .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InventoryMismatch))?;
   1602     let (manifest_bytes, manifest_evidence) = snapshot
   1603         .read_evidenced(manifest_file, MAX_SNAPSHOT_MANIFEST_BYTES)
   1604         .map_err(map_binding_error)?;
   1605     let manifest: SnapshotManifest = serde_json::from_slice(&manifest_bytes)
   1606         .map_err(|_| AdvisoryError::new(AdvisoryFailureKind::InvalidSnapshot))?;
   1607     let manifest_value: Value = serde_json::from_slice(&manifest_bytes)
   1608         .map_err(|_| AdvisoryError::new(AdvisoryFailureKind::InvalidSnapshot))?;
   1609     if canonical_json(&manifest_value) != manifest_bytes {
   1610         return Err(AdvisoryError::new(AdvisoryFailureKind::InvalidSnapshot));
   1611     }
   1612     validate_manifest(&manifest, request)?;
   1613 
   1614     let mut archive_evidence = BTreeMap::new();
   1615     let mut materialized_archives = Vec::new();
   1616     let mut report_evidences = BTreeMap::new();
   1617     let mut findings = Vec::new();
   1618     for graph in &request.admitted_gradle_graphs {
   1619         graph.revalidate()?;
   1620     }
   1621     for provider in &manifest.provider_snapshot {
   1622         let archive_file = files
   1623             .get(provider.archive.path.as_str())
   1624             .copied()
   1625             .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InventoryMismatch))?;
   1626         let archive = snapshot
   1627             .admit_deterministic_tar_gzip(archive_file, archive_limits())
   1628             .map_err(map_archive_error)?;
   1629         validate_blob(&provider.archive, &archive.compressed)?;
   1630         if provider.archive_expanded_bytes != archive.expanded_bytes
   1631             || provider.archive_member_count != archive.member_count
   1632             || provider.archive_payload_bytes != archive.payload_bytes
   1633         {
   1634             return Err(AdvisoryError::new(AdvisoryFailureKind::BindingChanged));
   1635         }
   1636         let materialized = snapshot
   1637             .materialize_deterministic_tar_gzip(
   1638                 archive_file,
   1639                 materialization_parent,
   1640                 archive_limits(),
   1641             )
   1642             .map_err(map_archive_error)?;
   1643         let materialized_tree = materialized_tree_digest(materialized.snapshot())?;
   1644         if materialized.evidence() != &archive
   1645             || materialized_tree != provider.materialized_tree_sha256
   1646             || !valid_hex(&provider.database_identity_sha256, 64)
   1647         {
   1648             return Err(AdvisoryError::new(AdvisoryFailureKind::BindingChanged));
   1649         }
   1650         materialized.revalidate().map_err(map_binding_error)?;
   1651         archive_evidence.insert(provider.provider, archive);
   1652         materialized_archives.push(materialized);
   1653 
   1654         let report_file = files
   1655             .get(provider.report.path.as_str())
   1656             .copied()
   1657             .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InventoryMismatch))?;
   1658         let (report_bytes, report_evidence) = snapshot
   1659             .read_evidenced(report_file, MAX_REPORT_BYTES)
   1660             .map_err(map_binding_error)?;
   1661         validate_blob(&provider.report, &report_evidence)?;
   1662         findings.extend(parse_unsuppressed_report(
   1663             provider,
   1664             &report_bytes,
   1665             request,
   1666             &manifest.gradle_projection,
   1667         )?);
   1668         report_evidences.insert(provider.provider, report_evidence);
   1669     }
   1670     for graph in &request.admitted_gradle_graphs {
   1671         graph.revalidate()?;
   1672     }
   1673     for materialized in &materialized_archives {
   1674         materialized.revalidate().map_err(map_binding_error)?;
   1675     }
   1676     for output in &request.admitted_scanner_outputs {
   1677         output.revalidate()?;
   1678     }
   1679 
   1680     let unsuppressed_count = findings.len();
   1681     let finding_inventory_sha256 = finding_digest(&findings)?;
   1682     apply_suppressions(
   1683         &mut findings,
   1684         &manifest.suppressions,
   1685         request.evaluation_epoch,
   1686     )?;
   1687     if !findings.is_empty() {
   1688         return Err(AdvisoryError::new(AdvisoryFailureKind::KnownVulnerability));
   1689     }
   1690     snapshot.revalidate().map_err(map_binding_error)?;
   1691     for materialized in &materialized_archives {
   1692         materialized.revalidate().map_err(map_binding_error)?;
   1693     }
   1694     for output in &request.admitted_scanner_outputs {
   1695         output.revalidate()?;
   1696     }
   1697     let rustsec = archive_evidence
   1698         .remove(&ProviderId::Rustsec)
   1699         .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InventoryMismatch))?;
   1700     let nvd = archive_evidence
   1701         .remove(&ProviderId::OwaspNvd)
   1702         .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InventoryMismatch))?;
   1703     let rustsec_report = report_evidences
   1704         .remove(&ProviderId::Rustsec)
   1705         .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InventoryMismatch))?;
   1706     let owasp_report = report_evidences
   1707         .remove(&ProviderId::OwaspNvd)
   1708         .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InventoryMismatch))?;
   1709     let provider_freshness = manifest
   1710         .provider_snapshot
   1711         .iter()
   1712         .map(|provider| ProviderFreshnessEvidence {
   1713             provider: provider.provider.as_str().to_owned(),
   1714             acquired_at_epoch: provider.acquired_at_epoch,
   1715             digest_time_epoch: provider.digest_time_epoch,
   1716             analyzed_at_epoch: provider.analyzed_at_epoch,
   1717             database_identity_sha256: provider.database_identity_sha256.clone(),
   1718             archive_sha256: provider.archive.sha256.clone(),
   1719             report_sha256: provider.report.sha256.clone(),
   1720             network_trace_sha256: provider.network_trace_sha256.clone(),
   1721         })
   1722         .collect();
   1723     Ok(AdvisorySnapshotEvidence {
   1724         candidate: manifest.candidate,
   1725         manifest: manifest_evidence,
   1726         source_inventory_sha256: source_digest(&manifest.sources)?,
   1727         inventory_sha256: manifest.inventory_sha256,
   1728         fresh_tool_observation_sha256: manifest.fresh_tool_observation_sha256,
   1729         tool_row_projection_sha256: tool_observation_digest(
   1730             &manifest.tool_acquisition,
   1731             &manifest.tool_state,
   1732         )?,
   1733         rustsec_archive: rustsec,
   1734         nvd_archive: nvd,
   1735         rustsec_report,
   1736         owasp_report,
   1737         provider_time_trace_sha256: provider_trace_digest(&manifest.provider_snapshot)?,
   1738         finding_inventory_sha256,
   1739         suppression_inventory_sha256: suppression_digest(&manifest.suppressions)?,
   1740         candidate_advisory_input_sha256: manifest.candidate_advisory_input_sha256,
   1741         gradle_projection_sha256: gradle_projection_digest(&manifest.gradle_projection)?,
   1742         provider_freshness,
   1743         finding_count: unsuppressed_count,
   1744         suppression_count: manifest.suppressions.len(),
   1745     })
   1746 }
   1747 
   1748 fn exact_snapshot_files(
   1749     snapshot: &TraversalSnapshot,
   1750 ) -> Result<BTreeMap<&str, &TraversedFile>, AdvisoryError> {
   1751     if snapshot.entry_count() != SNAPSHOT_FILE_NAMES.len() as u64
   1752         || snapshot.files().len() != SNAPSHOT_FILE_NAMES.len()
   1753     {
   1754         return Err(AdvisoryError::new(AdvisoryFailureKind::InventoryMismatch));
   1755     }
   1756     let mut files = BTreeMap::new();
   1757     for file in snapshot.files() {
   1758         let relative = file
   1759             .relative_path()
   1760             .to_str()
   1761             .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InventoryMismatch))?;
   1762         if Path::new(relative).components().count() != 1
   1763             || !SNAPSHOT_FILE_NAMES.contains(&relative)
   1764             || files.insert(relative, file).is_some()
   1765         {
   1766             return Err(AdvisoryError::new(AdvisoryFailureKind::InventoryMismatch));
   1767         }
   1768     }
   1769     Ok(files)
   1770 }
   1771 
   1772 fn validate_request(request: &AdmissionRequest) -> Result<(), AdvisoryError> {
   1773     if request.candidate.generation == 0
   1774         || !valid_hex(&request.candidate.digest, 64)
   1775         || request.evaluation_epoch == 0
   1776         || request.sources.is_empty()
   1777         || request.inventory.len() != WORKLOADS.len()
   1778         || request.admitted_gradle_graphs.len()
   1779             != WORKLOADS
   1780                 .iter()
   1781                 .filter(|workload| workload.package_manager == "gradle")
   1782                 .count()
   1783         || request.admitted_scanner_outputs.len() != WORKLOADS.len()
   1784     {
   1785         return Err(AdvisoryError::new(AdvisoryFailureKind::InvalidSnapshot));
   1786     }
   1787     validate_trusted_authority(request)?;
   1788     if request.sources.len() != CANDIDATE_REPOSITORIES.len()
   1789         || !request
   1790             .sources
   1791             .iter()
   1792             .map(|source| source.repository.as_str())
   1793             .eq(CANDIDATE_REPOSITORIES)
   1794     {
   1795         return Err(AdvisoryError::new(AdvisoryFailureKind::InventoryMismatch));
   1796     }
   1797     let mut repositories = BTreeSet::new();
   1798     for source in &request.sources {
   1799         if !valid_identifier(&source.repository)
   1800             || !valid_hex(&source.revision, 40)
   1801             || !valid_hex(&source.tree, 40)
   1802             || !repositories.insert(source.repository.as_str())
   1803         {
   1804             return Err(AdvisoryError::new(AdvisoryFailureKind::InvalidSnapshot));
   1805         }
   1806     }
   1807     for (actual, expected) in request.inventory.iter().zip(WORKLOADS) {
   1808         if actual.id != expected.id
   1809             || actual.repository != expected.repository
   1810             || actual.build_root != expected.build_root
   1811             || actual.package_manager != expected.package_manager
   1812             || actual.language != expected.language
   1813             || actual.manifest_path.as_deref() != expected.manifest_path
   1814             || actual.lockfile_path.as_deref() != expected.lockfile_path
   1815             || actual.project_path.as_deref() != expected.project_path
   1816             || actual.configuration.as_deref() != expected.configuration
   1817             || actual.dependency_count == 0
   1818             || !valid_hex(&actual.input_sha256, 64)
   1819             || !repositories.contains(actual.repository.as_str())
   1820         {
   1821             return Err(AdvisoryError::new(AdvisoryFailureKind::InventoryMismatch));
   1822         }
   1823     }
   1824     if !request
   1825         .admitted_gradle_graphs
   1826         .iter()
   1827         .map(|graph| graph.workload_id.as_str())
   1828         .eq(WORKLOADS
   1829             .iter()
   1830             .filter(|workload| workload.package_manager == "gradle")
   1831             .map(|workload| workload.id))
   1832     {
   1833         return Err(AdvisoryError::new(AdvisoryFailureKind::InventoryMismatch));
   1834     }
   1835     for graph in &request.admitted_gradle_graphs {
   1836         graph.revalidate()?;
   1837     }
   1838     let expected_scanner_outputs = [ProviderId::Rustsec, ProviderId::OwaspNvd]
   1839         .into_iter()
   1840         .flat_map(|provider| {
   1841             WORKLOADS
   1842                 .iter()
   1843                 .filter(move |workload| match provider {
   1844                     ProviderId::Rustsec => workload.package_manager == "cargo",
   1845                     ProviderId::OwaspNvd => workload.package_manager == "gradle",
   1846                 })
   1847                 .map(move |workload| (provider, workload.id))
   1848         });
   1849     if !request
   1850         .admitted_scanner_outputs
   1851         .iter()
   1852         .map(|output| (output.provider, output.workload_id.as_str()))
   1853         .eq(expected_scanner_outputs)
   1854     {
   1855         return Err(AdvisoryError::new(AdvisoryFailureKind::InventoryMismatch));
   1856     }
   1857     for output in &request.admitted_scanner_outputs {
   1858         output.revalidate()?;
   1859     }
   1860     Ok(())
   1861 }
   1862 
   1863 fn validate_manifest(
   1864     manifest: &SnapshotManifest,
   1865     request: &AdmissionRequest,
   1866 ) -> Result<(), AdvisoryError> {
   1867     if manifest.schema != "radroots.advisory-snapshot.v1"
   1868         || manifest.candidate != request.candidate
   1869         || manifest.sources != request.sources
   1870         || manifest.inventory != request.inventory
   1871         || manifest.inventory_sha256 != inventory_digest(&request.inventory)?
   1872     {
   1873         return Err(AdvisoryError::new(AdvisoryFailureKind::InventoryMismatch));
   1874     }
   1875     let (tool_manifest, tool_observation, trace, provider_evidence) =
   1876         validate_trusted_authority(request)?;
   1877     if manifest.step_297_tool_manifest_sha256 != sha256(&request.step_297_tool_manifest)
   1878         || manifest.fresh_tool_observation_sha256 != sha256(&request.fresh_tool_observation)
   1879         || manifest.tool_acquisition != tool_manifest.tool_acquisition
   1880         || manifest.tool_state != tool_observation.tool_state
   1881         || manifest.gradle_projection != provider_evidence.gradle_projection
   1882         || manifest.provider_snapshot != provider_evidence.provider_snapshot
   1883         || manifest.suppressions != provider_evidence.suppressions
   1884         || manifest.candidate_advisory_input_sha256
   1885             != provider_evidence.candidate_advisory_input_sha256
   1886     {
   1887         return Err(AdvisoryError::new(AdvisoryFailureKind::BindingChanged));
   1888     }
   1889     validate_tool_acquisitions(&manifest.tool_acquisition)?;
   1890     validate_tool_states(&manifest.tool_state, request.evaluation_epoch)?;
   1891     validate_gradle_projections(
   1892         &manifest.gradle_projection,
   1893         request,
   1894         &provider_evidence.process_receipt,
   1895     )?;
   1896     if manifest.provider_snapshot.len() != 2
   1897         || manifest.provider_snapshot[0].provider != ProviderId::Rustsec
   1898         || manifest.provider_snapshot[1].provider != ProviderId::OwaspNvd
   1899     {
   1900         return Err(AdvisoryError::new(AdvisoryFailureKind::InventoryMismatch));
   1901     }
   1902     for provider in &manifest.provider_snapshot {
   1903         validate_provider(
   1904             provider,
   1905             request,
   1906             &trace,
   1907             &provider_evidence.process_receipt,
   1908         )?;
   1909     }
   1910     validate_temporal_order(
   1911         &tool_observation,
   1912         &manifest.provider_snapshot,
   1913         &provider_evidence.process_receipt,
   1914     )?;
   1915     if manifest.candidate_advisory_input_sha256 != candidate_advisory_input_digest(manifest)? {
   1916         return Err(AdvisoryError::new(AdvisoryFailureKind::BindingChanged));
   1917     }
   1918     Ok(())
   1919 }
   1920 
   1921 fn validate_trusted_authority(
   1922     request: &AdmissionRequest,
   1923 ) -> Result<
   1924     (
   1925         TrustedToolManifest,
   1926         TrustedToolObservation,
   1927         NvdNetworkTrace,
   1928         TrustedProviderEvidence,
   1929     ),
   1930     AdvisoryError,
   1931 > {
   1932     if request.producer_request.len() > MAX_PRODUCER_REQUEST_BYTES
   1933         || request.step_297_tool_manifest.len() > MAX_TOOL_MANIFEST_BYTES
   1934         || request.fresh_tool_observation.len() > MAX_TOOL_OBSERVATION_BYTES
   1935         || request.nvd_network_trace.len() > MAX_NVD_TRACE_BYTES
   1936         || request.provider_execution_evidence.len() > MAX_PROVIDER_EVIDENCE_BYTES
   1937     {
   1938         return Err(AdvisoryError::new(AdvisoryFailureKind::InvalidSnapshot));
   1939     }
   1940     let producer: TrustedProducerRequest = parse_canonical_authority(&request.producer_request)?;
   1941     let producer_digest = sha256(&request.producer_request);
   1942     let tool_manifest: TrustedToolManifest =
   1943         parse_canonical_authority(&request.step_297_tool_manifest)?;
   1944     let observation: TrustedToolObservation =
   1945         parse_canonical_authority(&request.fresh_tool_observation)?;
   1946     let trace: NvdNetworkTrace = parse_canonical_authority(&request.nvd_network_trace)?;
   1947     let provider_evidence: TrustedProviderEvidence =
   1948         parse_canonical_authority(&request.provider_execution_evidence)?;
   1949     let row_projection =
   1950         tool_observation_digest(&tool_manifest.tool_acquisition, &observation.tool_state)?;
   1951     let expected_workloads = WORKLOADS
   1952         .iter()
   1953         .map(|workload| workload.id.to_owned())
   1954         .collect::<Vec<_>>();
   1955     let expected_query_keys = [
   1956         "lastModStartDate",
   1957         "lastModEndDate",
   1958         "startIndex",
   1959         "resultsPerPage",
   1960     ]
   1961     .into_iter()
   1962     .map(str::to_owned)
   1963     .collect::<Vec<_>>();
   1964     if producer.schema != "radroots.advisory-producer-request.v1"
   1965         || producer.candidate_generation != request.candidate.generation
   1966         || producer.candidate_digest != request.candidate.digest
   1967         || producer.platform != "macos_aarch64"
   1968         || producer.process_contract_sha256 != BOUNDED_PROCESS_DECISION_SHA256
   1969         || producer.process_runner_source_sha256 != BOUNDED_PROCESS_SOURCE_SHA256
   1970         || producer.nvd_endpoint != "/rest/json/cves/2.0"
   1971         || !valid_hex(&producer.nvd_enforcement_program_sha256, 64)
   1972         || !valid_hex(&producer.nvd_enforcement_configuration_sha256, 64)
   1973         || producer.nvd_query_keys != expected_query_keys
   1974         || producer.workload_ids != expected_workloads
   1975         || tool_manifest.schema != "radroots.advisory-tool-manifest-projection.v1"
   1976         || tool_manifest.candidate_generation != request.candidate.generation
   1977         || tool_manifest.candidate_digest != request.candidate.digest
   1978         || tool_manifest.platform != "macos_aarch64"
   1979         || tool_manifest.producer_request_sha256 != producer_digest
   1980         || observation.schema != "radroots.advisory-tool-observation.v1"
   1981         || observation.candidate_generation != request.candidate.generation
   1982         || observation.candidate_digest != request.candidate.digest
   1983         || observation.platform != "macos_aarch64"
   1984         || observation.producer_request_sha256 != producer_digest
   1985         || observation.tool_manifest_sha256 != sha256(&request.step_297_tool_manifest)
   1986         || observation.row_projection_sha256 != row_projection
   1987         || observation.observed_at_epoch > request.evaluation_epoch
   1988         || request
   1989             .evaluation_epoch
   1990             .checked_sub(observation.observed_at_epoch)
   1991             .is_none_or(|age| age > TOOL_REVIEW_SECONDS)
   1992         || trace.schema != "radroots.advisory-nvd-network-trace.v1"
   1993         || trace.candidate_generation != request.candidate.generation
   1994         || trace.candidate_digest != request.candidate.digest
   1995         || trace.producer_request_sha256 != producer_digest
   1996         || trace.enforcement != "deny_by_default_nvd_api_get_only_proxy"
   1997         || trace.enforcement_program_sha256 != producer.nvd_enforcement_program_sha256
   1998         || trace.enforcement_configuration_sha256 != producer.nvd_enforcement_configuration_sha256
   1999         || trace.request.is_empty()
   2000         || trace.request.len() > MAX_NVD_REQUESTS
   2001         || provider_evidence.schema != "radroots.advisory-provider-execution-evidence.v1"
   2002         || provider_evidence.candidate_generation != request.candidate.generation
   2003         || provider_evidence.candidate_digest != request.candidate.digest
   2004         || provider_evidence.platform != "macos_aarch64"
   2005         || provider_evidence.producer_request_sha256 != producer_digest
   2006         || provider_evidence.gradle_projection.len() != 6
   2007         || provider_evidence.provider_snapshot.len() != 2
   2008     {
   2009         return Err(AdvisoryError::new(AdvisoryFailureKind::BindingChanged));
   2010     }
   2011     validate_tool_acquisitions(&tool_manifest.tool_acquisition)?;
   2012     validate_tool_states(&observation.tool_state, request.evaluation_epoch)?;
   2013     validate_process_receipts(&provider_evidence.process_receipt, request.evaluation_epoch)?;
   2014     validate_nvd_trace(&producer, &trace, &provider_evidence.process_receipt)?;
   2015     Ok((tool_manifest, observation, trace, provider_evidence))
   2016 }
   2017 
   2018 fn validate_nvd_trace(
   2019     producer: &TrustedProducerRequest,
   2020     trace: &NvdNetworkTrace,
   2021     receipts: &[TrustedProcessReceipt],
   2022 ) -> Result<(), AdvisoryError> {
   2023     let update = process_receipt(receipts, "owasp_nvd_update")?;
   2024     let mut aggregate_bytes = 0_u64;
   2025     let mut previous_request_completed = None;
   2026     let mut previous_window: Option<(u64, u64, u64, u64, u64)> = None;
   2027     for (index, row) in trace.request.iter().enumerate() {
   2028         let values = row
   2029             .query
   2030             .iter()
   2031             .map(|query| (query.name.as_str(), query.value.as_str()))
   2032             .collect::<BTreeMap<_, _>>();
   2033         let start = values
   2034             .get("lastModStartDate")
   2035             .and_then(|value| parse_report_epoch(value).ok())
   2036             .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InvalidSnapshot))?;
   2037         let end = values
   2038             .get("lastModEndDate")
   2039             .and_then(|value| parse_report_epoch(value).ok())
   2040             .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InvalidSnapshot))?;
   2041         let start_index = values
   2042             .get("startIndex")
   2043             .and_then(|value| parse_canonical_u64(value))
   2044             .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InvalidSnapshot))?;
   2045         let results_per_page = values
   2046             .get("resultsPerPage")
   2047             .and_then(|value| parse_canonical_u64(value))
   2048             .filter(|value| (1..=2_000).contains(value))
   2049             .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InvalidSnapshot))?;
   2050         aggregate_bytes = aggregate_bytes
   2051             .checked_add(row.response_byte_length)
   2052             .filter(|value| *value <= MAX_NVD_AGGREGATE_BYTES)
   2053             .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InvalidSnapshot))?;
   2054         if row.sequence != index as u64 + 1
   2055             || row.method != "GET"
   2056             || row.scheme != "https"
   2057             || row.authority != "services.nvd.nist.gov"
   2058             || row.path != producer.nvd_endpoint
   2059             || row.query.len() != producer.nvd_query_keys.len()
   2060             || !row
   2061                 .query
   2062                 .iter()
   2063                 .map(|query| query.name.as_str())
   2064                 .eq(producer.nvd_query_keys.iter().map(String::as_str))
   2065             || row.query.iter().any(|query| {
   2066                 !valid_bounded_text(&query.value, 128) || query.name.eq_ignore_ascii_case("apiKey")
   2067             })
   2068             || start > end
   2069             || row.started_at_epoch < update.started_at_epoch
   2070             || previous_request_completed.is_some_and(|completed| completed > row.started_at_epoch)
   2071             || row.completed_at_epoch < row.started_at_epoch
   2072             || row.completed_at_epoch > update.completed_at_epoch
   2073             || row.response_status != 200
   2074             || row.response_byte_length == 0
   2075             || row.response_byte_length > MAX_NVD_RESPONSE_BYTES
   2076             || !valid_hex(&row.response_sha256, 64)
   2077             || row.response_start_index != start_index
   2078             || row.response_results_per_page != results_per_page
   2079             || row.response_total_results == 0
   2080             || start_index >= row.response_total_results
   2081         {
   2082             return Err(AdvisoryError::new(AdvisoryFailureKind::InvalidSnapshot));
   2083         }
   2084         if let Some((previous_start, previous_end, previous_index, previous_page, previous_total)) =
   2085             previous_window
   2086         {
   2087             if start == previous_start && end == previous_end {
   2088                 if start_index != previous_index.saturating_add(previous_page)
   2089                     || row.response_total_results != previous_total
   2090                 {
   2091                     return Err(AdvisoryError::new(AdvisoryFailureKind::InvalidSnapshot));
   2092                 }
   2093             } else if start != previous_end.saturating_add(1) || start_index != 0 {
   2094                 return Err(AdvisoryError::new(AdvisoryFailureKind::InvalidSnapshot));
   2095             }
   2096         } else if start_index != 0 {
   2097             return Err(AdvisoryError::new(AdvisoryFailureKind::InvalidSnapshot));
   2098         }
   2099         let page_complete = start_index
   2100             .checked_add(results_per_page)
   2101             .is_some_and(|next| next >= row.response_total_results);
   2102         let next_same_window = trace.request.get(index + 1).is_some_and(|next| {
   2103             let next_values = next
   2104                 .query
   2105                 .iter()
   2106                 .map(|query| (query.name.as_str(), query.value.as_str()))
   2107                 .collect::<BTreeMap<_, _>>();
   2108             next_values.get("lastModStartDate") == values.get("lastModStartDate")
   2109                 && next_values.get("lastModEndDate") == values.get("lastModEndDate")
   2110         });
   2111         if page_complete == next_same_window {
   2112             return Err(AdvisoryError::new(AdvisoryFailureKind::InvalidSnapshot));
   2113         }
   2114         previous_request_completed = Some(row.completed_at_epoch);
   2115         previous_window = Some((
   2116             start,
   2117             end,
   2118             start_index,
   2119             results_per_page,
   2120             row.response_total_results,
   2121         ));
   2122     }
   2123     Ok(())
   2124 }
   2125 
   2126 fn parse_canonical_u64(value: &str) -> Option<u64> {
   2127     if value.is_empty()
   2128         || !value.bytes().all(|byte| byte.is_ascii_digit())
   2129         || (value.len() > 1 && value.starts_with('0'))
   2130     {
   2131         return None;
   2132     }
   2133     value.parse().ok()
   2134 }
   2135 
   2136 fn expected_process_receipt_ids() -> Vec<String> {
   2137     let mut ids = vec!["owasp_nvd_update".to_owned()];
   2138     ids.extend(
   2139         WORKLOADS
   2140             .iter()
   2141             .filter(|workload| workload.package_manager == "gradle")
   2142             .map(|workload| format!("gradle:{}", workload.id)),
   2143     );
   2144     ids.extend(
   2145         WORKLOADS
   2146             .iter()
   2147             .filter(|workload| workload.package_manager == "cargo")
   2148             .map(|workload| format!("cargo_audit:{}", workload.id)),
   2149     );
   2150     ids.extend(
   2151         WORKLOADS
   2152             .iter()
   2153             .filter(|workload| workload.package_manager == "gradle")
   2154             .map(|workload| format!("owasp_analysis:{}", workload.id)),
   2155     );
   2156     ids
   2157 }
   2158 
   2159 fn validate_process_receipts(
   2160     receipts: &[TrustedProcessReceipt],
   2161     evaluation_epoch: u64,
   2162 ) -> Result<(), AdvisoryError> {
   2163     if !receipts
   2164         .iter()
   2165         .map(|receipt| receipt.id.as_str())
   2166         .eq(expected_process_receipt_ids().iter().map(String::as_str))
   2167     {
   2168         return Err(AdvisoryError::new(AdvisoryFailureKind::InventoryMismatch));
   2169     }
   2170     let mut previous_completed = None;
   2171     let mut working_directory_identities = BTreeSet::new();
   2172     let mut private_environment_values = BTreeSet::new();
   2173     let mut shared_environment_values = BTreeMap::<String, String>::new();
   2174     for receipt in receipts {
   2175         require_complete(receipt.state)?;
   2176         validate_process_environment(
   2177             &receipt.id,
   2178             &receipt.environment,
   2179             &mut private_environment_values,
   2180             &mut shared_environment_values,
   2181         )?;
   2182         if !valid_exact_identity_token(&receipt.id, 256)
   2183             || !valid_hex(&receipt.program_sha256, 64)
   2184             || receipt.runtime_sha256 != expected_runtime_sha256(&receipt.id)
   2185             || !valid_hex(&receipt.arguments_sha256, 64)
   2186             || receipt.environment_sha256 != process_environment_digest(&receipt.environment)?
   2187             || validate_process_working_directory(&receipt.id, &receipt.working_directory).is_err()
   2188             || !working_directory_identities
   2189                 .insert(receipt.working_directory.identity_sha256.clone())
   2190             || receipt.working_directory_sha256
   2191                 != process_working_directory_digest(&receipt.working_directory)?
   2192             || receipt
   2193                 .path_binding
   2194                 .windows(2)
   2195                 .any(|rows| rows[0].argument_index >= rows[1].argument_index)
   2196             || receipt.path_binding.iter().any(|binding| {
   2197                 !valid_exact_identity_token(&binding.logical_role, 128)
   2198                     || !valid_hex(&binding.identity_sha256, 64)
   2199             })
   2200             || !receipt.stdin_closed
   2201             || receipt.deadline_seconds != ANALYSIS_DEADLINE_SECONDS
   2202             || receipt.started_at_epoch == 0
   2203             || receipt.completed_at_epoch < receipt.started_at_epoch
   2204             || previous_completed.is_some_and(|previous| previous > receipt.started_at_epoch)
   2205             || receipt.completed_at_epoch > evaluation_epoch
   2206             || receipt.stdout_byte_length > 67_108_864
   2207             || receipt.stderr_byte_length > 67_108_864
   2208             || !valid_hex(&receipt.stdout_sha256, 64)
   2209             || !valid_hex(&receipt.stderr_sha256, 64)
   2210             || !valid_hex(&receipt.input_sha256, 64)
   2211             || !valid_hex(&receipt.output_sha256, 64)
   2212         {
   2213             return Err(AdvisoryError::new(AdvisoryFailureKind::InvalidSnapshot));
   2214         }
   2215         previous_completed = Some(receipt.completed_at_epoch);
   2216     }
   2217     if private_environment_values
   2218         .iter()
   2219         .any(|value| working_directory_identities.contains(value))
   2220     {
   2221         return Err(AdvisoryError::new(AdvisoryFailureKind::InvalidSnapshot));
   2222     }
   2223     Ok(())
   2224 }
   2225 
   2226 fn expected_process_environment(id: &str) -> Result<Vec<ProcessEnvironmentRow>, AdvisoryError> {
   2227     expected_environment_names(id)
   2228         .map(|name| {
   2229             let logical_value = expected_environment_logical_value(name)?;
   2230             let synthetic_value = match name {
   2231                 "LC_ALL" => "C".to_owned(),
   2232                 "TZ" => "UTC".to_owned(),
   2233                 "JAVA_HOME" => "/governed/jdk-21.0.12.1".to_owned(),
   2234                 "PATH" => "/governed/tool-manifest/bin".to_owned(),
   2235                 _ => format!("/private/{id}/{name}"),
   2236             };
   2237             Ok(ProcessEnvironmentRow {
   2238                 name: name.to_owned(),
   2239                 logical_value: logical_value.to_owned(),
   2240                 value_sha256: sha256(synthetic_value.as_bytes()),
   2241             })
   2242         })
   2243         .collect()
   2244 }
   2245 
   2246 fn expected_environment_logical_value(name: &str) -> Result<&'static str, AdvisoryError> {
   2247     match name {
   2248         "CARGO_HOME" => Ok("private_empty_cargo_home"),
   2249         "GRADLE_USER_HOME" => Ok("private_seeded_gradle_user_home"),
   2250         "HOME" => Ok("private_empty_home"),
   2251         "JAVA_HOME" => Ok("pinned_java_21_0_12_1_home"),
   2252         "LC_ALL" => Ok("C"),
   2253         "PATH" => Ok("pinned_tool_manifest_only_path"),
   2254         "TMPDIR" => Ok("private_owner_only_attempt_tmp"),
   2255         "TZ" => Ok("UTC"),
   2256         _ => Err(AdvisoryError::new(AdvisoryFailureKind::InvalidContract)),
   2257     }
   2258 }
   2259 
   2260 fn validate_process_environment(
   2261     id: &str,
   2262     environment: &[ProcessEnvironmentRow],
   2263     private_values: &mut BTreeSet<String>,
   2264     shared_values: &mut BTreeMap<String, String>,
   2265 ) -> Result<(), AdvisoryError> {
   2266     let expected_names = expected_environment_names(id).collect::<Vec<_>>();
   2267     if environment.len() != expected_names.len() {
   2268         return Err(AdvisoryError::new(AdvisoryFailureKind::InvalidSnapshot));
   2269     }
   2270     for (row, expected_name) in environment.iter().zip(expected_names) {
   2271         let expected_logical_value = expected_environment_logical_value(expected_name)?;
   2272         if row.name != expected_name
   2273             || row.logical_value != expected_logical_value
   2274             || !valid_hex(&row.value_sha256, 64)
   2275             || match expected_name {
   2276                 "LC_ALL" => row.value_sha256 != sha256(b"C"),
   2277                 "TZ" => row.value_sha256 != sha256(b"UTC"),
   2278                 "JAVA_HOME" | "PATH" => {
   2279                     shared_values
   2280                         .entry(expected_name.to_owned())
   2281                         .or_insert_with(|| row.value_sha256.clone())
   2282                         != &row.value_sha256
   2283                 }
   2284                 _ => !private_values.insert(row.value_sha256.clone()),
   2285             }
   2286         {
   2287             return Err(AdvisoryError::new(AdvisoryFailureKind::InvalidSnapshot));
   2288         }
   2289     }
   2290     Ok(())
   2291 }
   2292 
   2293 fn expected_process_working_directory(id: &str) -> Result<ProcessWorkingDirectory, AdvisoryError> {
   2294     let (logical_uri, kind, pre_entries, post_entries) =
   2295         if let Some(workload) = id.strip_prefix("cargo_audit:") {
   2296             (
   2297                 format!("rshr-private-cargo-audit://{workload}"),
   2298                 "fresh_empty_config_free",
   2299                 0,
   2300                 0,
   2301             )
   2302         } else if let Some(workload) = id.strip_prefix("gradle:") {
   2303             (
   2304                 format!("rshr-candidate-gradle-build://{workload}"),
   2305                 "retained_candidate_source",
   2306                 1,
   2307                 1,
   2308             )
   2309         } else if let Some(workload) = id.strip_prefix("owasp_analysis:") {
   2310             (
   2311                 format!("rshr-private-owasp-analysis://{workload}"),
   2312                 "fresh_private_analysis",
   2313                 0,
   2314                 0,
   2315             )
   2316         } else if id == "owasp_nvd_update" {
   2317             (
   2318                 "rshr-private-owasp-nvd-update://attempt".to_owned(),
   2319                 "fresh_private_update",
   2320                 0,
   2321                 1,
   2322             )
   2323         } else {
   2324             return Err(AdvisoryError::new(AdvisoryFailureKind::InvalidContract));
   2325         };
   2326     let empty_tree = empty_directory_tree_sha256()?;
   2327     let nonempty_tree = domain_json_digest(
   2328         b"radroots-advisory-synthetic-working-directory-tree-v1\0",
   2329         &serde_json::json!({"kind": kind, "process_id": id}),
   2330     )?;
   2331     Ok(ProcessWorkingDirectory {
   2332         identity_sha256: domain_json_digest(
   2333             b"radroots-advisory-synthetic-retained-directory-identity-v1\0",
   2334             &serde_json::json!({"kind": kind, "logical_uri": logical_uri, "process_id": id}),
   2335         )?,
   2336         logical_uri,
   2337         kind: kind.to_owned(),
   2338         pre_execution_entry_count: pre_entries,
   2339         pre_execution_tree_sha256: if pre_entries == 0 {
   2340             empty_tree.clone()
   2341         } else {
   2342             nonempty_tree.clone()
   2343         },
   2344         post_execution_entry_count: post_entries,
   2345         post_execution_tree_sha256: if post_entries == 0 {
   2346             empty_tree
   2347         } else {
   2348             nonempty_tree
   2349         },
   2350     })
   2351 }
   2352 
   2353 fn validate_process_working_directory(
   2354     id: &str,
   2355     directory: &ProcessWorkingDirectory,
   2356 ) -> Result<(), AdvisoryError> {
   2357     let expected = expected_process_working_directory(id)?;
   2358     if directory.logical_uri != expected.logical_uri
   2359         || directory.kind != expected.kind
   2360         || !valid_hex(&directory.identity_sha256, 64)
   2361         || !valid_hex(&directory.pre_execution_tree_sha256, 64)
   2362         || !valid_hex(&directory.post_execution_tree_sha256, 64)
   2363         || match directory.kind.as_str() {
   2364             "fresh_empty_config_free" | "fresh_private_analysis" => {
   2365                 directory.pre_execution_entry_count != 0
   2366                     || directory.post_execution_entry_count != 0
   2367                     || directory.pre_execution_tree_sha256 != empty_directory_tree_sha256()?
   2368                     || directory.post_execution_tree_sha256 != empty_directory_tree_sha256()?
   2369             }
   2370             "retained_candidate_source" => {
   2371                 directory.pre_execution_entry_count == 0
   2372                     || directory.pre_execution_entry_count != directory.post_execution_entry_count
   2373                     || directory.pre_execution_tree_sha256 != directory.post_execution_tree_sha256
   2374             }
   2375             "fresh_private_update" => {
   2376                 directory.pre_execution_entry_count != 0
   2377                     || directory.post_execution_entry_count == 0
   2378                     || directory.pre_execution_tree_sha256 != empty_directory_tree_sha256()?
   2379             }
   2380             _ => true,
   2381         }
   2382     {
   2383         return Err(AdvisoryError::new(AdvisoryFailureKind::InvalidSnapshot));
   2384     }
   2385     Ok(())
   2386 }
   2387 
   2388 fn empty_directory_tree_sha256() -> Result<String, AdvisoryError> {
   2389     domain_json_digest(
   2390         b"radroots-advisory-empty-directory-tree-v1\0",
   2391         &Vec::<Value>::new(),
   2392     )
   2393 }
   2394 
   2395 fn expected_environment_names(id: &str) -> impl Iterator<Item = &'static str> {
   2396     let names: &'static [&'static str] = if id.starts_with("cargo_audit:") {
   2397         &["CARGO_HOME", "HOME", "LC_ALL", "PATH", "TMPDIR", "TZ"]
   2398     } else if id.starts_with("gradle:") {
   2399         &[
   2400             "GRADLE_USER_HOME",
   2401             "HOME",
   2402             "JAVA_HOME",
   2403             "LC_ALL",
   2404             "PATH",
   2405             "TMPDIR",
   2406             "TZ",
   2407         ]
   2408     } else {
   2409         &["HOME", "JAVA_HOME", "LC_ALL", "PATH", "TMPDIR", "TZ"]
   2410     };
   2411     names.iter().copied()
   2412 }
   2413 
   2414 fn expected_runtime_sha256(id: &str) -> Vec<String> {
   2415     let values = if id.starts_with("cargo_audit:") {
   2416         vec![TOOL_PINS[0].executable_sha256]
   2417     } else if id.starts_with("gradle:") {
   2418         vec![
   2419             TOOL_PINS[3].executable_sha256,
   2420             TOOL_PINS[2].executable_sha256,
   2421         ]
   2422     } else {
   2423         vec![
   2424             TOOL_PINS[1].executable_sha256,
   2425             TOOL_PINS[2].executable_sha256,
   2426         ]
   2427     };
   2428     values.into_iter().map(str::to_owned).collect()
   2429 }
   2430 
   2431 fn process_environment_digest(
   2432     environment: &[ProcessEnvironmentRow],
   2433 ) -> Result<String, AdvisoryError> {
   2434     domain_json_digest(b"radroots-advisory-process-environment-v1\0", environment)
   2435 }
   2436 
   2437 fn process_working_directory_digest(
   2438     working_directory: &ProcessWorkingDirectory,
   2439 ) -> Result<String, AdvisoryError> {
   2440     domain_json_digest(
   2441         b"radroots-advisory-process-working-directory-v1\0",
   2442         working_directory,
   2443     )
   2444 }
   2445 
   2446 fn process_receipt<'a>(
   2447     receipts: &'a [TrustedProcessReceipt],
   2448     id: &str,
   2449 ) -> Result<&'a TrustedProcessReceipt, AdvisoryError> {
   2450     receipts
   2451         .iter()
   2452         .find(|receipt| receipt.id == id)
   2453         .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InventoryMismatch))
   2454 }
   2455 
   2456 fn process_receipt_digest(receipt: &TrustedProcessReceipt) -> Result<String, AdvisoryError> {
   2457     domain_json_digest(b"radroots-advisory-bounded-process-receipt-v1\0", receipt)
   2458 }
   2459 
   2460 fn parse_canonical_authority<T>(bytes: &[u8]) -> Result<T, AdvisoryError>
   2461 where
   2462     T: for<'de> Deserialize<'de>,
   2463 {
   2464     let value: Value = serde_json::from_slice(bytes)
   2465         .map_err(|_| AdvisoryError::new(AdvisoryFailureKind::InvalidSnapshot))?;
   2466     if canonical_json(&value) != bytes {
   2467         return Err(AdvisoryError::new(AdvisoryFailureKind::InvalidSnapshot));
   2468     }
   2469     serde_json::from_value(value)
   2470         .map_err(|_| AdvisoryError::new(AdvisoryFailureKind::InvalidSnapshot))
   2471 }
   2472 
   2473 fn validate_tool_acquisitions(actual: &[ToolAcquisition]) -> Result<(), AdvisoryError> {
   2474     if actual.len() != TOOL_PINS.len() {
   2475         return Err(AdvisoryError::new(AdvisoryFailureKind::InventoryMismatch));
   2476     }
   2477     for (tool, pin) in actual.iter().zip(TOOL_PINS) {
   2478         if tool.id != pin.id
   2479             || sha256(&canonical_json_without_lf(&tool.projection)) != pin.projection_sha256
   2480         {
   2481             return Err(AdvisoryError::new(AdvisoryFailureKind::InvalidContract));
   2482         }
   2483     }
   2484     Ok(())
   2485 }
   2486 
   2487 fn validate_tool_states(actual: &[ToolState], evaluation_epoch: u64) -> Result<(), AdvisoryError> {
   2488     if actual.len() != TOOL_PINS.len() {
   2489         return Err(AdvisoryError::new(AdvisoryFailureKind::InventoryMismatch));
   2490     }
   2491     for (state, pin) in actual.iter().zip(TOOL_PINS) {
   2492         if state.id != pin.id
   2493             || state.normalized_version != pin.version
   2494             || state.executable_sha256 != pin.executable_sha256
   2495             || state.source_sha256 != pin.source_sha256
   2496             || state.package_receipt_sha256 != pin.receipt_sha256
   2497             || state.reviewed_at_epoch == 0
   2498         {
   2499             return Err(AdvisoryError::new(AdvisoryFailureKind::InventoryMismatch));
   2500         }
   2501         require_complete(state.state)?;
   2502         let age = evaluation_epoch
   2503             .checked_sub(state.reviewed_at_epoch)
   2504             .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::StaleSnapshot))?;
   2505         if age > TOOL_REVIEW_SECONDS {
   2506             return Err(AdvisoryError::new(AdvisoryFailureKind::StaleSnapshot));
   2507         }
   2508     }
   2509     Ok(())
   2510 }
   2511 
   2512 fn validate_gradle_projections(
   2513     projections: &[GradleProjection],
   2514     request: &AdmissionRequest,
   2515     receipts: &[TrustedProcessReceipt],
   2516 ) -> Result<(), AdvisoryError> {
   2517     if sha256(GRADLE_INIT_SCRIPT_BYTES) != GRADLE_INIT_SCRIPT_SHA256 {
   2518         return Err(AdvisoryError::new(AdvisoryFailureKind::InvalidContract));
   2519     }
   2520     let expected = WORKLOADS
   2521         .iter()
   2522         .filter(|workload| workload.package_manager == "gradle")
   2523         .collect::<Vec<_>>();
   2524     if projections.len() != expected.len() {
   2525         return Err(AdvisoryError::new(AdvisoryFailureKind::InventoryMismatch));
   2526     }
   2527     let harvest_source = request
   2528         .sources
   2529         .iter()
   2530         .find(|source| source.repository == "oss/harvestcircle")
   2531         .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InventoryMismatch))?;
   2532     for ((projection, authority), admitted) in projections
   2533         .iter()
   2534         .zip(expected)
   2535         .zip(&request.admitted_gradle_graphs)
   2536     {
   2537         let inventory = request
   2538             .inventory
   2539             .iter()
   2540             .find(|row| row.id == authority.id)
   2541             .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InventoryMismatch))?;
   2542         require_complete(projection.state)?;
   2543         let receipt = process_receipt(receipts, &format!("gradle:{}", authority.id))?;
   2544         admitted.revalidate()?;
   2545         if !admitted.matches_projection(projection) {
   2546             return Err(AdvisoryError::new(AdvisoryFailureKind::InventoryMismatch));
   2547         }
   2548         validate_gradle_projection_bindings(
   2549             projection,
   2550             authority,
   2551             inventory,
   2552             harvest_source,
   2553             request,
   2554             receipt,
   2555         )?;
   2556         validate_gradle_graph_structure(projection, authority, &harvest_source.revision)?;
   2557         admitted.revalidate()?;
   2558     }
   2559     Ok(())
   2560 }
   2561 
   2562 fn validate_gradle_projection_bindings(
   2563     projection: &GradleProjection,
   2564     authority: &WorkloadAuthority,
   2565     inventory: &WorkloadInventory,
   2566     harvest_source: &SourceIdentity,
   2567     request: &AdmissionRequest,
   2568     receipt: &TrustedProcessReceipt,
   2569 ) -> Result<(), AdvisoryError> {
   2570     if projection.workload_id != authority.id
   2571         || projection.raw_graph_byte_length == 0
   2572         || projection.raw_graph_byte_length > MAX_GRADLE_GRAPH_BYTES
   2573         || !valid_hex(&projection.raw_graph_sha256, 64)
   2574         || projection.init_script_sha256 != GRADLE_INIT_SCRIPT_SHA256
   2575         || projection.wrapper_arguments != expected_gradle_arguments(authority)?
   2576         || projection.environment_keys
   2577             != [
   2578                 "GRADLE_USER_HOME",
   2579                 "HOME",
   2580                 "JAVA_HOME",
   2581                 "LC_ALL",
   2582                 "PATH",
   2583                 "TMPDIR",
   2584                 "TZ",
   2585             ]
   2586         || !valid_hex(&projection.environment_sha256, 64)
   2587         || projection.exit_code != 0
   2588         || projection.source_revision != harvest_source.revision
   2589         || projection.source_tree != harvest_source.tree
   2590         || projection.input_sha256
   2591             != gradle_candidate_input_digest(request, inventory, harvest_source, projection)?
   2592         || projection.dependency_count != inventory.dependency_count
   2593         || projection.component_count != projection.components.len() as u64
   2594         || projection.edge_count != projection.edges.len() as u64
   2595         || projection.artifact_count != projection.artifacts.len() as u64
   2596         || projection.component_count == 0
   2597         || !valid_hex(&projection.materialized_tree_sha256, 64)
   2598         || !valid_hex(&projection.artifact_source_roots_sha256, 64)
   2599         || !valid_hex(&projection.seed_cache_inventory_sha256, 64)
   2600         || projection.wrapper_distribution_sha256
   2601             != "553c78f50dafcd54d65b9a444649057857469edf836431389695608536d6b746"
   2602         || projection.process_receipt_sha256 != process_receipt_digest(receipt)?
   2603         || projection.canonical_graph_sha256 != gradle_graph_digest(projection)?
   2604         || projection.normalization_receipt_sha256
   2605             != gradle_normalization_receipt_digest(
   2606                 &projection.workload_id,
   2607                 projection.raw_graph_byte_length,
   2608                 &projection.raw_graph_sha256,
   2609                 &projection.canonical_graph_sha256,
   2610                 &projection.materialized_tree_sha256,
   2611                 &projection.artifact_source_roots_sha256,
   2612             )?
   2613         || receipt.program_sha256 != TOOL_PINS[3].executable_sha256
   2614         || receipt.arguments_sha256
   2615             != domain_json_digest(
   2616                 b"radroots-advisory-process-arguments-v1\0",
   2617                 &projection.wrapper_arguments,
   2618             )?
   2619         || receipt.environment_sha256 != projection.environment_sha256
   2620         || receipt.input_sha256 != gradle_process_input_digest(projection)?
   2621         || receipt.output_sha256 != gradle_process_output_digest(projection)?
   2622         || receipt.path_binding
   2623             != vec![
   2624                 ProcessPathBinding {
   2625                     argument_index: 5,
   2626                     logical_role: "gradle_build_root".to_owned(),
   2627                     identity_sha256: gradle_build_root_binding_digest(projection)?,
   2628                 },
   2629                 ProcessPathBinding {
   2630                     argument_index: 7,
   2631                     logical_role: "gradle_init_script".to_owned(),
   2632                     identity_sha256: projection.init_script_sha256.clone(),
   2633                 },
   2634                 ProcessPathBinding {
   2635                     argument_index: 10,
   2636                     logical_role: "gradle_projection_output".to_owned(),
   2637                     identity_sha256: projection.raw_graph_sha256.clone(),
   2638                 },
   2639             ]
   2640         || receipt.exit_code != projection.exit_code
   2641     {
   2642         return Err(AdvisoryError::new(AdvisoryFailureKind::InventoryMismatch));
   2643     }
   2644     Ok(())
   2645 }
   2646 
   2647 fn validate_gradle_graph_structure(
   2648     projection: &GradleProjection,
   2649     authority: &WorkloadAuthority,
   2650     source_revision: &str,
   2651 ) -> Result<(), AdvisoryError> {
   2652     let mut component_ids = BTreeSet::new();
   2653     let mut component_by_id = BTreeMap::new();
   2654     let mut previous_component = None::<&str>;
   2655     for component in &projection.components {
   2656         if previous_component.is_some_and(|previous| previous >= component.identity_sha256.as_str())
   2657             || component.identity_sha256 != gradle_component_digest(component)?
   2658             || component.variant_sha256 != gradle_variant_digest(&component.variant)?
   2659             || !valid_gradle_variant_envelope(&component.variant)
   2660             || !valid_gradle_component(component)
   2661             || !component_ids.insert(component.identity_sha256.as_str())
   2662         {
   2663             return Err(AdvisoryError::new(AdvisoryFailureKind::InventoryMismatch));
   2664         }
   2665         component_by_id.insert(component.identity_sha256.as_str(), component);
   2666         previous_component = Some(&component.identity_sha256);
   2667     }
   2668     let roots = projection
   2669         .components
   2670         .iter()
   2671         .filter(|component| {
   2672             component.root
   2673                 && component.kind == "project"
   2674                 && component.build_root.as_deref() == Some(authority.build_root)
   2675                 && component.project_path.as_deref() == authority.project_path
   2676         })
   2677         .collect::<Vec<_>>();
   2678     if roots.len() != 1 {
   2679         return Err(AdvisoryError::new(AdvisoryFailureKind::InventoryMismatch));
   2680     }
   2681     let mut previous_edge = None::<Vec<u8>>;
   2682     for edge in &projection.edges {
   2683         let key = canonical_row_key(edge)?;
   2684         if previous_edge
   2685             .as_ref()
   2686             .is_some_and(|previous| previous >= &key)
   2687             || !component_ids.contains(edge.from_identity_sha256.as_str())
   2688             || !component_ids.contains(edge.to_identity_sha256.as_str())
   2689             || edge.from_identity_sha256 == edge.to_identity_sha256
   2690             || edge.requested_sha256 != gradle_request_digest(&edge.requested)?
   2691             || edge.selected_variant_sha256 != gradle_variant_digest(&edge.selected_variant)?
   2692             || !valid_gradle_request(&edge.requested)
   2693             || !valid_gradle_variant(&edge.selected_variant)
   2694         {
   2695             return Err(AdvisoryError::new(AdvisoryFailureKind::InventoryMismatch));
   2696         }
   2697         previous_edge = Some(key);
   2698     }
   2699     let mut reachable = BTreeSet::from([roots[0].identity_sha256.as_str()]);
   2700     loop {
   2701         let before = reachable.len();
   2702         for edge in &projection.edges {
   2703             if reachable.contains(edge.from_identity_sha256.as_str()) {
   2704                 reachable.insert(edge.to_identity_sha256.as_str());
   2705             }
   2706         }
   2707         if reachable.len() == before {
   2708             break;
   2709         }
   2710     }
   2711     if reachable.len() != projection.components.len() {
   2712         return Err(AdvisoryError::new(AdvisoryFailureKind::InventoryMismatch));
   2713     }
   2714     let mut previous = None::<Vec<u8>>;
   2715     let mut materialized = BTreeMap::<(&str, &str), (&str, u64)>::new();
   2716     for artifact in &projection.artifacts {
   2717         let key = canonical_row_key(artifact)?;
   2718         if previous.as_ref().is_some_and(|prior| prior >= &key)
   2719             || !component_ids.contains(artifact.component_identity_sha256.as_str())
   2720             || !valid_bounded_text(&artifact.component, 512)
   2721             || artifact.package_ecosystem != "maven"
   2722             || !valid_bounded_text(&artifact.package_namespace, 256)
   2723             || !valid_bounded_text(&artifact.package_name, 256)
   2724             || !valid_bounded_text(&artifact.package_version, 128)
   2725             || !valid_hex(&artifact.artifact_sha256, 64)
   2726             || artifact.variant_sha256 != gradle_variant_digest(&artifact.variant)?
   2727             || !valid_gradle_variant(&artifact.variant)
   2728             || artifact.byte_length == 0
   2729             || !valid_bounded_text(&artifact.artifact_name, 256)
   2730             || !valid_bounded_text(&artifact.logical_name, 256)
   2731             || !valid_bounded_text(&artifact.artifact_type, 64)
   2732             || artifact
   2733                 .classifier
   2734                 .as_deref()
   2735                 .is_some_and(|classifier| !valid_bounded_text(classifier, 128))
   2736             || !valid_artifact_extension(&artifact.extension)
   2737             || artifact.materialized_name
   2738                 != format!("{}.{}", artifact.artifact_sha256, artifact.extension)
   2739         {
   2740             return Err(AdvisoryError::new(AdvisoryFailureKind::InventoryMismatch));
   2741         }
   2742         let component = component_by_id
   2743             .get(artifact.component_identity_sha256.as_str())
   2744             .copied()
   2745             .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InventoryMismatch))?;
   2746         if !artifact_matches_component(artifact, component, source_revision)
   2747             || !projection.edges.iter().any(|edge| {
   2748                 edge.to_identity_sha256 == artifact.component_identity_sha256
   2749                     && edge.selected_variant_sha256 == artifact.variant_sha256
   2750             })
   2751         {
   2752             return Err(AdvisoryError::new(AdvisoryFailureKind::InventoryMismatch));
   2753         }
   2754         let materialized_key = (
   2755             artifact.artifact_sha256.as_str(),
   2756             artifact.extension.as_str(),
   2757         );
   2758         if let Some((name, length)) = materialized.insert(
   2759             materialized_key,
   2760             (artifact.materialized_name.as_str(), artifact.byte_length),
   2761         ) && (name != artifact.materialized_name || length != artifact.byte_length)
   2762         {
   2763             return Err(AdvisoryError::new(AdvisoryFailureKind::InventoryMismatch));
   2764         }
   2765         previous = Some(key);
   2766     }
   2767     Ok(())
   2768 }
   2769 
   2770 fn expected_gradle_arguments(workload: &WorkloadAuthority) -> Result<Vec<String>, AdvisoryError> {
   2771     let project = workload
   2772         .project_path
   2773         .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InvalidContract))?;
   2774     let configuration = workload
   2775         .configuration
   2776         .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InvalidContract))?;
   2777     Ok([
   2778         "--offline".to_owned(),
   2779         "--no-daemon".to_owned(),
   2780         "--no-parallel".to_owned(),
   2781         "--no-configuration-cache".to_owned(),
   2782         "--project-dir".to_owned(),
   2783         workload.build_root.to_owned(),
   2784         "--init-script".to_owned(),
   2785         "{private_evidenced_init_script}".to_owned(),
   2786         format!("{project}:rshrAdvisoryGraph"),
   2787         format!("-PrshrAdvisoryConfiguration={configuration}"),
   2788         "-PrshrAdvisoryOutput={private_create_new_projection}".to_owned(),
   2789     ]
   2790     .into_iter()
   2791     .collect())
   2792 }
   2793 
   2794 fn gradle_graph_digest(projection: &GradleProjection) -> Result<String, AdvisoryError> {
   2795     gradle_graph_digest_parts(
   2796         &projection.workload_id,
   2797         &projection.components,
   2798         &projection.edges,
   2799         &projection.artifacts,
   2800     )
   2801 }
   2802 
   2803 fn gradle_graph_digest_parts(
   2804     workload_id: &str,
   2805     components: &[GradleComponent],
   2806     edges: &[GradleEdge],
   2807     artifacts: &[GradleArtifact],
   2808 ) -> Result<String, AdvisoryError> {
   2809     domain_json_digest(
   2810         b"radroots-advisory-gradle-graph-v1\0",
   2811         &serde_json::json!({
   2812             "artifacts": artifacts,
   2813             "components": components,
   2814             "edges": edges,
   2815             "workload_id": workload_id,
   2816         }),
   2817     )
   2818 }
   2819 
   2820 fn canonical_row_key<T: Serialize>(value: &T) -> Result<Vec<u8>, AdvisoryError> {
   2821     let value = serde_json::to_value(value)
   2822         .map_err(|_| AdvisoryError::new(AdvisoryFailureKind::InvalidSnapshot))?;
   2823     Ok(canonical_json_without_lf(&value))
   2824 }
   2825 
   2826 fn gradle_process_input_digest(projection: &GradleProjection) -> Result<String, AdvisoryError> {
   2827     domain_json_digest(
   2828         b"radroots-advisory-gradle-process-input-v1\0",
   2829         &serde_json::json!({
   2830             "artifact_source_roots_sha256": projection.artifact_source_roots_sha256,
   2831             "init_script_sha256": projection.init_script_sha256,
   2832             "input_sha256": projection.input_sha256,
   2833             "seed_cache_inventory_sha256": projection.seed_cache_inventory_sha256,
   2834             "source_revision": projection.source_revision,
   2835             "source_tree": projection.source_tree,
   2836             "wrapper_distribution_sha256": projection.wrapper_distribution_sha256,
   2837             "workload_id": projection.workload_id,
   2838         }),
   2839     )
   2840 }
   2841 
   2842 fn gradle_build_root_binding_digest(
   2843     projection: &GradleProjection,
   2844 ) -> Result<String, AdvisoryError> {
   2845     domain_json_digest(
   2846         b"radroots-advisory-gradle-build-root-binding-v1\0",
   2847         &serde_json::json!({
   2848             "source_revision": projection.source_revision,
   2849             "source_tree": projection.source_tree,
   2850             "workload_id": projection.workload_id,
   2851         }),
   2852     )
   2853 }
   2854 
   2855 fn gradle_candidate_input_digest(
   2856     request: &AdmissionRequest,
   2857     inventory: &WorkloadInventory,
   2858     source: &SourceIdentity,
   2859     projection: &GradleProjection,
   2860 ) -> Result<String, AdvisoryError> {
   2861     domain_json_digest(
   2862         b"radroots-advisory-gradle-candidate-input-v1\0",
   2863         &serde_json::json!({
   2864             "artifact_source_roots_sha256": projection.artifact_source_roots_sha256,
   2865             "candidate": request.candidate,
   2866             "dependency_control_sha256": inventory.input_sha256,
   2867             "fresh_tool_observation_sha256": sha256(&request.fresh_tool_observation),
   2868             "init_script_sha256": projection.init_script_sha256,
   2869             "seed_cache_inventory_sha256": projection.seed_cache_inventory_sha256,
   2870             "source": source,
   2871             "step_297_tool_manifest_sha256": sha256(&request.step_297_tool_manifest),
   2872             "workload": inventory,
   2873             "wrapper_distribution_sha256": projection.wrapper_distribution_sha256,
   2874         }),
   2875     )
   2876 }
   2877 
   2878 fn gradle_process_output_digest(projection: &GradleProjection) -> Result<String, AdvisoryError> {
   2879     domain_json_digest(
   2880         b"radroots-advisory-gradle-process-output-v1\0",
   2881         &serde_json::json!({
   2882             "raw_graph_byte_length": projection.raw_graph_byte_length,
   2883             "raw_graph_sha256": projection.raw_graph_sha256,
   2884             "workload_id": projection.workload_id,
   2885         }),
   2886     )
   2887 }
   2888 
   2889 fn gradle_normalization_receipt_digest(
   2890     workload_id: &str,
   2891     raw_graph_byte_length: u64,
   2892     raw_graph_sha256: &str,
   2893     canonical_graph_sha256: &str,
   2894     materialized_tree_sha256: &str,
   2895     artifact_source_roots_sha256: &str,
   2896 ) -> Result<String, AdvisoryError> {
   2897     domain_json_digest(
   2898         b"radroots-advisory-gradle-normalization-v1\0",
   2899         &serde_json::json!({
   2900             "artifact_source_roots_sha256": artifact_source_roots_sha256,
   2901             "canonical_graph_sha256": canonical_graph_sha256,
   2902             "materialized_tree_sha256": materialized_tree_sha256,
   2903             "raw_graph_byte_length": raw_graph_byte_length,
   2904             "raw_graph_sha256": raw_graph_sha256,
   2905             "workload_id": workload_id,
   2906         }),
   2907     )
   2908 }
   2909 
   2910 fn gradle_component_digest(component: &GradleComponent) -> Result<String, AdvisoryError> {
   2911     domain_json_digest(
   2912         b"radroots-advisory-gradle-component-v1\0",
   2913         &serde_json::json!({
   2914             "build_root": component.build_root,
   2915             "group": component.group,
   2916             "kind": component.kind,
   2917             "name": component.name,
   2918             "project_path": component.project_path,
   2919             "root": component.root,
   2920             "variant": component.variant,
   2921             "version": component.version,
   2922         }),
   2923     )
   2924 }
   2925 
   2926 fn gradle_variant_digest(variant: &Value) -> Result<String, AdvisoryError> {
   2927     domain_json_digest(b"radroots-advisory-gradle-variant-v1\0", variant)
   2928 }
   2929 
   2930 fn gradle_request_digest(request: &Value) -> Result<String, AdvisoryError> {
   2931     domain_json_digest(b"radroots-advisory-gradle-request-v1\0", request)
   2932 }
   2933 
   2934 fn valid_gradle_variant(value: &Value) -> bool {
   2935     serde_json::from_value::<RawGradleVariant>(value.clone())
   2936         .ok()
   2937         .is_some_and(|variant| validate_raw_variant(&variant, 0).is_ok())
   2938 }
   2939 
   2940 fn valid_gradle_variant_envelope(value: &Value) -> bool {
   2941     serde_json::from_value::<RawGradleVariantEnvelope>(value.clone())
   2942         .ok()
   2943         .is_some_and(|envelope| validate_raw_variant_envelope(&envelope).is_ok())
   2944 }
   2945 
   2946 fn valid_gradle_request(value: &Value) -> bool {
   2947     serde_json::from_value::<RawGradleSelector>(value.clone())
   2948         .ok()
   2949         .is_some_and(|selector| validate_raw_selector(&selector).is_ok())
   2950 }
   2951 
   2952 fn validate_raw_variant_envelope(envelope: &RawGradleVariantEnvelope) -> Result<(), AdvisoryError> {
   2953     if envelope.selected.is_empty() || envelope.selected.len() > MAX_GRADLE_VARIANTS {
   2954         return Err(AdvisoryError::new(AdvisoryFailureKind::InventoryMismatch));
   2955     }
   2956     require_strict_canonical_order(&envelope.selected)?;
   2957     for variant in &envelope.selected {
   2958         validate_raw_variant(variant, 0)?;
   2959     }
   2960     Ok(())
   2961 }
   2962 
   2963 fn validate_raw_variant(variant: &RawGradleVariant, depth: usize) -> Result<(), AdvisoryError> {
   2964     if depth > MAX_GRADLE_EXTERNAL_VARIANT_DEPTH
   2965         || variant.attributes.len() > MAX_GRADLE_ATTRIBUTES
   2966         || variant.capabilities.len() > MAX_GRADLE_CAPABILITIES
   2967     {
   2968         return Err(AdvisoryError::new(AdvisoryFailureKind::InventoryMismatch));
   2969     }
   2970     require_strict_canonical_order(&variant.attributes)?;
   2971     require_strict_canonical_order(&variant.capabilities)?;
   2972     let mut attribute_names = BTreeSet::new();
   2973     for attribute in &variant.attributes {
   2974         if !valid_ascii_text(&attribute.name, 512, false)
   2975             || !valid_ascii_text(&attribute.value, 512, true)
   2976             || !attribute_names.insert(attribute.name.as_str())
   2977         {
   2978             return Err(AdvisoryError::new(AdvisoryFailureKind::InventoryMismatch));
   2979         }
   2980     }
   2981     for capability in &variant.capabilities {
   2982         if !valid_ascii_text(&capability.group, 256, true)
   2983             || !valid_ascii_text(&capability.name, 256, false)
   2984             || capability
   2985                 .version
   2986                 .as_deref()
   2987                 .is_some_and(|value| !valid_ascii_text(value, 128, true))
   2988         {
   2989             return Err(AdvisoryError::new(AdvisoryFailureKind::InventoryMismatch));
   2990         }
   2991     }
   2992     if let Some(external) = &variant.external_variant {
   2993         validate_raw_variant(external, depth.saturating_add(1))?;
   2994     }
   2995     Ok(())
   2996 }
   2997 
   2998 fn validate_raw_selector(selector: &RawGradleSelector) -> Result<(), AdvisoryError> {
   2999     if selector.attributes.len() > MAX_GRADLE_ATTRIBUTES
   3000         || selector.capabilities.len() > MAX_GRADLE_CAPABILITIES
   3001     {
   3002         return Err(AdvisoryError::new(AdvisoryFailureKind::InventoryMismatch));
   3003     }
   3004     require_strict_canonical_order(&selector.attributes)?;
   3005     require_strict_canonical_order(&selector.capabilities)?;
   3006     let variant = RawGradleVariant {
   3007         attributes: selector.attributes.clone(),
   3008         capabilities: selector.capabilities.clone(),
   3009         external_variant: None,
   3010     };
   3011     validate_raw_variant(&variant, 0)?;
   3012     match selector.kind.as_str() {
   3013         "module" => {
   3014             if selector
   3015                 .group
   3016                 .as_deref()
   3017                 .is_none_or(|value| !valid_ascii_text(value, 256, false))
   3018                 || selector
   3019                     .name
   3020                     .as_deref()
   3021                     .is_none_or(|value| !valid_ascii_text(value, 256, false))
   3022                 || selector
   3023                     .version
   3024                     .as_deref()
   3025                     .is_none_or(|value| !valid_ascii_text(value, 512, true))
   3026                 || selector.build_root.is_some()
   3027                 || selector.project_path.is_some()
   3028             {
   3029                 return Err(AdvisoryError::new(AdvisoryFailureKind::InventoryMismatch));
   3030             }
   3031             let constraint = selector
   3032                 .version_constraint
   3033                 .as_ref()
   3034                 .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InventoryMismatch))?;
   3035             if constraint.rejected.len() > MAX_GRADLE_REJECTED_VERSIONS
   3036                 || constraint
   3037                     .branch
   3038                     .as_deref()
   3039                     .is_some_and(|value| !valid_ascii_text(value, 256, true))
   3040                 || !valid_ascii_text(&constraint.preferred, 512, true)
   3041                 || !valid_ascii_text(&constraint.required, 512, true)
   3042                 || !valid_ascii_text(&constraint.strict, 512, true)
   3043                 || constraint
   3044                     .rejected
   3045                     .iter()
   3046                     .any(|value| !valid_ascii_text(value, 512, false))
   3047                 || constraint
   3048                     .rejected
   3049                     .windows(2)
   3050                     .any(|window| window[0] >= window[1])
   3051             {
   3052                 return Err(AdvisoryError::new(AdvisoryFailureKind::InventoryMismatch));
   3053             }
   3054         }
   3055         "project" => {
   3056             if selector.group.is_some()
   3057                 || selector.name.is_some()
   3058                 || selector.version.is_some()
   3059                 || selector.version_constraint.is_some()
   3060                 || selector
   3061                     .build_root
   3062                     .as_deref()
   3063                     .is_none_or(|value| !valid_gradle_build_root(value))
   3064                 || selector
   3065                     .project_path
   3066                     .as_deref()
   3067                     .is_none_or(|value| !valid_gradle_project_path(value))
   3068             {
   3069                 return Err(AdvisoryError::new(AdvisoryFailureKind::InventoryMismatch));
   3070             }
   3071         }
   3072         _ => {
   3073             return Err(AdvisoryError::new(AdvisoryFailureKind::InventoryMismatch));
   3074         }
   3075     }
   3076     Ok(())
   3077 }
   3078 
   3079 fn validate_raw_component_core(core: &RawGradleComponentCore) -> Result<(), AdvisoryError> {
   3080     match core.kind.as_str() {
   3081         "module"
   3082             if core
   3083                 .group
   3084                 .as_deref()
   3085                 .is_some_and(|value| valid_ascii_text(value, 256, false))
   3086                 && core
   3087                     .name
   3088                     .as_deref()
   3089                     .is_some_and(|value| valid_ascii_text(value, 256, false))
   3090                 && core
   3091                     .version
   3092                     .as_deref()
   3093                     .is_some_and(|value| valid_ascii_text(value, 128, false))
   3094                 && core.build_root.is_none()
   3095                 && core.project_path.is_none() =>
   3096         {
   3097             Ok(())
   3098         }
   3099         "project"
   3100             if core.group.is_none()
   3101                 && core.name.is_none()
   3102                 && core.version.is_none()
   3103                 && core
   3104                     .build_root
   3105                     .as_deref()
   3106                     .is_some_and(valid_gradle_build_root)
   3107                 && core
   3108                     .project_path
   3109                     .as_deref()
   3110                     .is_some_and(valid_gradle_project_path) =>
   3111         {
   3112             Ok(())
   3113         }
   3114         _ => Err(AdvisoryError::new(AdvisoryFailureKind::InventoryMismatch)),
   3115     }
   3116 }
   3117 
   3118 fn require_strict_canonical_order<T: Serialize>(values: &[T]) -> Result<(), AdvisoryError> {
   3119     let mut previous = None::<Vec<u8>>;
   3120     for value in values {
   3121         let key = canonical_row_key(value)?;
   3122         if previous.as_ref().is_some_and(|prior| prior >= &key) {
   3123             return Err(AdvisoryError::new(AdvisoryFailureKind::InventoryMismatch));
   3124         }
   3125         previous = Some(key);
   3126     }
   3127     Ok(())
   3128 }
   3129 
   3130 fn valid_ascii_text(value: &str, maximum: usize, allow_empty: bool) -> bool {
   3131     (allow_empty || !value.is_empty())
   3132         && value.len() <= maximum
   3133         && value
   3134             .bytes()
   3135             .all(|byte| byte.is_ascii() && !byte.is_ascii_control())
   3136 }
   3137 
   3138 fn valid_gradle_build_root(value: &str) -> bool {
   3139     matches!(value, "." | "build-logic")
   3140 }
   3141 
   3142 fn valid_gradle_project_path(value: &str) -> bool {
   3143     valid_ascii_text(value, 256, false)
   3144         && value.starts_with(':')
   3145         && !value.ends_with(':')
   3146         && value
   3147             .split(':')
   3148             .skip(1)
   3149             .all(|component| !component.is_empty() && valid_ascii_text(component, 128, false))
   3150 }
   3151 
   3152 fn artifact_matches_component(
   3153     artifact: &GradleArtifact,
   3154     component: &GradleComponent,
   3155     _source_revision: &str,
   3156 ) -> bool {
   3157     match component.kind.as_str() {
   3158         "module" => {
   3159             let (Some(group), Some(name), Some(version)) = (
   3160                 component.group.as_deref(),
   3161                 component.name.as_deref(),
   3162                 component.version.as_deref(),
   3163             ) else {
   3164                 return false;
   3165             };
   3166             artifact.component == format!("{group}:{name}:{version}")
   3167                 && artifact.package_ecosystem == "maven"
   3168                 && artifact.package_namespace == group
   3169                 && artifact.package_name == name
   3170                 && artifact.package_version == version
   3171         }
   3172         "project" => {
   3173             let (Some(root), Some(project)) = (
   3174                 component.build_root.as_deref(),
   3175                 component.project_path.as_deref(),
   3176             ) else {
   3177                 return false;
   3178             };
   3179             artifact.component == format!("{root}:{project}")
   3180                 && artifact.package_ecosystem == "maven"
   3181                 && valid_project_package_identity(
   3182                     root,
   3183                     project,
   3184                     &artifact.package_namespace,
   3185                     &artifact.package_name,
   3186                     &artifact.package_version,
   3187                 )
   3188                 && artifact.classifier.is_none()
   3189         }
   3190         _ => false,
   3191     }
   3192 }
   3193 
   3194 fn valid_project_module_version(
   3195     build_root: &str,
   3196     project_path: &str,
   3197     module_version: &RawGradleModuleVersion,
   3198     source_revision: &str,
   3199 ) -> bool {
   3200     valid_hex(source_revision, 40)
   3201         && valid_project_package_identity(
   3202             build_root,
   3203             project_path,
   3204             &module_version.group,
   3205             &module_version.name,
   3206             &module_version.version,
   3207         )
   3208 }
   3209 
   3210 fn valid_project_package_identity(
   3211     build_root: &str,
   3212     project_path: &str,
   3213     group: &str,
   3214     name: &str,
   3215     version: &str,
   3216 ) -> bool {
   3217     version == "unspecified"
   3218         && matches!(
   3219             (build_root, project_path, group, name),
   3220             (
   3221                 ".",
   3222                 ":app:design_system",
   3223                 "harvestcircle.app",
   3224                 "design_system"
   3225             ) | (".", ":app:shared", "harvestcircle.app", "shared")
   3226                 | (
   3227                     "build-logic",
   3228                     ":contracts",
   3229                     "harvestcircle-build-logic",
   3230                     "contracts"
   3231                 )
   3232         )
   3233 }
   3234 
   3235 fn valid_gradle_component(component: &GradleComponent) -> bool {
   3236     match component.kind.as_str() {
   3237         "module" => {
   3238             component
   3239                 .group
   3240                 .as_deref()
   3241                 .is_some_and(|value| valid_bounded_text(value, 256))
   3242                 && component
   3243                     .name
   3244                     .as_deref()
   3245                     .is_some_and(|value| valid_bounded_text(value, 256))
   3246                 && component
   3247                     .version
   3248                     .as_deref()
   3249                     .is_some_and(|value| valid_bounded_text(value, 128))
   3250                 && component.build_root.is_none()
   3251                 && component.project_path.is_none()
   3252                 && !component.root
   3253         }
   3254         "project" => {
   3255             component.group.is_none()
   3256                 && component.name.is_none()
   3257                 && component.version.is_none()
   3258                 && component
   3259                     .build_root
   3260                     .as_deref()
   3261                     .is_some_and(|value| value == "." || valid_identifier(value))
   3262                 && component
   3263                     .project_path
   3264                     .as_deref()
   3265                     .is_some_and(|value| value.starts_with(':') && valid_bounded_text(value, 256))
   3266         }
   3267         _ => false,
   3268     }
   3269 }
   3270 
   3271 fn valid_artifact_extension(extension: &str) -> bool {
   3272     matches!(
   3273         extension,
   3274         "aar" | "jar" | "js" | "klib" | "module" | "pom" | "wasm" | "zip"
   3275     )
   3276 }
   3277 
   3278 fn validate_provider(
   3279     provider: &ProviderSnapshot,
   3280     request: &AdmissionRequest,
   3281     trace: &NvdNetworkTrace,
   3282     receipts: &[TrustedProcessReceipt],
   3283 ) -> Result<(), AdvisoryError> {
   3284     require_complete(provider.acquisition_state)?;
   3285     require_complete(provider.analysis_state)?;
   3286     if provider.acquisition_count != 1
   3287         || provider.bounded_deadline_seconds != ANALYSIS_DEADLINE_SECONDS
   3288         || !valid_hex(&provider.network_trace_sha256, 64)
   3289         || !valid_hex(&provider.producer_request_sha256, 64)
   3290         || provider.producer_request_sha256 != sha256(&request.producer_request)
   3291         || !valid_hex(&provider.database_identity_sha256, 64)
   3292         || provider.archive_format
   3293             != "deterministic_tar_gzip_exact_bytes_normalized_headers_sorted_members"
   3294         || provider.archive_expanded_bytes == 0
   3295         || provider.archive_member_count == 0
   3296         || provider.archive_payload_bytes == 0
   3297         || provider.acquired_at_epoch == 0
   3298         || provider.digest_time_epoch == 0
   3299         || provider.analyzed_at_epoch == 0
   3300         || provider.acquired_at_epoch > provider.digest_time_epoch
   3301         || provider.digest_time_epoch > provider.analyzed_at_epoch
   3302         || provider.analyzed_at_epoch > request.evaluation_epoch
   3303     {
   3304         return Err(AdvisoryError::new(AdvisoryFailureKind::InvalidSnapshot));
   3305     }
   3306     let maximum_age = match request.freshness {
   3307         FreshnessMode::Development => DEVELOPMENT_REUSE_SECONDS,
   3308         FreshnessMode::Qualification => QUALIFICATION_FRESHNESS_SECONDS,
   3309     };
   3310     let age = request
   3311         .evaluation_epoch
   3312         .checked_sub(provider.digest_time_epoch)
   3313         .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::StaleSnapshot))?;
   3314     if age > maximum_age {
   3315         return Err(AdvisoryError::new(AdvisoryFailureKind::StaleSnapshot));
   3316     }
   3317     let (kind, network, archive, report, acquisition_arguments, environment, expected_arguments) =
   3318         match provider.provider {
   3319             ProviderId::Rustsec => (
   3320                 "externally_admitted_immutable_snapshot",
   3321                 "external_snapshot_admission",
   3322                 RUSTSEC_ARCHIVE_NAME,
   3323                 RUSTSEC_REPORT_NAME,
   3324                 &[] as &[&str],
   3325                 "fresh_private_config_free_workdir_home_and_cargo_home",
   3326                 CARGO_AUDIT_ARGUMENTS.as_slice(),
   3327             ),
   3328             ProviderId::OwaspNvd => (
   3329                 "bounded_nvd_update_only",
   3330                 "contracted_fetch_only",
   3331                 NVD_ARCHIVE_NAME,
   3332                 OWASP_REPORT_NAME,
   3333                 OWASP_UPDATE_ARGUMENTS.as_slice(),
   3334                 "replacement_allowlist_private_data_and_output",
   3335                 OWASP_OFFLINE_ARGUMENTS.as_slice(),
   3336             ),
   3337         };
   3338     if provider.acquisition_kind != kind
   3339         || provider.network_mode != network
   3340         || provider.archive.path != archive
   3341         || provider.report.path != report
   3342         || provider.acquisition_arguments
   3343             != acquisition_arguments
   3344                 .iter()
   3345                 .map(|argument| (*argument).to_owned())
   3346                 .collect::<Vec<_>>()
   3347         || provider.analysis_environment != environment
   3348         || provider.analysis_arguments
   3349             != expected_arguments
   3350                 .iter()
   3351                 .map(|argument| (*argument).to_owned())
   3352                 .collect::<Vec<_>>()
   3353         || !valid_provider_blob_binding(provider.provider, &provider.archive, true)
   3354         || !valid_provider_blob_binding(provider.provider, &provider.report, false)
   3355     {
   3356         return Err(AdvisoryError::new(AdvisoryFailureKind::InvalidSnapshot));
   3357     }
   3358     match provider.provider {
   3359         ProviderId::Rustsec => {
   3360             if provider.network_trace_sha256 != "0".repeat(64) {
   3361                 return Err(AdvisoryError::new(AdvisoryFailureKind::InvalidSnapshot));
   3362             }
   3363         }
   3364         ProviderId::OwaspNvd => {
   3365             let receipt = process_receipt(receipts, "owasp_nvd_update")?;
   3366             if provider.network_trace_sha256 != sha256(&request.nvd_network_trace)
   3367                 || provider.producer_request_sha256 != trace.producer_request_sha256
   3368                 || receipt.program_sha256 != TOOL_PINS[1].executable_sha256
   3369                 || receipt.arguments_sha256
   3370                     != domain_json_digest(
   3371                         b"radroots-advisory-process-arguments-v1\0",
   3372                         &provider.acquisition_arguments,
   3373                     )?
   3374                 || receipt.output_sha256
   3375                     != nvd_update_output_digest(
   3376                         &provider.materialized_tree_sha256,
   3377                         &provider.network_trace_sha256,
   3378                     )?
   3379                 || receipt.input_sha256 != sha256(&request.producer_request)
   3380                 || receipt.path_binding
   3381                     != vec![ProcessPathBinding {
   3382                         argument_index: 2,
   3383                         logical_role: "nvd_update_data".to_owned(),
   3384                         identity_sha256: provider.materialized_tree_sha256.clone(),
   3385                     }]
   3386                 || receipt.completed_at_epoch != provider.acquired_at_epoch
   3387                 || receipt.exit_code != 0
   3388             {
   3389                 return Err(AdvisoryError::new(AdvisoryFailureKind::BindingChanged));
   3390             }
   3391         }
   3392     }
   3393     Ok(())
   3394 }
   3395 
   3396 fn require_complete(state: OperationState) -> Result<(), AdvisoryError> {
   3397     match state {
   3398         OperationState::Complete => Ok(()),
   3399         OperationState::TimedOut => Err(AdvisoryError::new(AdvisoryFailureKind::TimedOut)),
   3400         OperationState::Unavailable => Err(AdvisoryError::new(AdvisoryFailureKind::Unavailable)),
   3401         OperationState::Failed => Err(AdvisoryError::new(AdvisoryFailureKind::InvalidSnapshot)),
   3402     }
   3403 }
   3404 
   3405 fn validate_temporal_order(
   3406     observation: &TrustedToolObservation,
   3407     providers: &[ProviderSnapshot],
   3408     receipts: &[TrustedProcessReceipt],
   3409 ) -> Result<(), AdvisoryError> {
   3410     if observation.observed_at_epoch == 0
   3411         || observation
   3412             .tool_state
   3413             .iter()
   3414             .any(|state| state.reviewed_at_epoch > observation.observed_at_epoch)
   3415         || providers
   3416             .iter()
   3417             .any(|provider| provider.acquired_at_epoch < observation.observed_at_epoch)
   3418     {
   3419         return Err(AdvisoryError::new(AdvisoryFailureKind::InvalidSnapshot));
   3420     }
   3421     for provider in providers {
   3422         let prefix = match provider.provider {
   3423             ProviderId::Rustsec => "cargo_audit:",
   3424             ProviderId::OwaspNvd => "owasp_analysis:",
   3425         };
   3426         let scans = receipts
   3427             .iter()
   3428             .filter(|receipt| receipt.id.starts_with(prefix))
   3429             .collect::<Vec<_>>();
   3430         if scans.is_empty()
   3431             || scans
   3432                 .iter()
   3433                 .any(|receipt| receipt.started_at_epoch < provider.digest_time_epoch)
   3434             || scans.iter().map(|receipt| receipt.completed_at_epoch).max()
   3435                 != Some(provider.analyzed_at_epoch)
   3436         {
   3437             return Err(AdvisoryError::new(AdvisoryFailureKind::InvalidSnapshot));
   3438         }
   3439         if provider.provider == ProviderId::OwaspNvd {
   3440             let update = process_receipt(receipts, "owasp_nvd_update")?;
   3441             if update.started_at_epoch < observation.observed_at_epoch
   3442                 || update.completed_at_epoch != provider.acquired_at_epoch
   3443             {
   3444                 return Err(AdvisoryError::new(AdvisoryFailureKind::InvalidSnapshot));
   3445             }
   3446             for scan in scans {
   3447                 let workload = scan
   3448                     .id
   3449                     .strip_prefix("owasp_analysis:")
   3450                     .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InvalidSnapshot))?;
   3451                 let gradle = process_receipt(receipts, &format!("gradle:{workload}"))?;
   3452                 if gradle.completed_at_epoch > scan.started_at_epoch {
   3453                     return Err(AdvisoryError::new(AdvisoryFailureKind::InvalidSnapshot));
   3454                 }
   3455             }
   3456         }
   3457     }
   3458     Ok(())
   3459 }
   3460 
   3461 fn parse_unsuppressed_report(
   3462     snapshot: &ProviderSnapshot,
   3463     bytes: &[u8],
   3464     request: &AdmissionRequest,
   3465     gradle_projections: &[GradleProjection],
   3466 ) -> Result<Vec<Finding>, AdvisoryError> {
   3467     let (_, _, _, provider_evidence) = validate_trusted_authority(request)?;
   3468     let value: Value = serde_json::from_slice(bytes)
   3469         .map_err(|_| AdvisoryError::new(AdvisoryFailureKind::InvalidReport))?;
   3470     if canonical_json(&value) != bytes {
   3471         return Err(AdvisoryError::new(AdvisoryFailureKind::InvalidReport));
   3472     }
   3473     let report: ReportEnvelope = serde_json::from_value(value)
   3474         .map_err(|_| AdvisoryError::new(AdvisoryFailureKind::InvalidReport))?;
   3475     if report.schema != "radroots.advisory-unsuppressed-report.v1"
   3476         || report.provider != snapshot.provider
   3477     {
   3478         return Err(AdvisoryError::new(AdvisoryFailureKind::InvalidReport));
   3479     }
   3480     let expected_ids = WORKLOADS
   3481         .iter()
   3482         .filter(|workload| match snapshot.provider {
   3483             ProviderId::Rustsec => workload.package_manager == "cargo",
   3484             ProviderId::OwaspNvd => workload.package_manager == "gradle",
   3485         })
   3486         .map(|workload| workload.id)
   3487         .collect::<Vec<_>>();
   3488     if report.workload_result.len() != expected_ids.len()
   3489         || !report
   3490             .workload_result
   3491             .iter()
   3492             .map(|result| result.workload_id.as_str())
   3493             .eq(expected_ids)
   3494     {
   3495         return Err(AdvisoryError::new(AdvisoryFailureKind::InventoryMismatch));
   3496     }
   3497     let mut findings = Vec::new();
   3498     let mut database_copy_roots = BTreeSet::new();
   3499     for result in report.workload_result {
   3500         let inventory = request
   3501             .inventory
   3502             .iter()
   3503             .find(|inventory| inventory.id == result.workload_id)
   3504             .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InventoryMismatch))?;
   3505         validate_workload_execution(snapshot, &result, inventory, request)?;
   3506         let admitted_output = request
   3507             .admitted_scanner_outputs
   3508             .iter()
   3509             .find(|output| {
   3510                 output.provider == snapshot.provider && output.workload_id == result.workload_id
   3511             })
   3512             .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InventoryMismatch))?;
   3513         admitted_output.revalidate()?;
   3514         if admitted_output.bytes.as_slice() != result.raw_scanner_output.as_bytes()
   3515             || admitted_output.evidence.byte_length != result.raw_output_byte_length
   3516             || admitted_output.evidence.sha256 != result.raw_output_sha256
   3517         {
   3518             return Err(AdvisoryError::new(AdvisoryFailureKind::BindingChanged));
   3519         }
   3520         if let Some(copy) = &result.database_copy
   3521             && !database_copy_roots.insert(copy.root_identity_sha256.clone())
   3522         {
   3523             return Err(AdvisoryError::new(AdvisoryFailureKind::BindingChanged));
   3524         }
   3525         let raw: Value = serde_json::from_slice(&admitted_output.bytes)
   3526             .map_err(|_| AdvisoryError::new(AdvisoryFailureKind::InvalidReport))?;
   3527         match snapshot.provider {
   3528             ProviderId::Rustsec => parse_rustsec_output(
   3529                 &result.workload_id,
   3530                 &raw,
   3531                 inventory.dependency_count,
   3532                 &snapshot.database_identity_sha256,
   3533                 &snapshot.materialized_tree_sha256,
   3534                 snapshot.digest_time_epoch,
   3535                 &mut findings,
   3536             )?,
   3537             ProviderId::OwaspNvd => {
   3538                 let projection = gradle_projections
   3539                     .iter()
   3540                     .find(|projection| projection.workload_id == result.workload_id)
   3541                     .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InventoryMismatch))?;
   3542                 parse_owasp_output(
   3543                     &result.workload_id,
   3544                     &raw,
   3545                     projection,
   3546                     &snapshot.database_identity_sha256,
   3547                     &snapshot.materialized_tree_sha256,
   3548                     result
   3549                         .arguments
   3550                         .get(4)
   3551                         .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InvalidReport))?,
   3552                     process_receipt(
   3553                         &provider_evidence.process_receipt,
   3554                         &format!("owasp_analysis:{}", result.workload_id),
   3555                     )?
   3556                     .started_at_epoch,
   3557                     process_receipt(
   3558                         &provider_evidence.process_receipt,
   3559                         &format!("owasp_analysis:{}", result.workload_id),
   3560                     )?
   3561                     .completed_at_epoch,
   3562                     snapshot.digest_time_epoch,
   3563                     &mut findings,
   3564                 )?
   3565             }
   3566         }
   3567         admitted_output.revalidate()?;
   3568     }
   3569     findings.sort_by(|left, right| finding_key(left).cmp(&finding_key(right)));
   3570     if findings.windows(2).any(|window| window[0] == window[1]) {
   3571         return Err(AdvisoryError::new(AdvisoryFailureKind::InvalidReport));
   3572     }
   3573     Ok(findings)
   3574 }
   3575 
   3576 fn validate_workload_execution(
   3577     snapshot: &ProviderSnapshot,
   3578     result: &WorkloadResult,
   3579     inventory: &WorkloadInventory,
   3580     request: &AdmissionRequest,
   3581 ) -> Result<(), AdvisoryError> {
   3582     let raw = result.raw_scanner_output.as_bytes();
   3583     let (tool_manifest, observation, _, provider_evidence) = validate_trusted_authority(request)?;
   3584     let expected_tool_observation =
   3585         tool_observation_digest(&tool_manifest.tool_acquisition, &observation.tool_state)?;
   3586     let receipt_id = match snapshot.provider {
   3587         ProviderId::Rustsec => format!("cargo_audit:{}", result.workload_id),
   3588         ProviderId::OwaspNvd => format!("owasp_analysis:{}", result.workload_id),
   3589     };
   3590     let receipt = process_receipt(&provider_evidence.process_receipt, &receipt_id)?;
   3591     let program_sha256 = match snapshot.provider {
   3592         ProviderId::Rustsec => TOOL_PINS[0].executable_sha256,
   3593         ProviderId::OwaspNvd => TOOL_PINS[1].executable_sha256,
   3594     };
   3595     let database_copy_valid = match (snapshot.provider, result.database_copy.as_ref()) {
   3596         (ProviderId::Rustsec, None) => true,
   3597         (ProviderId::OwaspNvd, Some(copy)) => {
   3598             valid_hex(&copy.root_identity_sha256, 64)
   3599                 && copy.source_tree_sha256 == snapshot.materialized_tree_sha256
   3600                 && copy.pre_scan_tree_sha256 == snapshot.materialized_tree_sha256
   3601                 && copy.post_scan_tree_sha256 == snapshot.materialized_tree_sha256
   3602         }
   3603         _ => false,
   3604     };
   3605     if raw.is_empty()
   3606         || raw.len() as u64 > MAX_RAW_WORKLOAD_REPORT_BYTES
   3607         || result.input_sha256 != inventory.input_sha256
   3608         || result.dependency_count != inventory.dependency_count
   3609         || result.provider_archive_sha256 != snapshot.archive.sha256
   3610         || result.materialized_tree_sha256 != snapshot.materialized_tree_sha256
   3611         || result.tool_observation_sha256 != expected_tool_observation
   3612         || result.raw_output_byte_length != raw.len() as u64
   3613         || result.raw_output_sha256 != sha256(raw)
   3614         || !valid_hex(&result.environment_sha256, 64)
   3615         || result.process_receipt_sha256 != process_receipt_digest(receipt)?
   3616         || !database_copy_valid
   3617         || receipt.program_sha256 != program_sha256
   3618         || receipt.arguments_sha256
   3619             != domain_json_digest(
   3620                 b"radroots-advisory-process-arguments-v1\0",
   3621                 &result.arguments,
   3622             )?
   3623         || receipt.environment_sha256 != result.environment_sha256
   3624         || receipt.input_sha256
   3625             != scanner_process_input_digest(snapshot.provider, result, &expected_tool_observation)?
   3626         || receipt.output_sha256 != result.raw_output_sha256
   3627         || match snapshot.provider {
   3628             ProviderId::Rustsec => {
   3629                 receipt.stdout_byte_length != result.raw_output_byte_length
   3630                     || receipt.stdout_sha256 != result.raw_output_sha256
   3631             }
   3632             ProviderId::OwaspNvd => {
   3633                 receipt.stdout_byte_length != 0 || receipt.stdout_sha256 != sha256(&[])
   3634             }
   3635         }
   3636         || receipt.exit_code != result.exit_code
   3637         || receipt.completed_at_epoch > snapshot.analyzed_at_epoch
   3638         || receipt.path_binding
   3639             != scanner_path_bindings(snapshot.provider, result, inventory, request)?
   3640         || !valid_scanner_arguments(snapshot.provider, &result.arguments)
   3641         || match snapshot.provider {
   3642             ProviderId::Rustsec => !matches!(result.exit_code, 0 | 1),
   3643             ProviderId::OwaspNvd => result.exit_code != 0,
   3644         }
   3645     {
   3646         return Err(AdvisoryError::new(AdvisoryFailureKind::InvalidReport));
   3647     }
   3648     Ok(())
   3649 }
   3650 
   3651 fn scanner_path_bindings(
   3652     provider: ProviderId,
   3653     result: &WorkloadResult,
   3654     inventory: &WorkloadInventory,
   3655     request: &AdmissionRequest,
   3656 ) -> Result<Vec<ProcessPathBinding>, AdvisoryError> {
   3657     match provider {
   3658         ProviderId::Rustsec => Ok(vec![
   3659             ProcessPathBinding {
   3660                 argument_index: 2,
   3661                 logical_role: "rustsec_database".to_owned(),
   3662                 identity_sha256: result.materialized_tree_sha256.clone(),
   3663             },
   3664             ProcessPathBinding {
   3665                 argument_index: 6,
   3666                 logical_role: "cargo_lock".to_owned(),
   3667                 identity_sha256: inventory.input_sha256.clone(),
   3668             },
   3669         ]),
   3670         ProviderId::OwaspNvd => {
   3671             let projection = validate_projection_for_workload(&result.workload_id, request)?;
   3672             let database_copy = result
   3673                 .database_copy
   3674                 .as_ref()
   3675                 .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InvalidReport))?;
   3676             Ok(vec![
   3677                 ProcessPathBinding {
   3678                     argument_index: 2,
   3679                     logical_role: "nvd_database".to_owned(),
   3680                     identity_sha256: scanner_database_copy_digest(database_copy)?,
   3681                 },
   3682                 ProcessPathBinding {
   3683                     argument_index: 4,
   3684                     logical_role: "gradle_scan_projection".to_owned(),
   3685                     identity_sha256: projection.materialized_tree_sha256.clone(),
   3686                 },
   3687                 ProcessPathBinding {
   3688                     argument_index: 10,
   3689                     logical_role: "raw_report_output".to_owned(),
   3690                     identity_sha256: result.raw_output_sha256.clone(),
   3691                 },
   3692             ])
   3693         }
   3694     }
   3695 }
   3696 
   3697 fn scanner_database_copy_digest(copy: &ScannerDatabaseCopy) -> Result<String, AdvisoryError> {
   3698     domain_json_digest(b"radroots-advisory-scanner-database-copy-v1\0", copy)
   3699 }
   3700 
   3701 fn validate_projection_for_workload(
   3702     workload_id: &str,
   3703     request: &AdmissionRequest,
   3704 ) -> Result<GradleProjection, AdvisoryError> {
   3705     let (_, _, _, evidence) = validate_trusted_authority(request)?;
   3706     evidence
   3707         .gradle_projection
   3708         .iter()
   3709         .find(|projection| projection.workload_id == workload_id)
   3710         .cloned()
   3711         .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InventoryMismatch))
   3712 }
   3713 
   3714 fn nvd_update_output_digest(
   3715     materialized_tree_sha256: &str,
   3716     network_trace_sha256: &str,
   3717 ) -> Result<String, AdvisoryError> {
   3718     domain_json_digest(
   3719         b"radroots-advisory-nvd-update-output-v1\0",
   3720         &serde_json::json!({
   3721             "materialized_tree_sha256": materialized_tree_sha256,
   3722             "network_trace_sha256": network_trace_sha256,
   3723         }),
   3724     )
   3725 }
   3726 
   3727 fn scanner_process_input_digest(
   3728     provider: ProviderId,
   3729     result: &WorkloadResult,
   3730     tool_observation_sha256: &str,
   3731 ) -> Result<String, AdvisoryError> {
   3732     scanner_process_input_digest_fields(
   3733         provider,
   3734         &result.workload_id,
   3735         &result.input_sha256,
   3736         result.dependency_count,
   3737         &result.provider_archive_sha256,
   3738         &result.materialized_tree_sha256,
   3739         &result
   3740             .database_copy
   3741             .as_ref()
   3742             .map(scanner_database_copy_digest)
   3743             .transpose()?
   3744             .unwrap_or_else(|| "none".to_owned()),
   3745         tool_observation_sha256,
   3746     )
   3747 }
   3748 
   3749 #[allow(clippy::too_many_arguments)]
   3750 fn scanner_process_input_digest_fields(
   3751     provider: ProviderId,
   3752     workload_id: &str,
   3753     input_sha256: &str,
   3754     dependency_count: u64,
   3755     provider_archive_sha256: &str,
   3756     materialized_tree_sha256: &str,
   3757     database_copy_sha256: &str,
   3758     tool_observation_sha256: &str,
   3759 ) -> Result<String, AdvisoryError> {
   3760     domain_json_digest(
   3761         b"radroots-advisory-scanner-input-v1\0",
   3762         &serde_json::json!({
   3763             "dependency_count": dependency_count,
   3764             "database_copy_sha256": database_copy_sha256,
   3765             "input_sha256": input_sha256,
   3766             "materialized_tree_sha256": materialized_tree_sha256,
   3767             "provider": provider,
   3768             "provider_archive_sha256": provider_archive_sha256,
   3769             "tool_observation_sha256": tool_observation_sha256,
   3770             "workload_id": workload_id,
   3771         }),
   3772     )
   3773 }
   3774 
   3775 fn valid_scanner_arguments(provider: ProviderId, arguments: &[String]) -> bool {
   3776     match provider {
   3777         ProviderId::Rustsec => {
   3778             arguments.len() == CARGO_AUDIT_ARGUMENTS.len()
   3779                 && arguments[0] == "audit"
   3780                 && arguments[1] == "--db"
   3781                 && absolute_path(&arguments[2])
   3782                 && arguments[3..6] == ["--no-fetch", "--json", "--file"]
   3783                 && absolute_path(&arguments[6])
   3784                 && !arguments
   3785                     .iter()
   3786                     .any(|argument| matches!(argument.as_str(), "--ignore" | "--stale"))
   3787         }
   3788         ProviderId::OwaspNvd => {
   3789             if arguments.len() != OWASP_OFFLINE_ARGUMENTS.len()
   3790                 || arguments[0] != "--noupdate"
   3791                 || arguments[1] != "--data"
   3792                 || !absolute_path(&arguments[2])
   3793                 || arguments[3] != "--scan"
   3794                 || !absolute_path(&arguments[4])
   3795                 || arguments[5] != "--project"
   3796                 || !WORKLOADS.iter().any(|workload| {
   3797                     workload.package_manager == "gradle" && workload.id == arguments[6]
   3798                 })
   3799                 || arguments[7] != "--format"
   3800                 || arguments[8] != "JSON"
   3801                 || arguments[9] != "--out"
   3802                 || !absolute_path(&arguments[10])
   3803             {
   3804                 return false;
   3805             }
   3806             let required = &OWASP_OFFLINE_ARGUMENTS[11..];
   3807             arguments[11..]
   3808                 == required
   3809                     .iter()
   3810                     .map(|argument| (*argument).to_owned())
   3811                     .collect::<Vec<_>>()
   3812                 && !arguments.iter().any(|argument| {
   3813                     argument == "--suppression" || argument.starts_with("--failOnCVSS")
   3814                 })
   3815         }
   3816     }
   3817 }
   3818 
   3819 fn absolute_path(value: &str) -> bool {
   3820     !value.is_empty() && Path::new(value).is_absolute() && !value.contains('\0')
   3821 }
   3822 
   3823 fn parse_rustsec_output(
   3824     workload_id: &str,
   3825     output: &Value,
   3826     expected_dependency_count: u64,
   3827     expected_database_identity: &str,
   3828     materialized_tree_sha256: &str,
   3829     database_digest_time_epoch: u64,
   3830     findings: &mut Vec<Finding>,
   3831 ) -> Result<(), AdvisoryError> {
   3832     validate_json_bounds(output)?;
   3833     let object = output
   3834         .as_object()
   3835         .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InvalidReport))?;
   3836     let expected_keys = [
   3837         "database",
   3838         "lockfile",
   3839         "settings",
   3840         "vulnerabilities",
   3841         "warnings",
   3842     ]
   3843     .into_iter()
   3844     .collect::<BTreeSet<_>>();
   3845     if object.keys().map(String::as_str).collect::<BTreeSet<_>>() != expected_keys {
   3846         return Err(AdvisoryError::new(AdvisoryFailureKind::InvalidReport));
   3847     }
   3848     let database = object["database"]
   3849         .as_object()
   3850         .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InvalidReport))?;
   3851     if database.keys().map(String::as_str).collect::<BTreeSet<_>>()
   3852         != ["advisory-count", "last-commit", "last-updated"]
   3853             .into_iter()
   3854             .collect()
   3855         || database["advisory-count"]
   3856             .as_u64()
   3857             .is_none_or(|count| count == 0)
   3858         || !database["last-commit"]
   3859             .as_str()
   3860             .is_some_and(|revision| valid_hex(revision, 40))
   3861         || database["last-updated"]
   3862             .as_str()
   3863             .and_then(|timestamp| parse_report_epoch(timestamp).ok())
   3864             != Some(database_digest_time_epoch)
   3865     {
   3866         return Err(AdvisoryError::new(AdvisoryFailureKind::InvalidReport));
   3867     }
   3868     let database_metadata_sha256 = domain_json_digest(
   3869         b"radroots-advisory-rustsec-database-metadata-v1\0",
   3870         database,
   3871     )?;
   3872     if provider_database_identity(
   3873         ProviderId::Rustsec,
   3874         materialized_tree_sha256,
   3875         &database_metadata_sha256,
   3876     )? != expected_database_identity
   3877     {
   3878         return Err(AdvisoryError::new(AdvisoryFailureKind::BindingChanged));
   3879     }
   3880     let lockfile = object["lockfile"]
   3881         .as_object()
   3882         .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InvalidReport))?;
   3883     if lockfile.keys().map(String::as_str).collect::<Vec<_>>() != ["dependency-count"]
   3884         || lockfile["dependency-count"].as_u64() != Some(expected_dependency_count)
   3885     {
   3886         return Err(AdvisoryError::new(AdvisoryFailureKind::InventoryMismatch));
   3887     }
   3888     let settings = object["settings"]
   3889         .as_object()
   3890         .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InvalidReport))?;
   3891     if settings.keys().map(String::as_str).collect::<BTreeSet<_>>()
   3892         != [
   3893             "ignore",
   3894             "informational_warnings",
   3895             "severity",
   3896             "target_arch",
   3897             "target_os",
   3898         ]
   3899         .into_iter()
   3900         .collect()
   3901         || settings["ignore"]
   3902             .as_array()
   3903             .is_none_or(|value| !value.is_empty())
   3904         || settings["target_arch"]
   3905             .as_array()
   3906             .is_none_or(|value| !value.is_empty())
   3907         || settings["target_os"]
   3908             .as_array()
   3909             .is_none_or(|value| !value.is_empty())
   3910         || !settings["severity"].is_null()
   3911         || settings["informational_warnings"]
   3912             != serde_json::json!(["unmaintained", "unsound", "notice"])
   3913     {
   3914         return Err(AdvisoryError::new(AdvisoryFailureKind::InvalidReport));
   3915     }
   3916     if !object["warnings"].is_object() {
   3917         return Err(AdvisoryError::new(AdvisoryFailureKind::InvalidReport));
   3918     }
   3919     let vulnerabilities_object = object["vulnerabilities"]
   3920         .as_object()
   3921         .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InvalidReport))?;
   3922     if vulnerabilities_object
   3923         .keys()
   3924         .map(String::as_str)
   3925         .collect::<BTreeSet<_>>()
   3926         != ["count", "found", "list"].into_iter().collect()
   3927     {
   3928         return Err(AdvisoryError::new(AdvisoryFailureKind::InvalidReport));
   3929     }
   3930     let vulnerabilities = vulnerabilities_object["list"]
   3931         .as_array()
   3932         .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InvalidReport))?;
   3933     for vulnerability in vulnerabilities {
   3934         let vulnerability_object = vulnerability
   3935             .as_object()
   3936             .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InvalidReport))?;
   3937         if vulnerability_object
   3938             .keys()
   3939             .map(String::as_str)
   3940             .collect::<BTreeSet<_>>()
   3941             != ["advisory", "affected", "package", "versions"]
   3942                 .into_iter()
   3943                 .collect()
   3944         {
   3945             return Err(AdvisoryError::new(AdvisoryFailureKind::InvalidReport));
   3946         }
   3947         let (package, version) = validate_rustsec_package(&vulnerability_object["package"])?;
   3948         let advisory_id = validate_rustsec_advisory(&vulnerability_object["advisory"], package)?;
   3949         validate_rustsec_affected(&vulnerability_object["affected"])?;
   3950         validate_rustsec_versions(&vulnerability_object["versions"])?;
   3951         findings.push(Finding {
   3952             provider: ProviderId::Rustsec,
   3953             advisory_id: advisory_id.to_owned(),
   3954             package_ecosystem: "cargo".to_owned(),
   3955             package_namespace: String::new(),
   3956             package_name: package.to_owned(),
   3957             package_version: version.to_owned(),
   3958             workload_id: workload_id.to_owned(),
   3959         });
   3960     }
   3961     let found = vulnerabilities_object["found"]
   3962         .as_bool()
   3963         .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InvalidReport))?;
   3964     if found != !vulnerabilities.is_empty()
   3965         || vulnerabilities_object["count"].as_u64() != Some(vulnerabilities.len() as u64)
   3966     {
   3967         return Err(AdvisoryError::new(AdvisoryFailureKind::InvalidReport));
   3968     }
   3969     let warnings = object["warnings"]
   3970         .as_object()
   3971         .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InvalidReport))?;
   3972     for (kind, rows) in warnings {
   3973         if !matches!(
   3974             kind.as_str(),
   3975             "notice" | "unmaintained" | "unsound" | "yanked"
   3976         ) {
   3977             return Err(AdvisoryError::new(AdvisoryFailureKind::InvalidReport));
   3978         }
   3979         let rows = rows
   3980             .as_array()
   3981             .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InvalidReport))?;
   3982         if kind == "yanked" && !rows.is_empty() {
   3983             return Err(AdvisoryError::new(AdvisoryFailureKind::InvalidReport));
   3984         }
   3985         for warning in rows {
   3986             let warning_object = warning
   3987                 .as_object()
   3988                 .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InvalidReport))?;
   3989             if warning_object
   3990                 .keys()
   3991                 .map(String::as_str)
   3992                 .collect::<BTreeSet<_>>()
   3993                 != ["advisory", "affected", "kind", "package", "versions"]
   3994                     .into_iter()
   3995                     .collect()
   3996                 || warning_object["kind"].as_str() != Some(kind)
   3997             {
   3998                 return Err(AdvisoryError::new(AdvisoryFailureKind::InvalidReport));
   3999             }
   4000             let (package_name, package_version) =
   4001                 validate_rustsec_package(&warning_object["package"])?;
   4002             let advisory_id = validate_rustsec_advisory(&warning_object["advisory"], package_name)?;
   4003             validate_rustsec_affected(&warning_object["affected"])?;
   4004             validate_rustsec_versions(&warning_object["versions"])?;
   4005             findings.push(Finding {
   4006                 provider: ProviderId::Rustsec,
   4007                 advisory_id: advisory_id.to_owned(),
   4008                 package_ecosystem: "cargo".to_owned(),
   4009                 package_namespace: String::new(),
   4010                 package_name: package_name.to_owned(),
   4011                 package_version: package_version.to_owned(),
   4012                 workload_id: workload_id.to_owned(),
   4013             });
   4014         }
   4015     }
   4016     Ok(())
   4017 }
   4018 
   4019 fn validate_rustsec_advisory<'a>(
   4020     value: &'a Value,
   4021     package_name: &str,
   4022 ) -> Result<&'a str, AdvisoryError> {
   4023     let object = value
   4024         .as_object()
   4025         .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InvalidReport))?;
   4026     if !object_keys_exact(
   4027         object,
   4028         &[
   4029             "aliases",
   4030             "categories",
   4031             "collection",
   4032             "cvss",
   4033             "date",
   4034             "description",
   4035             "expect-deleted",
   4036             "id",
   4037             "informational",
   4038             "keywords",
   4039             "license",
   4040             "package",
   4041             "references",
   4042             "related",
   4043             "source",
   4044             "title",
   4045             "url",
   4046             "withdrawn",
   4047         ],
   4048         &[],
   4049     ) || object["package"].as_str() != Some(package_name)
   4050         || !valid_rustsec_date(&object["date"])
   4051         || !valid_text_allow_empty_value(&object["title"], 4_096)
   4052         || !valid_text_allow_empty_value(&object["description"], 1_048_576)
   4053         || !valid_exact_string_array(&object["aliases"], 4_096, 256)
   4054         || !valid_exact_string_array(&object["related"], 4_096, 256)
   4055         || !valid_exact_string_array(&object["categories"], 256, 128)
   4056         || !valid_exact_string_array(&object["keywords"], 4_096, 256)
   4057         || !valid_exact_string_array(&object["references"], 65_536, 4_096)
   4058         || !optional_rustsec_text(&object["collection"], 64)
   4059         || !optional_rustsec_text(&object["cvss"], 256)
   4060         || !optional_rustsec_text(&object["informational"], 64)
   4061         || !optional_rustsec_text(&object["source"], 4_096)
   4062         || !optional_rustsec_text(&object["url"], 4_096)
   4063         || !(object["withdrawn"].is_null() || valid_rustsec_date(&object["withdrawn"]))
   4064         || !object["license"]
   4065             .as_str()
   4066             .is_some_and(|value| valid_bounded_text(value, 128))
   4067         || object["expect-deleted"].as_bool().is_none()
   4068     {
   4069         return Err(AdvisoryError::new(AdvisoryFailureKind::InvalidReport));
   4070     }
   4071     let advisory_id = required_text(object.get("id"))?;
   4072     if !valid_advisory_id(ProviderId::Rustsec, advisory_id) {
   4073         return Err(AdvisoryError::new(AdvisoryFailureKind::InvalidReport));
   4074     }
   4075     Ok(advisory_id)
   4076 }
   4077 
   4078 fn validate_rustsec_package(value: &Value) -> Result<(&str, &str), AdvisoryError> {
   4079     let object = value
   4080         .as_object()
   4081         .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InvalidReport))?;
   4082     if !object_keys_exact(
   4083         object,
   4084         &["checksum", "name", "replace", "source", "version"],
   4085         &["dependencies"],
   4086     ) || !optional_rustsec_text(&object["source"], 4_096)
   4087         || !(object["checksum"].is_null()
   4088             || object["checksum"]
   4089                 .as_str()
   4090                 .is_some_and(|value| valid_hex(value, 64)))
   4091         || !(object["replace"].is_null() || valid_rustsec_dependency(&object["replace"]))
   4092         || object.get("dependencies").is_some_and(|value| {
   4093             value.as_array().is_none_or(|rows| {
   4094                 rows.is_empty()
   4095                     || rows.len() > 65_536
   4096                     || rows.iter().any(|row| !valid_rustsec_dependency(row))
   4097             })
   4098         })
   4099     {
   4100         return Err(AdvisoryError::new(AdvisoryFailureKind::InvalidReport));
   4101     }
   4102     let name = required_text(object.get("name"))?;
   4103     let version = required_text(object.get("version"))?;
   4104     if !valid_exact_identity_token(name, 256) || !valid_exact_identity_token(version, 128) {
   4105         return Err(AdvisoryError::new(AdvisoryFailureKind::InvalidReport));
   4106     }
   4107     Ok((name, version))
   4108 }
   4109 
   4110 fn valid_rustsec_dependency(value: &Value) -> bool {
   4111     let Some(object) = value.as_object() else {
   4112         return false;
   4113     };
   4114     object_keys_exact(object, &["name", "source", "version"], &[])
   4115         && object["name"]
   4116             .as_str()
   4117             .is_some_and(|value| valid_exact_identity_token(value, 256))
   4118         && object["version"]
   4119             .as_str()
   4120             .is_some_and(|value| valid_exact_identity_token(value, 128))
   4121         && optional_rustsec_text(&object["source"], 4_096)
   4122 }
   4123 
   4124 fn validate_rustsec_versions(value: &Value) -> Result<(), AdvisoryError> {
   4125     let object = value
   4126         .as_object()
   4127         .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InvalidReport))?;
   4128     if !object_keys_exact(object, &["patched", "unaffected"], &[])
   4129         || !valid_exact_string_array(&object["patched"], 65_536, 256)
   4130         || !valid_exact_string_array(&object["unaffected"], 65_536, 256)
   4131     {
   4132         return Err(AdvisoryError::new(AdvisoryFailureKind::InvalidReport));
   4133     }
   4134     Ok(())
   4135 }
   4136 
   4137 fn validate_rustsec_affected(value: &Value) -> Result<(), AdvisoryError> {
   4138     if value.is_null() {
   4139         return Ok(());
   4140     }
   4141     let object = value
   4142         .as_object()
   4143         .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InvalidReport))?;
   4144     let functions = object
   4145         .get("functions")
   4146         .and_then(Value::as_object)
   4147         .filter(|rows| rows.len() <= 65_536)
   4148         .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InvalidReport))?;
   4149     if !object_keys_exact(object, &["arch", "functions", "os"], &[])
   4150         || !valid_exact_string_array(&object["arch"], 1_024, 128)
   4151         || !valid_exact_string_array(&object["os"], 1_024, 128)
   4152         || functions.iter().any(|(path, versions)| {
   4153             !valid_rust_function_path(path) || !valid_exact_string_array(versions, 65_536, 256)
   4154         })
   4155     {
   4156         return Err(AdvisoryError::new(AdvisoryFailureKind::InvalidReport));
   4157     }
   4158     Ok(())
   4159 }
   4160 
   4161 fn valid_rust_function_path(value: &str) -> bool {
   4162     let mut components = value.split("::");
   4163     let valid_component = |component: &str| {
   4164         let mut chars = component.chars();
   4165         chars
   4166             .next()
   4167             .is_some_and(|first| first.is_ascii_alphabetic() || matches!(first, '_' | '<'))
   4168             && chars.all(|character| {
   4169                 character.is_ascii_alphanumeric() || matches!(character, '_' | '<' | '>' | ',')
   4170             })
   4171     };
   4172     let first = components.next().is_some_and(valid_component);
   4173     let rest = components.collect::<Vec<_>>();
   4174     first && !rest.is_empty() && rest.into_iter().all(valid_component)
   4175 }
   4176 
   4177 fn valid_rustsec_date(value: &Value) -> bool {
   4178     value.as_str().is_some_and(|value| {
   4179         let bytes = value.as_bytes();
   4180         bytes.len() == 10
   4181             && bytes[4] == b'-'
   4182             && bytes[7] == b'-'
   4183             && bytes
   4184                 .iter()
   4185                 .enumerate()
   4186                 .all(|(index, byte)| matches!(index, 4 | 7) || byte.is_ascii_digit())
   4187             && parse_report_epoch(&format!("{value}T00:00:00Z")).is_ok()
   4188     })
   4189 }
   4190 
   4191 fn valid_text_allow_empty_value(value: &Value, maximum: usize) -> bool {
   4192     value
   4193         .as_str()
   4194         .is_some_and(|value| valid_text_allow_empty(value, maximum))
   4195 }
   4196 
   4197 fn optional_rustsec_text(value: &Value, maximum: usize) -> bool {
   4198     value.is_null()
   4199         || value
   4200             .as_str()
   4201             .is_some_and(|value| valid_bounded_text(value, maximum))
   4202 }
   4203 
   4204 fn valid_exact_string_array(value: &Value, maximum_rows: usize, maximum_text: usize) -> bool {
   4205     value
   4206         .as_array()
   4207         .filter(|rows| rows.len() <= maximum_rows)
   4208         .is_some_and(|rows| {
   4209             rows.iter().all(|row| {
   4210                 row.as_str()
   4211                     .is_some_and(|value| valid_bounded_text(value, maximum_text))
   4212             })
   4213         })
   4214 }
   4215 
   4216 #[allow(clippy::too_many_arguments)]
   4217 fn parse_owasp_output(
   4218     workload_id: &str,
   4219     output: &Value,
   4220     projection: &GradleProjection,
   4221     expected_database_identity: &str,
   4222     materialized_tree_sha256: &str,
   4223     scan_root: &str,
   4224     scan_started_at_epoch: u64,
   4225     scan_completed_at_epoch: u64,
   4226     database_digest_time_epoch: u64,
   4227     findings: &mut Vec<Finding>,
   4228 ) -> Result<(), AdvisoryError> {
   4229     validate_json_bounds(output)?;
   4230     let object = output
   4231         .as_object()
   4232         .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InvalidReport))?;
   4233     if !object_keys_exact(
   4234         object,
   4235         &["dependencies", "projectInfo", "reportSchema", "scanInfo"],
   4236         &[],
   4237     ) || object["reportSchema"].as_str() != Some("1.1")
   4238     {
   4239         return Err(AdvisoryError::new(AdvisoryFailureKind::InvalidReport));
   4240     }
   4241     let scan_info = object["scanInfo"]
   4242         .as_object()
   4243         .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InvalidReport))?;
   4244     if !object_keys_exact(scan_info, &["dataSource", "engineVersion"], &[])
   4245         || scan_info["engineVersion"].as_str() != Some("12.2.2")
   4246     {
   4247         return Err(AdvisoryError::new(AdvisoryFailureKind::InvalidReport));
   4248     }
   4249     let data_source = scan_info["dataSource"]
   4250         .as_array()
   4251         .filter(|sources| !sources.is_empty())
   4252         .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InvalidReport))?;
   4253     if data_source.len() > 64 {
   4254         return Err(AdvisoryError::new(AdvisoryFailureKind::InvalidReport));
   4255     }
   4256     let mut previous_source = None::<Vec<u8>>;
   4257     let mut observed_database_epoch = None::<u64>;
   4258     for source in data_source {
   4259         let source = source
   4260             .as_object()
   4261             .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InvalidReport))?;
   4262         if !object_keys_exact(source, &["name", "timestamp"], &[])
   4263             || !required_text(source.get("name"))?.starts_with("NVD CVE ")
   4264         {
   4265             return Err(AdvisoryError::new(AdvisoryFailureKind::InvalidReport));
   4266         }
   4267         let epoch = parse_report_epoch(required_text(source.get("timestamp"))?)?;
   4268         if epoch == 0 || epoch > database_digest_time_epoch {
   4269             return Err(AdvisoryError::new(AdvisoryFailureKind::InvalidReport));
   4270         }
   4271         observed_database_epoch =
   4272             Some(observed_database_epoch.map_or(epoch, |value| value.max(epoch)));
   4273         let key = canonical_json_without_lf(&Value::Object(source.clone()));
   4274         if previous_source
   4275             .as_ref()
   4276             .is_some_and(|previous| previous >= &key)
   4277         {
   4278             return Err(AdvisoryError::new(AdvisoryFailureKind::InvalidReport));
   4279         }
   4280         previous_source = Some(key);
   4281     }
   4282     if observed_database_epoch != Some(database_digest_time_epoch) {
   4283         return Err(AdvisoryError::new(AdvisoryFailureKind::BindingChanged));
   4284     }
   4285     let database_metadata_sha256 = domain_json_digest(
   4286         b"radroots-advisory-owasp-database-metadata-v1\0",
   4287         data_source,
   4288     )?;
   4289     if provider_database_identity(
   4290         ProviderId::OwaspNvd,
   4291         materialized_tree_sha256,
   4292         &database_metadata_sha256,
   4293     )? != expected_database_identity
   4294     {
   4295         return Err(AdvisoryError::new(AdvisoryFailureKind::BindingChanged));
   4296     }
   4297     let project = object["projectInfo"]
   4298         .as_object()
   4299         .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InvalidReport))?;
   4300     if !object_keys_exact(
   4301         project,
   4302         &["credits", "name", "reportDate"],
   4303         &["artifactID", "groupID", "version"],
   4304     ) || project["name"].as_str() != Some(workload_id)
   4305         || project
   4306             .get("groupID")
   4307             .is_some_and(|value| !value.is_string())
   4308         || project
   4309             .get("artifactID")
   4310             .is_some_and(|value| !value.is_string())
   4311         || project
   4312             .get("version")
   4313             .is_some_and(|value| !value.is_string())
   4314         || !valid_owasp_credits(&project["credits"])
   4315     {
   4316         return Err(AdvisoryError::new(AdvisoryFailureKind::InvalidReport));
   4317     }
   4318     let report_epoch = parse_report_epoch(required_text(project.get("reportDate"))?)?;
   4319     if report_epoch < scan_started_at_epoch || report_epoch > scan_completed_at_epoch {
   4320         return Err(AdvisoryError::new(AdvisoryFailureKind::BindingChanged));
   4321     }
   4322     if !absolute_path(scan_root) || scan_root.ends_with('/') {
   4323         return Err(AdvisoryError::new(AdvisoryFailureKind::InvalidReport));
   4324     }
   4325     let dependencies = object["dependencies"]
   4326         .as_array()
   4327         .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InvalidReport))?;
   4328     let mut expected = BTreeMap::<&str, Vec<&GradleArtifact>>::new();
   4329     for artifact in &projection.artifacts {
   4330         expected
   4331             .entry(artifact.materialized_name.as_str())
   4332             .or_default()
   4333             .push(artifact);
   4334     }
   4335     if dependencies.len() < expected.len()
   4336         || dependencies.len() > expected.len().saturating_add(65_536)
   4337     {
   4338         return Err(AdvisoryError::new(AdvisoryFailureKind::InventoryMismatch));
   4339     }
   4340     let mut observed = BTreeSet::new();
   4341     let mut parsed_dependencies = Vec::with_capacity(dependencies.len());
   4342     for dependency in dependencies {
   4343         let dependency = dependency
   4344             .as_object()
   4345             .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InvalidReport))?;
   4346         if !object_keys_exact(
   4347             dependency,
   4348             &["evidenceCollected", "fileName", "filePath", "isVirtual"],
   4349             &[
   4350                 "description",
   4351                 "includedBy",
   4352                 "license",
   4353                 "md5",
   4354                 "packages",
   4355                 "projectReferences",
   4356                 "relatedDependencies",
   4357                 "sha1",
   4358                 "sha256",
   4359                 "vulnerabilities",
   4360                 "vulnerabilityIds",
   4361             ],
   4362         ) || dependency.contains_key("suppressedVulnerabilities")
   4363             || dependency.contains_key("suppressedVulnerabilityIds")
   4364             || !valid_owasp_evidence(&dependency["evidenceCollected"])
   4365             || !optional_bounded_string(dependency.get("description"), 65_536)
   4366             || !optional_bounded_string(dependency.get("license"), 65_536)
   4367             || !valid_sorted_string_array(dependency.get("projectReferences"), 16_384, 1024)?
   4368         {
   4369             return Err(AdvisoryError::new(AdvisoryFailureKind::InvalidReport));
   4370         }
   4371         let file_name = required_text(dependency.get("fileName"))?;
   4372         let file_path = required_text(dependency.get("filePath"))?;
   4373         let is_virtual = dependency
   4374             .get("isVirtual")
   4375             .and_then(Value::as_bool)
   4376             .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InvalidReport))?;
   4377         let digest = dependency.get("sha256").and_then(Value::as_str);
   4378         if is_virtual {
   4379             if dependency.contains_key("md5")
   4380                 || dependency.contains_key("sha1")
   4381                 || dependency.contains_key("sha256")
   4382                 || !file_path.starts_with(&format!("{scan_root}/"))
   4383             {
   4384                 return Err(AdvisoryError::new(AdvisoryFailureKind::InvalidReport));
   4385             }
   4386         } else if !dependency
   4387             .get("md5")
   4388             .and_then(Value::as_str)
   4389             .is_some_and(|value| valid_hex(value, 32))
   4390             || !dependency
   4391                 .get("sha1")
   4392                 .and_then(Value::as_str)
   4393                 .is_some_and(|value| valid_hex(value, 40))
   4394             || !digest.is_some_and(|value| valid_hex(value, 64))
   4395             || file_path != format!("{scan_root}/{file_name}")
   4396         {
   4397             return Err(AdvisoryError::new(AdvisoryFailureKind::InvalidReport));
   4398         }
   4399         let package_ids = parse_owasp_identifiers(dependency.get("packages"), true)?;
   4400         let vulnerability_ids = parse_owasp_identifiers(dependency.get("vulnerabilityIds"), true)?;
   4401         let vulnerabilities = parse_owasp_vulnerabilities(dependency.get("vulnerabilities"))?;
   4402         let vulnerable_software_ids = vulnerabilities
   4403             .iter()
   4404             .flat_map(|vulnerability| vulnerability.vulnerable_software_ids.iter())
   4405             .collect::<BTreeSet<_>>();
   4406         if vulnerability_ids
   4407             .iter()
   4408             .any(|id| !valid_owasp_software_identifier(id))
   4409             || (!vulnerability_ids.is_empty() && vulnerabilities.is_empty())
   4410             || vulnerability_ids
   4411                 .iter()
   4412                 .any(|id| !vulnerable_software_ids.contains(id))
   4413         {
   4414             return Err(AdvisoryError::new(AdvisoryFailureKind::InvalidReport));
   4415         }
   4416         let included_by = parse_owasp_included_by(dependency.get("includedBy"))?;
   4417         let related = parse_owasp_related(dependency.get("relatedDependencies"), &expected)?;
   4418         let mut aliases = package_ids.clone();
   4419         aliases.extend(related.aliases);
   4420         aliases.sort();
   4421         if aliases.windows(2).any(|rows| rows[0] == rows[1]) {
   4422             return Err(AdvisoryError::new(AdvisoryFailureKind::InvalidReport));
   4423         }
   4424         if is_virtual {
   4425             if package_ids.is_empty() || included_by.is_empty() {
   4426                 return Err(AdvisoryError::new(AdvisoryFailureKind::InventoryMismatch));
   4427             }
   4428             if expected.contains_key(file_name) {
   4429                 return Err(AdvisoryError::new(AdvisoryFailureKind::InventoryMismatch));
   4430             }
   4431         } else {
   4432             let artifacts = expected
   4433                 .get(file_name)
   4434                 .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InventoryMismatch))?;
   4435             let digest =
   4436                 digest.ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InvalidReport))?;
   4437             if artifacts
   4438                 .iter()
   4439                 .any(|artifact| artifact.artifact_sha256 != digest)
   4440                 || !observed.insert(file_name.to_owned())
   4441                 || package_ids.iter().any(|id| {
   4442                     !artifacts
   4443                         .iter()
   4444                         .any(|artifact| package_identifier_compatible(id, artifact))
   4445                 })
   4446             {
   4447                 return Err(AdvisoryError::new(AdvisoryFailureKind::InvalidReport));
   4448             }
   4449         }
   4450         parsed_dependencies.push(ParsedOwaspDependency {
   4451             aliases,
   4452             included_by,
   4453             is_virtual,
   4454             file_name: file_name.to_owned(),
   4455             package_ids,
   4456             vulnerabilities,
   4457         });
   4458     }
   4459     if observed.len() != expected.len() {
   4460         return Err(AdvisoryError::new(AdvisoryFailureKind::InventoryMismatch));
   4461     }
   4462     validate_owasp_lineage(workload_id, &parsed_dependencies)?;
   4463     for dependency in &parsed_dependencies {
   4464         let advisory_ids = dependency
   4465             .vulnerabilities
   4466             .iter()
   4467             .map(|vulnerability| vulnerability.advisory_id.as_str())
   4468             .collect::<BTreeSet<_>>();
   4469         if dependency.is_virtual {
   4470             for package_id in &dependency.package_ids {
   4471                 let (ecosystem, namespace, name, version) = parse_exact_package_url(package_id)?;
   4472                 for advisory_id in &advisory_ids {
   4473                     findings.push(Finding {
   4474                         provider: ProviderId::OwaspNvd,
   4475                         advisory_id: (*advisory_id).to_owned(),
   4476                         package_ecosystem: ecosystem.clone(),
   4477                         package_namespace: namespace.clone(),
   4478                         package_name: name.clone(),
   4479                         package_version: version.clone(),
   4480                         workload_id: workload_id.to_owned(),
   4481                     });
   4482                 }
   4483             }
   4484         } else {
   4485             let artifacts = expected
   4486                 .get(dependency.file_name.as_str())
   4487                 .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InventoryMismatch))?;
   4488             for artifact in artifacts {
   4489                 for advisory_id in &advisory_ids {
   4490                     findings.push(Finding {
   4491                         provider: ProviderId::OwaspNvd,
   4492                         advisory_id: (*advisory_id).to_owned(),
   4493                         package_ecosystem: artifact.package_ecosystem.clone(),
   4494                         package_namespace: artifact.package_namespace.clone(),
   4495                         package_name: artifact.package_name.clone(),
   4496                         package_version: artifact.package_version.clone(),
   4497                         workload_id: workload_id.to_owned(),
   4498                     });
   4499                 }
   4500             }
   4501         }
   4502     }
   4503     Ok(())
   4504 }
   4505 
   4506 #[derive(Clone)]
   4507 struct ParsedOwaspVulnerability {
   4508     advisory_id: String,
   4509     vulnerable_software_ids: BTreeSet<String>,
   4510 }
   4511 
   4512 struct ParsedOwaspDependency {
   4513     aliases: Vec<String>,
   4514     included_by: Vec<String>,
   4515     is_virtual: bool,
   4516     file_name: String,
   4517     package_ids: Vec<String>,
   4518     vulnerabilities: Vec<ParsedOwaspVulnerability>,
   4519 }
   4520 
   4521 struct ParsedOwaspRelated {
   4522     aliases: Vec<String>,
   4523 }
   4524 
   4525 fn object_keys_exact(
   4526     object: &serde_json::Map<String, Value>,
   4527     required: &[&str],
   4528     optional: &[&str],
   4529 ) -> bool {
   4530     required.iter().all(|key| object.contains_key(*key))
   4531         && object
   4532             .keys()
   4533             .all(|key| required.contains(&key.as_str()) || optional.contains(&key.as_str()))
   4534         && object.len() >= required.len()
   4535         && object.len() <= required.len() + optional.len()
   4536 }
   4537 
   4538 fn validate_json_bounds(value: &Value) -> Result<(), AdvisoryError> {
   4539     fn visit(value: &Value, depth: usize, nodes: &mut usize) -> bool {
   4540         if depth > 32 || *nodes >= 1_000_000 {
   4541             return false;
   4542         }
   4543         *nodes += 1;
   4544         match value {
   4545             Value::String(value) => valid_bounded_text(value, 1_048_576),
   4546             Value::Array(values) => {
   4547                 values.len() <= 65_536 && values.iter().all(|value| visit(value, depth + 1, nodes))
   4548             }
   4549             Value::Object(values) => {
   4550                 values.len() <= 1_024
   4551                     && values.keys().all(|key| valid_bounded_text(key, 256))
   4552                     && values.values().all(|value| visit(value, depth + 1, nodes))
   4553             }
   4554             _ => true,
   4555         }
   4556     }
   4557     if visit(value, 0, &mut 0) {
   4558         Ok(())
   4559     } else {
   4560         Err(AdvisoryError::new(AdvisoryFailureKind::InvalidReport))
   4561     }
   4562 }
   4563 
   4564 fn parse_report_epoch(value: &str) -> Result<u64, AdvisoryError> {
   4565     let bytes = value.as_bytes();
   4566     if bytes.len() < 20
   4567         || bytes.get(4) != Some(&b'-')
   4568         || bytes.get(7) != Some(&b'-')
   4569         || bytes.get(10) != Some(&b'T')
   4570         || bytes.get(13) != Some(&b':')
   4571         || bytes.get(16) != Some(&b':')
   4572         || bytes.last() != Some(&b'Z')
   4573         || (bytes.len() > 20
   4574             && (bytes.get(19) != Some(&b'.')
   4575                 || !bytes[20..bytes.len() - 1].iter().all(u8::is_ascii_digit)))
   4576     {
   4577         return Err(AdvisoryError::new(AdvisoryFailureKind::InvalidReport));
   4578     }
   4579     fn number(bytes: &[u8]) -> Option<u64> {
   4580         bytes.iter().try_fold(0_u64, |value, byte| {
   4581             byte.is_ascii_digit()
   4582                 .then(|| value * 10 + u64::from(byte - b'0'))
   4583         })
   4584     }
   4585     let year = i64::try_from(number(&bytes[0..4]).unwrap_or_default()).unwrap_or_default();
   4586     let month = number(&bytes[5..7]).unwrap_or_default();
   4587     let day = number(&bytes[8..10]).unwrap_or_default();
   4588     let hour = number(&bytes[11..13]).unwrap_or(24);
   4589     let minute = number(&bytes[14..16]).unwrap_or(60);
   4590     let second = number(&bytes[17..19]).unwrap_or(60);
   4591     let leap = year % 4 == 0 && (year % 100 != 0 || year % 400 == 0);
   4592     let days_in_month = match month {
   4593         1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
   4594         4 | 6 | 9 | 11 => 30,
   4595         2 if leap => 29,
   4596         2 => 28,
   4597         _ => 0,
   4598     };
   4599     if year < 1970 || day == 0 || day > days_in_month || hour > 23 || minute > 59 || second > 59 {
   4600         return Err(AdvisoryError::new(AdvisoryFailureKind::InvalidReport));
   4601     }
   4602     let adjusted_year = year - i64::from(month <= 2);
   4603     let era = adjusted_year.div_euclid(400);
   4604     let year_of_era = adjusted_year - era * 400;
   4605     let adjusted_month = i64::try_from(month).unwrap_or_default() + if month > 2 { -3 } else { 9 };
   4606     let day_of_year = (153 * adjusted_month + 2) / 5 + i64::try_from(day).unwrap_or_default() - 1;
   4607     let day_of_era = year_of_era * 365 + year_of_era / 4 - year_of_era / 100 + day_of_year;
   4608     let days = era * 146_097 + day_of_era - 719_468;
   4609     u64::try_from(days)
   4610         .ok()
   4611         .and_then(|days| days.checked_mul(86_400))
   4612         .and_then(|value| value.checked_add(hour * 3_600 + minute * 60 + second))
   4613         .filter(|value| *value > 0)
   4614         .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InvalidReport))
   4615 }
   4616 
   4617 fn format_report_epoch(value: u64) -> Result<String, AdvisoryError> {
   4618     let days = i64::try_from(value / 86_400)
   4619         .map_err(|_| AdvisoryError::new(AdvisoryFailureKind::InvalidSnapshot))?;
   4620     let seconds = value % 86_400;
   4621     let z = days + 719_468;
   4622     let era = z.div_euclid(146_097);
   4623     let day_of_era = z - era * 146_097;
   4624     let year_of_era =
   4625         (day_of_era - day_of_era / 1_460 + day_of_era / 36_524 - day_of_era / 146_096) / 365;
   4626     let mut year = year_of_era + era * 400;
   4627     let day_of_year = day_of_era - (365 * year_of_era + year_of_era / 4 - year_of_era / 100);
   4628     let month_prime = (5 * day_of_year + 2) / 153;
   4629     let day = day_of_year - (153 * month_prime + 2) / 5 + 1;
   4630     let month = month_prime + if month_prime < 10 { 3 } else { -9 };
   4631     year += i64::from(month <= 2);
   4632     Ok(format!(
   4633         "{year:04}-{month:02}-{day:02}T{:02}:{:02}:{:02}Z",
   4634         seconds / 3_600,
   4635         seconds % 3_600 / 60,
   4636         seconds % 60,
   4637     ))
   4638 }
   4639 
   4640 fn optional_bounded_string(value: Option<&Value>, maximum: usize) -> bool {
   4641     value.is_none_or(|value| {
   4642         value
   4643             .as_str()
   4644             .is_some_and(|value| valid_bounded_text(value, maximum))
   4645     })
   4646 }
   4647 
   4648 fn valid_text_allow_empty(value: &str, maximum: usize) -> bool {
   4649     value.len() <= maximum && !value.contains('\0') && !value.chars().any(char::is_control)
   4650 }
   4651 
   4652 fn valid_sorted_string_array(
   4653     value: Option<&Value>,
   4654     maximum_rows: usize,
   4655     maximum_bytes: usize,
   4656 ) -> Result<bool, AdvisoryError> {
   4657     let Some(value) = value else {
   4658         return Ok(true);
   4659     };
   4660     let rows = value
   4661         .as_array()
   4662         .filter(|rows| rows.len() <= maximum_rows)
   4663         .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InvalidReport))?;
   4664     let mut previous = None;
   4665     for row in rows {
   4666         let row = row
   4667             .as_str()
   4668             .filter(|row| valid_bounded_text(row, maximum_bytes))
   4669             .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InvalidReport))?;
   4670         if previous.is_some_and(|previous| previous >= row) {
   4671             return Ok(false);
   4672         }
   4673         previous = Some(row);
   4674     }
   4675     Ok(true)
   4676 }
   4677 
   4678 fn valid_owasp_credits(value: &Value) -> bool {
   4679     let Some(object) = value.as_object() else {
   4680         return false;
   4681     };
   4682     const CREDITS: [(&str, &str); 5] = [
   4683         (
   4684             "CISA",
   4685             "This report may contain data retrieved from the CISA Known Exploited Vulnerability Catalog: https://www.cisa.gov/known-exploited-vulnerabilities-catalog",
   4686         ),
   4687         (
   4688             "NPM",
   4689             "This report may contain data retrieved from the Github Advisory Database (via NPM Audit API): https://github.com/advisories/",
   4690         ),
   4691         (
   4692             "NVD",
   4693             "This product uses the NVD API but is not endorsed or certified by the NVD. This report contains data retrieved from the National Vulnerability Database: https://nvd.nist.gov",
   4694         ),
   4695         (
   4696             "OSSINDEX",
   4697             "This report may contain data retrieved from the Sonatype Guide OSS Index API: https://www.sonatype.com/products/sonatype-guide/oss-index-users",
   4698         ),
   4699         (
   4700             "RETIREJS",
   4701             "This report may contain data retrieved from the RetireJS community: https://retirejs.github.io/retire.js/",
   4702         ),
   4703     ];
   4704     object.len() == CREDITS.len()
   4705         && CREDITS
   4706             .iter()
   4707             .all(|(key, value)| object.get(*key).and_then(Value::as_str) == Some(*value))
   4708 }
   4709 
   4710 fn valid_owasp_evidence(value: &Value) -> bool {
   4711     let Some(object) = value.as_object() else {
   4712         return false;
   4713     };
   4714     if !object_keys_exact(
   4715         object,
   4716         &["productEvidence", "vendorEvidence", "versionEvidence"],
   4717         &[],
   4718     ) {
   4719         return false;
   4720     }
   4721     [
   4722         ("productEvidence", "product"),
   4723         ("vendorEvidence", "vendor"),
   4724         ("versionEvidence", "version"),
   4725     ]
   4726     .into_iter()
   4727     .all(|(key, kind)| {
   4728         object[key]
   4729             .as_array()
   4730             .filter(|rows| rows.len() <= 65_536)
   4731             .is_some_and(|rows| {
   4732                 rows.iter().all(|row| {
   4733                     row.as_object().is_some_and(|row| {
   4734                         object_keys_exact(
   4735                             row,
   4736                             &["confidence", "name", "source", "type", "value"],
   4737                             &[],
   4738                         ) && row["type"].as_str() == Some(kind)
   4739                             && ["confidence", "name", "source", "value"].iter().all(|key| {
   4740                                 row[*key]
   4741                                     .as_str()
   4742                                     .is_some_and(|value| valid_bounded_text(value, 4096))
   4743                             })
   4744                     })
   4745                 })
   4746             })
   4747     })
   4748 }
   4749 
   4750 fn parse_owasp_identifiers(
   4751     value: Option<&Value>,
   4752     allow_confidence: bool,
   4753 ) -> Result<Vec<String>, AdvisoryError> {
   4754     let Some(value) = value else {
   4755         return Ok(Vec::new());
   4756     };
   4757     let rows = value
   4758         .as_array()
   4759         .filter(|rows| !rows.is_empty() && rows.len() <= 65_536)
   4760         .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InvalidReport))?;
   4761     let mut result = Vec::with_capacity(rows.len());
   4762     for row in rows {
   4763         let row = row
   4764             .as_object()
   4765             .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InvalidReport))?;
   4766         let optional = if allow_confidence {
   4767             ["confidence", "notes", "url"].as_slice()
   4768         } else {
   4769             ["notes", "url"].as_slice()
   4770         };
   4771         if !object_keys_exact(row, &["id"], optional)
   4772             || optional
   4773                 .iter()
   4774                 .any(|key| !optional_bounded_string(row.get(*key), 4096))
   4775         {
   4776             return Err(AdvisoryError::new(AdvisoryFailureKind::InvalidReport));
   4777         }
   4778         result.push(required_text(row.get("id"))?.to_owned());
   4779     }
   4780     if result.windows(2).any(|rows| rows[0] >= rows[1]) {
   4781         return Err(AdvisoryError::new(AdvisoryFailureKind::InvalidReport));
   4782     }
   4783     Ok(result)
   4784 }
   4785 
   4786 fn parse_owasp_included_by(value: Option<&Value>) -> Result<Vec<String>, AdvisoryError> {
   4787     let Some(value) = value else {
   4788         return Ok(Vec::new());
   4789     };
   4790     let rows = value
   4791         .as_array()
   4792         .filter(|rows| !rows.is_empty() && rows.len() <= 65_536)
   4793         .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InvalidReport))?;
   4794     let mut result = Vec::with_capacity(rows.len());
   4795     let mut previous = None::<Vec<u8>>;
   4796     for row in rows {
   4797         let row = row
   4798             .as_object()
   4799             .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InvalidReport))?;
   4800         if !object_keys_exact(row, &["reference"], &["type"])
   4801             || !optional_bounded_string(row.get("type"), 256)
   4802         {
   4803             return Err(AdvisoryError::new(AdvisoryFailureKind::InvalidReport));
   4804         }
   4805         let key = canonical_json_without_lf(&Value::Object(row.clone()));
   4806         if previous.as_ref().is_some_and(|previous| previous >= &key) {
   4807             return Err(AdvisoryError::new(AdvisoryFailureKind::InvalidReport));
   4808         }
   4809         previous = Some(key);
   4810         result.push(required_text(row.get("reference"))?.to_owned());
   4811     }
   4812     Ok(result)
   4813 }
   4814 
   4815 fn parse_owasp_related(
   4816     value: Option<&Value>,
   4817     expected: &BTreeMap<&str, Vec<&GradleArtifact>>,
   4818 ) -> Result<ParsedOwaspRelated, AdvisoryError> {
   4819     let Some(value) = value else {
   4820         return Ok(ParsedOwaspRelated {
   4821             aliases: Vec::new(),
   4822         });
   4823     };
   4824     let rows = value
   4825         .as_array()
   4826         .filter(|rows| !rows.is_empty() && rows.len() <= 65_536)
   4827         .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InvalidReport))?;
   4828     let mut aliases = Vec::new();
   4829     let mut previous = None::<Vec<u8>>;
   4830     for row in rows {
   4831         let row = row
   4832             .as_object()
   4833             .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InvalidReport))?;
   4834         if !object_keys_exact(
   4835             row,
   4836             &["fileName", "filePath", "isVirtual"],
   4837             &["md5", "packageIds", "sha1", "sha256"],
   4838         ) {
   4839             return Err(AdvisoryError::new(AdvisoryFailureKind::InvalidReport));
   4840         }
   4841         let is_virtual = row["isVirtual"]
   4842             .as_bool()
   4843             .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InvalidReport))?;
   4844         let file_name = required_text(row.get("fileName"))?;
   4845         required_text(row.get("filePath"))?;
   4846         if is_virtual {
   4847             if row.contains_key("md5") || row.contains_key("sha1") || row.contains_key("sha256") {
   4848                 return Err(AdvisoryError::new(AdvisoryFailureKind::InvalidReport));
   4849             }
   4850         } else if !row
   4851             .get("md5")
   4852             .and_then(Value::as_str)
   4853             .is_some_and(|value| valid_hex(value, 32))
   4854             || !row
   4855                 .get("sha1")
   4856                 .and_then(Value::as_str)
   4857                 .is_some_and(|value| valid_hex(value, 40))
   4858             || !row
   4859                 .get("sha256")
   4860                 .and_then(Value::as_str)
   4861                 .is_some_and(|value| valid_hex(value, 64))
   4862         {
   4863             return Err(AdvisoryError::new(AdvisoryFailureKind::InvalidReport));
   4864         }
   4865         if let (Some(expected), Some(digest)) = (
   4866             expected.get(file_name),
   4867             row.get("sha256").and_then(Value::as_str),
   4868         ) && expected
   4869             .iter()
   4870             .any(|artifact| artifact.artifact_sha256 != digest)
   4871         {
   4872             return Err(AdvisoryError::new(AdvisoryFailureKind::BindingChanged));
   4873         }
   4874         aliases.extend(parse_owasp_identifiers(row.get("packageIds"), false)?);
   4875         let key = canonical_json_without_lf(&Value::Object(row.clone()));
   4876         if previous.as_ref().is_some_and(|previous| previous >= &key) {
   4877             return Err(AdvisoryError::new(AdvisoryFailureKind::InvalidReport));
   4878         }
   4879         previous = Some(key);
   4880     }
   4881     Ok(ParsedOwaspRelated { aliases })
   4882 }
   4883 
   4884 fn parse_owasp_vulnerabilities(
   4885     value: Option<&Value>,
   4886 ) -> Result<Vec<ParsedOwaspVulnerability>, AdvisoryError> {
   4887     let Some(value) = value else {
   4888         return Ok(Vec::new());
   4889     };
   4890     let rows = value
   4891         .as_array()
   4892         .filter(|rows| !rows.is_empty() && rows.len() <= 65_536)
   4893         .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InvalidReport))?;
   4894     let mut result = Vec::with_capacity(rows.len());
   4895     for row in rows {
   4896         let row = row
   4897             .as_object()
   4898             .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InvalidReport))?;
   4899         if !object_keys_exact(
   4900             row,
   4901             &[
   4902                 "description",
   4903                 "name",
   4904                 "notes",
   4905                 "references",
   4906                 "source",
   4907                 "vulnerableSoftware",
   4908             ],
   4909             &["cvssv2", "cvssv3", "cvssv4", "cwes", "severity", "unscored"],
   4910         ) || row["source"].as_str() != Some("NVD")
   4911             || !optional_bounded_string(row.get("severity"), 128)
   4912             || row
   4913                 .get("unscored")
   4914                 .is_some_and(|value| value.as_str() != Some("true"))
   4915             || !["description", "notes"].iter().all(|key| {
   4916                 row[*key]
   4917                     .as_str()
   4918                     .is_some_and(|value| valid_text_allow_empty(value, 1_048_576))
   4919             })
   4920             || !valid_owasp_references(&row["references"])
   4921             || !valid_sorted_string_array(row.get("cwes"), 65_536, 256)?
   4922             || !valid_owasp_cvss(row.get("cvssv2"), OwaspCvssVersion::V2)
   4923             || !valid_owasp_cvss(row.get("cvssv3"), OwaspCvssVersion::V3)
   4924             || !valid_owasp_cvss(row.get("cvssv4"), OwaspCvssVersion::V4)
   4925         {
   4926             return Err(AdvisoryError::new(AdvisoryFailureKind::InvalidReport));
   4927         }
   4928         let advisory_id = required_text(row.get("name"))?;
   4929         let vulnerable_software_ids = parse_owasp_vulnerable_software(&row["vulnerableSoftware"])?;
   4930         if !valid_advisory_id(ProviderId::OwaspNvd, advisory_id) {
   4931             return Err(AdvisoryError::new(AdvisoryFailureKind::InvalidReport));
   4932         }
   4933         result.push(ParsedOwaspVulnerability {
   4934             advisory_id: advisory_id.to_owned(),
   4935             vulnerable_software_ids,
   4936         });
   4937     }
   4938     if result
   4939         .windows(2)
   4940         .any(|rows| rows[0].advisory_id >= rows[1].advisory_id)
   4941     {
   4942         return Err(AdvisoryError::new(AdvisoryFailureKind::InvalidReport));
   4943     }
   4944     Ok(result)
   4945 }
   4946 
   4947 fn valid_owasp_references(value: &Value) -> bool {
   4948     value
   4949         .as_array()
   4950         .filter(|rows| rows.len() <= 65_536)
   4951         .is_some_and(|rows| {
   4952             rows.iter().all(|row| {
   4953                 row.as_object().is_some_and(|row| {
   4954                     object_keys_exact(row, &["source"], &["name", "url"])
   4955                         && ["source", "name", "url"]
   4956                             .iter()
   4957                             .all(|key| optional_bounded_string(row.get(*key), 4096))
   4958                 })
   4959             })
   4960         })
   4961 }
   4962 
   4963 fn parse_owasp_vulnerable_software(value: &Value) -> Result<BTreeSet<String>, AdvisoryError> {
   4964     let rows = value
   4965         .as_array()
   4966         .filter(|rows| !rows.is_empty() && rows.len() <= 65_536)
   4967         .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InvalidReport))?;
   4968     let mut identifiers = BTreeSet::new();
   4969     for row in rows {
   4970         let row = row
   4971             .as_object()
   4972             .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InvalidReport))?;
   4973         if !object_keys_exact(row, &["software"], &[]) {
   4974             return Err(AdvisoryError::new(AdvisoryFailureKind::InvalidReport));
   4975         }
   4976         let software = row["software"]
   4977             .as_object()
   4978             .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InvalidReport))?;
   4979         if !object_keys_exact(
   4980             software,
   4981             &["id"],
   4982             &[
   4983                 "versionEndExcluding",
   4984                 "versionEndIncluding",
   4985                 "versionStartExcluding",
   4986                 "versionStartIncluding",
   4987                 "vulnerabilityIdMatched",
   4988                 "vulnerable",
   4989             ],
   4990         ) || software
   4991             .get("vulnerabilityIdMatched")
   4992             .is_some_and(|value| value.as_str() != Some("true"))
   4993             || software
   4994                 .get("vulnerable")
   4995                 .is_some_and(|value| value.as_str() != Some("false"))
   4996             || [
   4997                 "versionEndExcluding",
   4998                 "versionEndIncluding",
   4999                 "versionStartExcluding",
   5000                 "versionStartIncluding",
   5001             ]
   5002             .iter()
   5003             .any(|key| !optional_bounded_string(software.get(*key), 256))
   5004         {
   5005             return Err(AdvisoryError::new(AdvisoryFailureKind::InvalidReport));
   5006         }
   5007         let identifier = required_text(software.get("id"))?;
   5008         if !valid_owasp_software_identifier(identifier)
   5009             || !identifiers.insert(identifier.to_owned())
   5010         {
   5011             return Err(AdvisoryError::new(AdvisoryFailureKind::InvalidReport));
   5012         }
   5013     }
   5014     Ok(identifiers)
   5015 }
   5016 
   5017 fn valid_owasp_software_identifier(value: &str) -> bool {
   5018     valid_bounded_text(value, 4096)
   5019         && (value.starts_with("cpe:2.3:")
   5020             || value.starts_with("cpe:/")
   5021             || parse_exact_package_url(value).is_ok())
   5022 }
   5023 
   5024 #[derive(Clone, Copy)]
   5025 enum OwaspCvssVersion {
   5026     V2,
   5027     V3,
   5028     V4,
   5029 }
   5030 
   5031 fn valid_owasp_cvss(value: Option<&Value>, version: OwaspCvssVersion) -> bool {
   5032     let Some(value) = value else {
   5033         return true;
   5034     };
   5035     let Some(object) = value.as_object() else {
   5036         return false;
   5037     };
   5038     let (required, optional, numeric): (&[&str], &[&str], &[&str]) = match version {
   5039         OwaspCvssVersion::V2 => (
   5040             &[
   5041                 "accessComplexity",
   5042                 "accessVector",
   5043                 "authenticationr",
   5044                 "availabilityImpact",
   5045                 "confidentialityImpact",
   5046                 "integrityImpact",
   5047                 "score",
   5048                 "severity",
   5049             ],
   5050             &[
   5051                 "acInsufInfo",
   5052                 "exploitabilityScore",
   5053                 "impactScore",
   5054                 "obtainAllPrivilege",
   5055                 "obtainOtherPrivilege",
   5056                 "obtainUserPrivilege",
   5057                 "userInteractionRequired",
   5058                 "version",
   5059             ],
   5060             &["score"],
   5061         ),
   5062         OwaspCvssVersion::V3 => (
   5063             &[
   5064                 "attackComplexity",
   5065                 "attackVector",
   5066                 "availabilityImpact",
   5067                 "baseScore",
   5068                 "baseSeverity",
   5069                 "confidentialityImpact",
   5070                 "integrityImpact",
   5071                 "privilegesRequired",
   5072                 "scope",
   5073                 "userInteraction",
   5074             ],
   5075             &["exploitabilityScore", "impactScore", "version"],
   5076             &["baseScore"],
   5077         ),
   5078         OwaspCvssVersion::V4 => (
   5079             &[],
   5080             &[
   5081                 "attackComplexity",
   5082                 "attackRequirements",
   5083                 "attackVector",
   5084                 "automatable",
   5085                 "availabilityRequirements",
   5086                 "baseScore",
   5087                 "baseSeverity",
   5088                 "confidentialityRequirements",
   5089                 "environmentalScore",
   5090                 "environmentalSeverity",
   5091                 "exploitMaturity",
   5092                 "integrityRequirements",
   5093                 "modifiedAttackComplexity",
   5094                 "modifiedAttackRequirements",
   5095                 "modifiedAttackVector",
   5096                 "modifiedPrivilegesRequired",
   5097                 "modifiedSubsequentSystemAvailability",
   5098                 "modifiedSubsequentSystemConfidentiality",
   5099                 "modifiedSubsequentSystemIntegrity",
   5100                 "modifiedUserInteraction",
   5101                 "modifiedVulnerableSystemAvailability",
   5102                 "modifiedVulnerableSystemConfidentiality",
   5103                 "modifiedVulnerableSystemIntegrity",
   5104                 "privilegesRequired",
   5105                 "providerUrgency",
   5106                 "recovery",
   5107                 "safety",
   5108                 "source",
   5109                 "subsequentSystemAvailability",
   5110                 "subsequentSystemConfidentiality",
   5111                 "subsequentSystemIntegrity",
   5112                 "threatScore",
   5113                 "threatSeverity",
   5114                 "type",
   5115                 "userInteraction",
   5116                 "valueDensity",
   5117                 "vectorString",
   5118                 "version",
   5119                 "vulnerabilityResponseEffort",
   5120                 "vulnerableSystemAvailability",
   5121                 "vulnerableSystemConfidentiality",
   5122                 "vulnerableSystemIntegrity",
   5123             ],
   5124             &["baseScore", "environmentalScore", "threatScore"],
   5125         ),
   5126     };
   5127     object_keys_exact(object, required, optional)
   5128         && object.iter().all(|(key, value)| {
   5129             if numeric.contains(&key.as_str()) {
   5130                 value.as_f64().is_some_and(|value| value.is_finite())
   5131             } else {
   5132                 value
   5133                     .as_str()
   5134                     .is_some_and(|value| valid_bounded_text(value, 4096))
   5135             }
   5136         })
   5137 }
   5138 
   5139 fn validate_owasp_lineage(
   5140     workload_id: &str,
   5141     dependencies: &[ParsedOwaspDependency],
   5142 ) -> Result<(), AdvisoryError> {
   5143     const ROOT: usize = usize::MAX;
   5144     let mut aliases = BTreeMap::<String, usize>::new();
   5145     for root in [workload_id.to_owned(), format!("project:{workload_id}")] {
   5146         if aliases.insert(root, ROOT).is_some() {
   5147             return Err(AdvisoryError::new(AdvisoryFailureKind::InvalidReport));
   5148         }
   5149     }
   5150     for (index, dependency) in dependencies.iter().enumerate() {
   5151         for alias in &dependency.aliases {
   5152             if aliases.insert(alias.clone(), index).is_some() {
   5153                 return Err(AdvisoryError::new(AdvisoryFailureKind::InvalidReport));
   5154             }
   5155         }
   5156     }
   5157     let mut states = vec![0_u8; dependencies.len()];
   5158     fn reaches_root(
   5159         index: usize,
   5160         dependencies: &[ParsedOwaspDependency],
   5161         aliases: &BTreeMap<String, usize>,
   5162         states: &mut [u8],
   5163     ) -> Result<bool, AdvisoryError> {
   5164         if !dependencies[index].is_virtual {
   5165             return Ok(true);
   5166         }
   5167         match states[index] {
   5168             1 => return Err(AdvisoryError::new(AdvisoryFailureKind::InvalidReport)),
   5169             2 => return Ok(true),
   5170             3 => return Ok(false),
   5171             _ => {}
   5172         }
   5173         states[index] = 1;
   5174         let mut reached = false;
   5175         for parent in &dependencies[index].included_by {
   5176             let target = aliases
   5177                 .get(parent)
   5178                 .copied()
   5179                 .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InventoryMismatch))?;
   5180             reached |= target == usize::MAX || reaches_root(target, dependencies, aliases, states)?;
   5181         }
   5182         states[index] = if reached { 2 } else { 3 };
   5183         Ok(reached)
   5184     }
   5185     for index in 0..dependencies.len() {
   5186         if dependencies[index].is_virtual
   5187             && !reaches_root(index, dependencies, &aliases, &mut states)?
   5188         {
   5189             return Err(AdvisoryError::new(AdvisoryFailureKind::InventoryMismatch));
   5190         }
   5191     }
   5192     Ok(())
   5193 }
   5194 
   5195 fn parse_exact_package_url(value: &str) -> Result<(String, String, String, String), AdvisoryError> {
   5196     if !valid_bounded_text(value, 512)
   5197         || value.contains(['%', '?', '#'])
   5198         || !value.starts_with("pkg:maven/")
   5199     {
   5200         return Err(AdvisoryError::new(AdvisoryFailureKind::InvalidReport));
   5201     }
   5202     let body = &value["pkg:maven/".len()..];
   5203     let (path, version) = body
   5204         .split_once('@')
   5205         .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InvalidReport))?;
   5206     let (namespace, name) = path
   5207         .rsplit_once('/')
   5208         .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InvalidReport))?;
   5209     if !valid_exact_identity_token(namespace, 256)
   5210         || !valid_exact_identity_token(name, 256)
   5211         || !valid_exact_identity_token(version, 128)
   5212     {
   5213         return Err(AdvisoryError::new(AdvisoryFailureKind::InvalidReport));
   5214     }
   5215     Ok((
   5216         "maven".to_owned(),
   5217         namespace.to_owned(),
   5218         name.to_owned(),
   5219         version.to_owned(),
   5220     ))
   5221 }
   5222 
   5223 fn package_identifier_compatible(value: &str, artifact: &GradleArtifact) -> bool {
   5224     let parsed = parse_exact_package_url(value).ok();
   5225     artifact.package_ecosystem == "maven"
   5226         && valid_exact_identity_token(&artifact.package_namespace, 256)
   5227         && parsed
   5228             == Some((
   5229                 "maven".to_owned(),
   5230                 artifact.package_namespace.clone(),
   5231                 artifact.package_name.clone(),
   5232                 artifact.package_version.clone(),
   5233             ))
   5234         && value
   5235             == format!(
   5236                 "pkg:maven/{}/{}@{}",
   5237                 artifact.package_namespace, artifact.package_name, artifact.package_version
   5238             )
   5239 }
   5240 
   5241 fn required_text(value: Option<&Value>) -> Result<&str, AdvisoryError> {
   5242     value
   5243         .and_then(Value::as_str)
   5244         .filter(|value| valid_bounded_text(value, 256))
   5245         .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InvalidReport))
   5246 }
   5247 
   5248 fn validate_suppression_inventory(
   5249     suppressions: &[Suppression],
   5250     evaluation_epoch: u64,
   5251 ) -> Result<(), AdvisoryError> {
   5252     let mut previous = None;
   5253     let mut identifiers = BTreeSet::new();
   5254     for suppression in suppressions {
   5255         if suppression.advisory_id == NON_WAIVABLE_RUSTSEC
   5256             || !valid_advisory_id(suppression.provider, &suppression.advisory_id)
   5257             || !valid_exact_identity_token(&suppression.id, 128)
   5258             || !matches!(suppression.package_ecosystem.as_str(), "cargo" | "maven")
   5259             || match suppression.package_ecosystem.as_str() {
   5260                 "cargo" => !suppression.package_namespace.is_empty(),
   5261                 "maven" => !valid_exact_identity_token(&suppression.package_namespace, 256),
   5262                 _ => true,
   5263             }
   5264             || !valid_exact_identity_token(&suppression.package_name, 256)
   5265             || !valid_exact_identity_token(&suppression.package_version, 128)
   5266             || !WORKLOADS
   5267                 .iter()
   5268                 .any(|workload| workload.id == suppression.workload_id)
   5269             || !valid_bounded_text(&suppression.owner, 256)
   5270             || !valid_bounded_text(&suppression.rationale, 1024)
   5271             || !identifiers.insert(suppression.id.as_str())
   5272             || suppression.created_at_epoch == 0
   5273             || suppression.created_at_epoch >= suppression.expires_at_epoch
   5274             || suppression.created_at_epoch > evaluation_epoch
   5275         {
   5276             return Err(AdvisoryError::new(AdvisoryFailureKind::InvalidSnapshot));
   5277         }
   5278         if evaluation_epoch >= suppression.expires_at_epoch {
   5279             return Err(AdvisoryError::new(AdvisoryFailureKind::ExpiredSuppression));
   5280         }
   5281         let key = suppression_key(suppression);
   5282         if previous.as_ref().is_some_and(|value| value >= &key) {
   5283             return Err(AdvisoryError::new(AdvisoryFailureKind::InvalidSnapshot));
   5284         }
   5285         previous = Some(key);
   5286     }
   5287     Ok(())
   5288 }
   5289 
   5290 fn apply_suppressions(
   5291     findings: &mut Vec<Finding>,
   5292     suppressions: &[Suppression],
   5293     evaluation_epoch: u64,
   5294 ) -> Result<(), AdvisoryError> {
   5295     validate_suppression_inventory(suppressions, evaluation_epoch)?;
   5296     for suppression in suppressions {
   5297         let matches = findings
   5298             .iter()
   5299             .enumerate()
   5300             .filter(|(_, finding)| suppression_matches(suppression, finding))
   5301             .map(|(index, _)| index)
   5302             .collect::<Vec<_>>();
   5303         if matches.len() != 1 {
   5304             return Err(AdvisoryError::new(AdvisoryFailureKind::InvalidSnapshot));
   5305         }
   5306         findings.remove(matches[0]);
   5307     }
   5308     Ok(())
   5309 }
   5310 
   5311 fn suppression_matches(suppression: &Suppression, finding: &Finding) -> bool {
   5312     suppression.provider == finding.provider
   5313         && suppression.advisory_id == finding.advisory_id
   5314         && suppression.package_ecosystem == finding.package_ecosystem
   5315         && suppression.package_namespace == finding.package_namespace
   5316         && suppression.package_name == finding.package_name
   5317         && suppression.package_version == finding.package_version
   5318         && suppression.workload_id == finding.workload_id
   5319 }
   5320 
   5321 fn finding_key(finding: &Finding) -> (&str, &str, &str, &str, &str, &str, &str) {
   5322     (
   5323         finding.provider.as_str(),
   5324         &finding.advisory_id,
   5325         &finding.package_ecosystem,
   5326         &finding.package_namespace,
   5327         &finding.package_name,
   5328         &finding.package_version,
   5329         &finding.workload_id,
   5330     )
   5331 }
   5332 
   5333 fn suppression_key(suppression: &Suppression) -> (&str, &str, &str, &str, &str, &str, &str, &str) {
   5334     (
   5335         suppression.provider.as_str(),
   5336         &suppression.advisory_id,
   5337         &suppression.workload_id,
   5338         &suppression.package_ecosystem,
   5339         &suppression.package_namespace,
   5340         &suppression.package_name,
   5341         &suppression.package_version,
   5342         &suppression.id,
   5343     )
   5344 }
   5345 
   5346 fn validate_blob(expected: &BlobBinding, actual: &FileEvidence) -> Result<(), AdvisoryError> {
   5347     if expected.byte_length != actual.byte_length || expected.sha256 != actual.sha256 {
   5348         Err(AdvisoryError::new(AdvisoryFailureKind::BindingChanged))
   5349     } else {
   5350         Ok(())
   5351     }
   5352 }
   5353 
   5354 fn valid_blob_binding(binding: &BlobBinding) -> bool {
   5355     let maximum = if binding.media_type == "application/gzip" {
   5356         2_147_483_648
   5357     } else {
   5358         MAX_REPORT_BYTES
   5359     };
   5360     binding.byte_length > 0
   5361         && binding.byte_length <= maximum
   5362         && valid_hex(&binding.sha256, 64)
   5363         && SNAPSHOT_FILE_NAMES.contains(&binding.path.as_str())
   5364         && binding.logical_uri == format!("extbuild-cas://sha256/{}", binding.sha256)
   5365         && matches!(
   5366             binding.media_type.as_str(),
   5367             "application/gzip" | "application/json"
   5368         )
   5369         && matches!(
   5370             binding.logical_role.as_str(),
   5371             "rustsec_database_snapshot"
   5372                 | "owasp_nvd_database_snapshot"
   5373                 | "rustsec_unsuppressed_report"
   5374                 | "owasp_unsuppressed_report"
   5375         )
   5376 }
   5377 
   5378 fn valid_provider_blob_binding(provider: ProviderId, binding: &BlobBinding, archive: bool) -> bool {
   5379     if !valid_blob_binding(binding) {
   5380         return false;
   5381     }
   5382     matches!(
   5383         (
   5384             provider,
   5385             archive,
   5386             binding.path.as_str(),
   5387             binding.media_type.as_str(),
   5388             binding.logical_role.as_str()
   5389         ),
   5390         (
   5391             ProviderId::Rustsec,
   5392             true,
   5393             RUSTSEC_ARCHIVE_NAME,
   5394             "application/gzip",
   5395             "rustsec_database_snapshot"
   5396         ) | (
   5397             ProviderId::Rustsec,
   5398             false,
   5399             RUSTSEC_REPORT_NAME,
   5400             "application/json",
   5401             "rustsec_unsuppressed_report"
   5402         ) | (
   5403             ProviderId::OwaspNvd,
   5404             true,
   5405             NVD_ARCHIVE_NAME,
   5406             "application/gzip",
   5407             "owasp_nvd_database_snapshot"
   5408         ) | (
   5409             ProviderId::OwaspNvd,
   5410             false,
   5411             OWASP_REPORT_NAME,
   5412             "application/json",
   5413             "owasp_unsuppressed_report"
   5414         )
   5415     )
   5416 }
   5417 
   5418 fn inventory_digest(inventory: &[WorkloadInventory]) -> Result<String, AdvisoryError> {
   5419     domain_json_digest(b"radroots-advisory-workload-inventory-v1\0", inventory)
   5420 }
   5421 
   5422 fn source_digest(sources: &[SourceIdentity]) -> Result<String, AdvisoryError> {
   5423     domain_json_digest(b"radroots-advisory-source-inventory-v1\0", sources)
   5424 }
   5425 
   5426 fn tool_observation_digest(
   5427     acquisitions: &[ToolAcquisition],
   5428     states: &[ToolState],
   5429 ) -> Result<String, AdvisoryError> {
   5430     domain_json_digest(
   5431         b"radroots-advisory-tool-observation-v1\0",
   5432         &serde_json::json!({"acquisition": acquisitions, "state": states}),
   5433     )
   5434 }
   5435 
   5436 fn provider_trace_digest(providers: &[ProviderSnapshot]) -> Result<String, AdvisoryError> {
   5437     domain_json_digest(b"radroots-advisory-provider-trace-v1\0", providers)
   5438 }
   5439 
   5440 fn provider_database_identity(
   5441     provider: ProviderId,
   5442     materialized_tree_sha256: &str,
   5443     report_database_metadata_sha256: &str,
   5444 ) -> Result<String, AdvisoryError> {
   5445     domain_json_digest(
   5446         b"radroots-advisory-provider-database-identity-v1\0",
   5447         &serde_json::json!({
   5448             "materialized_tree_sha256": materialized_tree_sha256,
   5449             "provider": provider,
   5450             "report_database_metadata_sha256": report_database_metadata_sha256,
   5451         }),
   5452     )
   5453 }
   5454 
   5455 fn database_identity_from_report(
   5456     provider: ProviderId,
   5457     report: &[u8],
   5458     materialized_tree_sha256: &str,
   5459 ) -> Result<String, AdvisoryError> {
   5460     let envelope: ReportEnvelope = serde_json::from_slice(report)
   5461         .map_err(|_| AdvisoryError::new(AdvisoryFailureKind::InvalidReport))?;
   5462     if envelope.provider != provider || envelope.workload_result.is_empty() {
   5463         return Err(AdvisoryError::new(AdvisoryFailureKind::InvalidReport));
   5464     }
   5465     let mut metadata = None::<String>;
   5466     for result in envelope.workload_result {
   5467         let raw: Value = serde_json::from_str(&result.raw_scanner_output)
   5468             .map_err(|_| AdvisoryError::new(AdvisoryFailureKind::InvalidReport))?;
   5469         let current = match provider {
   5470             ProviderId::Rustsec => domain_json_digest(
   5471                 b"radroots-advisory-rustsec-database-metadata-v1\0",
   5472                 raw.get("database")
   5473                     .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InvalidReport))?,
   5474             )?,
   5475             ProviderId::OwaspNvd => domain_json_digest(
   5476                 b"radroots-advisory-owasp-database-metadata-v1\0",
   5477                 raw.pointer("/scanInfo/dataSource")
   5478                     .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InvalidReport))?,
   5479             )?,
   5480         };
   5481         if metadata.as_ref().is_some_and(|value| value != &current) {
   5482             return Err(AdvisoryError::new(AdvisoryFailureKind::BindingChanged));
   5483         }
   5484         metadata = Some(current);
   5485     }
   5486     provider_database_identity(
   5487         provider,
   5488         materialized_tree_sha256,
   5489         &metadata.ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InvalidReport))?,
   5490     )
   5491 }
   5492 
   5493 fn finding_digest(findings: &[Finding]) -> Result<String, AdvisoryError> {
   5494     let rows = findings
   5495         .iter()
   5496         .map(|finding| {
   5497             serde_json::json!({
   5498                 "advisory_id": finding.advisory_id,
   5499                 "package_ecosystem": finding.package_ecosystem,
   5500                 "package_namespace": finding.package_namespace,
   5501                 "package_name": finding.package_name,
   5502                 "package_version": finding.package_version,
   5503                 "provider": finding.provider,
   5504                 "workload_id": finding.workload_id,
   5505             })
   5506         })
   5507         .collect::<Vec<_>>();
   5508     domain_json_digest(b"radroots-advisory-finding-inventory-v1\0", &rows)
   5509 }
   5510 
   5511 fn suppression_digest(suppressions: &[Suppression]) -> Result<String, AdvisoryError> {
   5512     domain_json_digest(
   5513         b"radroots-advisory-suppression-inventory-v1\0",
   5514         suppressions,
   5515     )
   5516 }
   5517 
   5518 fn gradle_projection_digest(projections: &[GradleProjection]) -> Result<String, AdvisoryError> {
   5519     domain_json_digest(b"radroots-advisory-gradle-projection-v1\0", projections)
   5520 }
   5521 
   5522 fn candidate_advisory_input_digest(manifest: &SnapshotManifest) -> Result<String, AdvisoryError> {
   5523     domain_json_digest(
   5524         b"radroots-advisory-candidate-input-v1\0",
   5525         &serde_json::json!({
   5526             "candidate": manifest.candidate,
   5527             "fresh_tool_observation_sha256": manifest.fresh_tool_observation_sha256,
   5528             "gradle_projection": manifest.gradle_projection,
   5529             "inventory": manifest.inventory,
   5530             "provider_snapshot": manifest.provider_snapshot,
   5531             "sources": manifest.sources,
   5532             "step_297_tool_manifest_sha256": manifest.step_297_tool_manifest_sha256,
   5533             "suppressions": manifest.suppressions,
   5534             "tool_acquisition": manifest.tool_acquisition,
   5535             "tool_state": manifest.tool_state,
   5536         }),
   5537     )
   5538 }
   5539 
   5540 fn domain_json_digest<T: Serialize + ?Sized>(
   5541     domain: &[u8],
   5542     value: &T,
   5543 ) -> Result<String, AdvisoryError> {
   5544     let value = serde_json::to_value(value)
   5545         .map_err(|_| AdvisoryError::new(AdvisoryFailureKind::InvalidSnapshot))?;
   5546     let bytes = canonical_json_without_lf(&value);
   5547     let mut hasher = Sha256::new();
   5548     hasher.update(domain);
   5549     hasher.update(bytes);
   5550     Ok(hex::encode(hasher.finalize()))
   5551 }
   5552 
   5553 fn materialized_tree_digest(snapshot: &TraversalSnapshot) -> Result<String, AdvisoryError> {
   5554     let directories = snapshot
   5555         .directories()
   5556         .map(|(path, mode)| {
   5557             path.to_str()
   5558                 .map(|path| serde_json::json!({"mode": mode, "path": path}))
   5559                 .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InvalidSnapshot))
   5560         })
   5561         .collect::<Result<Vec<_>, _>>()?;
   5562     let mut files = Vec::with_capacity(snapshot.files().len());
   5563     for file in snapshot.files() {
   5564         let evidence = snapshot
   5565             .hash(file, 17_179_869_184)
   5566             .map_err(map_binding_error)?;
   5567         let path = file
   5568             .relative_path()
   5569             .to_str()
   5570             .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InvalidSnapshot))?;
   5571         files.push(serde_json::json!({
   5572             "byte_length": evidence.byte_length,
   5573             "mode": file.permission_mode(),
   5574             "path": path,
   5575             "sha256": evidence.sha256,
   5576         }));
   5577     }
   5578     snapshot.revalidate().map_err(map_binding_error)?;
   5579     domain_json_digest(
   5580         b"radroots-advisory-materialized-tree-v1\0",
   5581         &serde_json::json!({
   5582             "directories": directories,
   5583             "entry_count": snapshot.entry_count(),
   5584             "files": files,
   5585             "total_bytes": snapshot.total_bytes(),
   5586         }),
   5587     )
   5588 }
   5589 
   5590 fn archive_limits() -> TarGzipLimits {
   5591     TarGzipLimits {
   5592         max_compressed_bytes: 2_147_483_648,
   5593         max_expanded_bytes: 17_179_869_184,
   5594         max_members: 65_536,
   5595         max_member_bytes: 17_179_869_184,
   5596         max_payload_bytes: 17_179_869_184,
   5597         max_depth: 64,
   5598         max_path_bytes: 4_096,
   5599     }
   5600 }
   5601 
   5602 fn map_binding_error(error: safe_artifact_io::ArtifactIoError) -> AdvisoryError {
   5603     match error.kind() {
   5604         safe_artifact_io::ArtifactIoFailureKind::ChangedDuringRead => {
   5605             AdvisoryError::new(AdvisoryFailureKind::BindingChanged)
   5606         }
   5607         _ => AdvisoryError::new(AdvisoryFailureKind::InvalidSnapshot),
   5608     }
   5609 }
   5610 
   5611 fn map_archive_error(error: safe_artifact_io::ArtifactIoError) -> AdvisoryError {
   5612     match error.kind() {
   5613         safe_artifact_io::ArtifactIoFailureKind::ChangedDuringRead => {
   5614             AdvisoryError::new(AdvisoryFailureKind::BindingChanged)
   5615         }
   5616         _ => AdvisoryError::new(AdvisoryFailureKind::ArchiveRejected),
   5617     }
   5618 }
   5619 
   5620 fn canonical_json(value: &Value) -> Vec<u8> {
   5621     let mut bytes = canonical_json_without_lf(value);
   5622     bytes.push(b'\n');
   5623     bytes
   5624 }
   5625 
   5626 fn canonical_json_without_lf(value: &Value) -> Vec<u8> {
   5627     serde_json::to_vec(value).unwrap_or_default()
   5628 }
   5629 
   5630 fn sha256(bytes: &[u8]) -> String {
   5631     hex::encode(Sha256::digest(bytes))
   5632 }
   5633 
   5634 fn valid_hex(value: &str, length: usize) -> bool {
   5635     value.len() == length
   5636         && value
   5637             .bytes()
   5638             .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
   5639 }
   5640 
   5641 fn valid_identifier(value: &str) -> bool {
   5642     valid_bounded_text(value, 256)
   5643         && !Path::new(value).is_absolute()
   5644         && Path::new(value)
   5645             .components()
   5646             .all(|component| matches!(component, Component::Normal(_)))
   5647         && !value.contains('\\')
   5648 }
   5649 
   5650 fn valid_bounded_text(value: &str, maximum: usize) -> bool {
   5651     !value.is_empty()
   5652         && value.len() <= maximum
   5653         && !value.contains('\0')
   5654         && !value.chars().any(char::is_control)
   5655 }
   5656 
   5657 fn valid_exact_identity_token(value: &str, maximum: usize) -> bool {
   5658     valid_bounded_text(value, maximum)
   5659         && value.bytes().all(|byte| {
   5660             byte.is_ascii_alphanumeric()
   5661                 || matches!(byte, b'-' | b'_' | b'.' | b'/' | b':' | b'@' | b'+')
   5662         })
   5663         && !value.contains("..")
   5664 }
   5665 
   5666 fn valid_advisory_id(provider: ProviderId, value: &str) -> bool {
   5667     let (prefix, minimum_digits) = match provider {
   5668         ProviderId::Rustsec => ("RUSTSEC-", 9),
   5669         ProviderId::OwaspNvd => ("CVE-", 9),
   5670     };
   5671     if !value.starts_with(prefix) || value.len() < prefix.len() + minimum_digits {
   5672         return false;
   5673     }
   5674     let remainder = &value[prefix.len()..];
   5675     let Some((year, sequence)) = remainder.split_once('-') else {
   5676         return false;
   5677     };
   5678     year.len() == 4
   5679         && year.bytes().all(|byte| byte.is_ascii_digit())
   5680         && sequence.len() >= 4
   5681         && sequence.bytes().all(|byte| byte.is_ascii_digit())
   5682 }
   5683 
   5684 pub(crate) fn self_test() -> Result<(), String> {
   5685     self_test_suite().map_err(|error| error.to_string())?;
   5686     verify_gradle_raw_bridge().map_err(|error| error.to_string())?;
   5687     verify_bounded_process_surface().map_err(|error| error.to_string())
   5688 }
   5689 
   5690 fn verify_gradle_raw_bridge() -> Result<(), AdvisoryError> {
   5691     let fixture = trusted_tempdir(".radroots-gradle-bridge-")?;
   5692     let raw_root = fixture.path().join("raw");
   5693     let source_root = fixture.path().join("source");
   5694     let materialization_parent = fixture.path().join("materialized");
   5695     for directory in [&raw_root, &source_root, &materialization_parent] {
   5696         std::fs::create_dir(directory)
   5697             .map_err(|_| AdvisoryError::new(AdvisoryFailureKind::Unavailable))?;
   5698         #[cfg(unix)]
   5699         {
   5700             use std::os::unix::fs::PermissionsExt as _;
   5701             std::fs::set_permissions(directory, std::fs::Permissions::from_mode(0o700))
   5702                 .map_err(|_| AdvisoryError::new(AdvisoryFailureKind::Unavailable))?;
   5703         }
   5704     }
   5705     let artifact_path = source_root.join("example-1.0.0.jar");
   5706     let artifact_bytes = b"synthetic-gradle-artifact";
   5707     std::fs::write(&artifact_path, artifact_bytes)
   5708         .map_err(|_| AdvisoryError::new(AdvisoryFailureKind::Unavailable))?;
   5709     #[cfg(unix)]
   5710     {
   5711         use std::os::unix::fs::PermissionsExt as _;
   5712         std::fs::set_permissions(&artifact_path, std::fs::Permissions::from_mode(0o600))
   5713             .map_err(|_| AdvisoryError::new(AdvisoryFailureKind::Unavailable))?;
   5714     }
   5715     let variant = RawGradleVariant {
   5716         attributes: vec![RawGradleAttribute {
   5717             name: "org.gradle.usage".to_owned(),
   5718             value: "java-runtime".to_owned(),
   5719         }],
   5720         capabilities: Vec::new(),
   5721         external_variant: None,
   5722     };
   5723     let root_core = RawGradleComponentCore {
   5724         build_root: Some(".".to_owned()),
   5725         group: None,
   5726         kind: "project".to_owned(),
   5727         name: None,
   5728         project_path: Some(":app:design_system".to_owned()),
   5729         version: None,
   5730     };
   5731     let module_core = RawGradleComponentCore {
   5732         build_root: None,
   5733         group: Some("com.example".to_owned()),
   5734         kind: "module".to_owned(),
   5735         name: Some("example".to_owned()),
   5736         project_path: None,
   5737         version: Some("1.0.0".to_owned()),
   5738     };
   5739     let mut components = vec![
   5740         RawGradleComponent {
   5741             build_root: root_core.build_root.clone(),
   5742             group: root_core.group.clone(),
   5743             kind: root_core.kind.clone(),
   5744             name: root_core.name.clone(),
   5745             project_path: root_core.project_path.clone(),
   5746             root: true,
   5747             variant: RawGradleVariantEnvelope {
   5748                 selected: vec![variant.clone()],
   5749             },
   5750             version: root_core.version.clone(),
   5751         },
   5752         RawGradleComponent {
   5753             build_root: module_core.build_root.clone(),
   5754             group: module_core.group.clone(),
   5755             kind: module_core.kind.clone(),
   5756             name: module_core.name.clone(),
   5757             project_path: module_core.project_path.clone(),
   5758             root: false,
   5759             variant: RawGradleVariantEnvelope {
   5760                 selected: vec![variant.clone()],
   5761             },
   5762             version: module_core.version.clone(),
   5763         },
   5764     ];
   5765     components.sort_by(|left, right| {
   5766         canonical_row_key(left)
   5767             .unwrap_or_default()
   5768             .cmp(&canonical_row_key(right).unwrap_or_default())
   5769     });
   5770     let raw = RawGradleGraph {
   5771         schema: "radroots.gradle-advisory-graph.v1".to_owned(),
   5772         workload_id: "app_design_system".to_owned(),
   5773         build_root: ".".to_owned(),
   5774         project_path: ":app:design_system".to_owned(),
   5775         configuration: "desktopRuntimeClasspath".to_owned(),
   5776         components,
   5777         edges: vec![RawGradleEdge {
   5778             constraint: false,
   5779             from: root_core,
   5780             requested: RawGradleSelector {
   5781                 attributes: Vec::new(),
   5782                 build_root: None,
   5783                 capabilities: Vec::new(),
   5784                 group: Some("com.example".to_owned()),
   5785                 kind: "module".to_owned(),
   5786                 name: Some("example".to_owned()),
   5787                 project_path: None,
   5788                 version: Some("1.0.0".to_owned()),
   5789                 version_constraint: Some(RawGradleVersionConstraint {
   5790                     branch: None,
   5791                     preferred: String::new(),
   5792                     rejected: Vec::new(),
   5793                     required: "1.0.0".to_owned(),
   5794                     strict: String::new(),
   5795                 }),
   5796             },
   5797             selected_variant: variant.clone(),
   5798             to: module_core.clone(),
   5799         }],
   5800         artifacts: vec![RawGradleArtifact {
   5801             artifact_name: "example-1.0.0.jar".to_owned(),
   5802             artifact_type: "jar".to_owned(),
   5803             classifier: None,
   5804             component: module_core,
   5805             extension: "jar".to_owned(),
   5806             group: Some("com.example".to_owned()),
   5807             logical_name: "example".to_owned(),
   5808             module_version: RawGradleModuleVersion {
   5809                 group: "com.example".to_owned(),
   5810                 name: "example".to_owned(),
   5811                 version: "1.0.0".to_owned(),
   5812             },
   5813             name: Some("example".to_owned()),
   5814             observed_byte_length: artifact_bytes.len() as u64,
   5815             observed_sha256: sha256(artifact_bytes),
   5816             source_path: artifact_path
   5817                 .to_str()
   5818                 .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InvalidSnapshot))?
   5819                 .to_owned(),
   5820             variant,
   5821             version: Some("1.0.0".to_owned()),
   5822         }],
   5823     };
   5824     let raw_value = serde_json::to_value(raw)
   5825         .map_err(|_| AdvisoryError::new(AdvisoryFailureKind::InvalidSnapshot))?;
   5826     std::fs::write(
   5827         raw_root.join(GRADLE_RAW_GRAPH_NAME),
   5828         canonical_json_without_lf(&raw_value),
   5829     )
   5830     .map_err(|_| AdvisoryError::new(AdvisoryFailureKind::Unavailable))?;
   5831     let authority = WORKLOADS
   5832         .iter()
   5833         .find(|workload| workload.id == "app_design_system")
   5834         .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InvalidContract))?;
   5835     let source_root_identity = "3".repeat(64);
   5836     let source_roots = [GradleArtifactSourceRoot {
   5837         path: &source_root,
   5838         logical_role: "candidate_build_output",
   5839         identity_sha256: &source_root_identity,
   5840     }];
   5841     let admitted = admit_raw_gradle_graph(
   5842         &raw_root,
   5843         &materialization_parent,
   5844         authority,
   5845         &"a".repeat(40),
   5846         1,
   5847         &source_roots,
   5848     )?;
   5849     if admitted.workload_id != authority.id
   5850         || admitted.raw_evidence().byte_length == 0
   5851         || admitted.components.len() != 2
   5852         || admitted.edges.len() != 1
   5853         || admitted.artifacts.len() != 1
   5854         || admitted.artifacts[0].package_namespace != "com.example"
   5855         || !valid_hex(&admitted.canonical_graph_sha256, 64)
   5856         || !valid_hex(&admitted.materialized_tree_sha256, 64)
   5857         || !valid_hex(&admitted.normalization_receipt_sha256, 64)
   5858     {
   5859         return Err(AdvisoryError::new(AdvisoryFailureKind::InvalidSnapshot));
   5860     }
   5861     admitted.revalidate()
   5862 }
   5863 
   5864 fn synthetic_admitted_gradle_graphs(
   5865     fixture_root: &Path,
   5866     materialization_parent: &Path,
   5867 ) -> Result<Vec<AdmittedGradleGraph>, AdvisoryError> {
   5868     let mut admitted = Vec::new();
   5869     for (index, authority) in WORKLOADS
   5870         .iter()
   5871         .filter(|workload| workload.package_manager == "gradle")
   5872         .enumerate()
   5873     {
   5874         let workload_root = fixture_root.join(authority.id);
   5875         let raw_root = workload_root.join("raw");
   5876         let source_root = workload_root.join("source");
   5877         for directory in [&workload_root, &raw_root, &source_root] {
   5878             std::fs::create_dir(directory)
   5879                 .map_err(|_| AdvisoryError::new(AdvisoryFailureKind::Unavailable))?;
   5880             #[cfg(unix)]
   5881             {
   5882                 use std::os::unix::fs::PermissionsExt as _;
   5883                 std::fs::set_permissions(directory, std::fs::Permissions::from_mode(0o700))
   5884                     .map_err(|_| AdvisoryError::new(AdvisoryFailureKind::Unavailable))?;
   5885             }
   5886         }
   5887         let package_name = format!("fixture-{}", authority.id.replace('_', "-"));
   5888         let artifact_name = format!("{package_name}-1.0.0.jar");
   5889         let artifact_path = source_root.join(&artifact_name);
   5890         let artifact_bytes = format!("synthetic-gradle-artifact-{index}").into_bytes();
   5891         std::fs::write(&artifact_path, &artifact_bytes)
   5892             .map_err(|_| AdvisoryError::new(AdvisoryFailureKind::Unavailable))?;
   5893         #[cfg(unix)]
   5894         {
   5895             use std::os::unix::fs::PermissionsExt as _;
   5896             std::fs::set_permissions(&artifact_path, std::fs::Permissions::from_mode(0o600))
   5897                 .map_err(|_| AdvisoryError::new(AdvisoryFailureKind::Unavailable))?;
   5898         }
   5899         let variant = RawGradleVariant {
   5900             attributes: vec![RawGradleAttribute {
   5901                 name: "org.gradle.usage".to_owned(),
   5902                 value: "java-runtime".to_owned(),
   5903             }],
   5904             capabilities: Vec::new(),
   5905             external_variant: None,
   5906         };
   5907         let root_core = RawGradleComponentCore {
   5908             build_root: Some(authority.build_root.to_owned()),
   5909             group: None,
   5910             kind: "project".to_owned(),
   5911             name: None,
   5912             project_path: authority.project_path.map(str::to_owned),
   5913             version: None,
   5914         };
   5915         let module_core = RawGradleComponentCore {
   5916             build_root: None,
   5917             group: Some("com.example".to_owned()),
   5918             kind: "module".to_owned(),
   5919             name: Some(package_name.clone()),
   5920             project_path: None,
   5921             version: Some("1.0.0".to_owned()),
   5922         };
   5923         let mut components = vec![
   5924             RawGradleComponent {
   5925                 build_root: root_core.build_root.clone(),
   5926                 group: root_core.group.clone(),
   5927                 kind: root_core.kind.clone(),
   5928                 name: root_core.name.clone(),
   5929                 project_path: root_core.project_path.clone(),
   5930                 root: true,
   5931                 variant: RawGradleVariantEnvelope {
   5932                     selected: vec![variant.clone()],
   5933                 },
   5934                 version: root_core.version.clone(),
   5935             },
   5936             RawGradleComponent {
   5937                 build_root: module_core.build_root.clone(),
   5938                 group: module_core.group.clone(),
   5939                 kind: module_core.kind.clone(),
   5940                 name: module_core.name.clone(),
   5941                 project_path: module_core.project_path.clone(),
   5942                 root: false,
   5943                 variant: RawGradleVariantEnvelope {
   5944                     selected: vec![variant.clone()],
   5945                 },
   5946                 version: module_core.version.clone(),
   5947             },
   5948         ];
   5949         components.sort_by(|left, right| {
   5950             canonical_row_key(left)
   5951                 .unwrap_or_default()
   5952                 .cmp(&canonical_row_key(right).unwrap_or_default())
   5953         });
   5954         let raw = RawGradleGraph {
   5955             schema: "radroots.gradle-advisory-graph.v1".to_owned(),
   5956             workload_id: authority.id.to_owned(),
   5957             build_root: authority.build_root.to_owned(),
   5958             project_path: authority
   5959                 .project_path
   5960                 .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InvalidContract))?
   5961                 .to_owned(),
   5962             configuration: authority
   5963                 .configuration
   5964                 .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InvalidContract))?
   5965                 .to_owned(),
   5966             components,
   5967             edges: vec![RawGradleEdge {
   5968                 constraint: false,
   5969                 from: root_core,
   5970                 requested: RawGradleSelector {
   5971                     attributes: Vec::new(),
   5972                     build_root: None,
   5973                     capabilities: Vec::new(),
   5974                     group: Some("com.example".to_owned()),
   5975                     kind: "module".to_owned(),
   5976                     name: Some(package_name.clone()),
   5977                     project_path: None,
   5978                     version: Some("1.0.0".to_owned()),
   5979                     version_constraint: Some(RawGradleVersionConstraint {
   5980                         branch: None,
   5981                         preferred: String::new(),
   5982                         rejected: Vec::new(),
   5983                         required: "1.0.0".to_owned(),
   5984                         strict: String::new(),
   5985                     }),
   5986                 },
   5987                 selected_variant: variant.clone(),
   5988                 to: module_core.clone(),
   5989             }],
   5990             artifacts: vec![RawGradleArtifact {
   5991                 artifact_name,
   5992                 artifact_type: "jar".to_owned(),
   5993                 classifier: None,
   5994                 component: module_core,
   5995                 extension: "jar".to_owned(),
   5996                 group: Some("com.example".to_owned()),
   5997                 logical_name: package_name.clone(),
   5998                 module_version: RawGradleModuleVersion {
   5999                     group: "com.example".to_owned(),
   6000                     name: package_name.clone(),
   6001                     version: "1.0.0".to_owned(),
   6002                 },
   6003                 name: Some(package_name),
   6004                 observed_byte_length: artifact_bytes.len() as u64,
   6005                 observed_sha256: sha256(&artifact_bytes),
   6006                 source_path: artifact_path
   6007                     .to_str()
   6008                     .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InvalidSnapshot))?
   6009                     .to_owned(),
   6010                 variant,
   6011                 version: Some("1.0.0".to_owned()),
   6012             }],
   6013         };
   6014         let value = serde_json::to_value(raw)
   6015             .map_err(|_| AdvisoryError::new(AdvisoryFailureKind::InvalidSnapshot))?;
   6016         std::fs::write(
   6017             raw_root.join(GRADLE_RAW_GRAPH_NAME),
   6018             canonical_json_without_lf(&value),
   6019         )
   6020         .map_err(|_| AdvisoryError::new(AdvisoryFailureKind::Unavailable))?;
   6021         let source_root_identity = format!("{:064x}", index + 20_000);
   6022         let source_roots = [GradleArtifactSourceRoot {
   6023             path: &source_root,
   6024             logical_role: "candidate_build_output",
   6025             identity_sha256: &source_root_identity,
   6026         }];
   6027         admitted.push(admit_raw_gradle_graph(
   6028             &raw_root,
   6029             materialization_parent,
   6030             authority,
   6031             &"1".repeat(40),
   6032             1,
   6033             &source_roots,
   6034         )?);
   6035     }
   6036     Ok(admitted)
   6037 }
   6038 
   6039 pub(crate) fn write_fixture_scanner_output(mode: &str) -> Result<(), String> {
   6040     match mode {
   6041         "report" => {
   6042             println!("radroots-advisory-fixture-report-v1");
   6043             Ok(())
   6044         }
   6045         "timeout" => {
   6046             std::thread::sleep(Duration::from_secs(5));
   6047             Ok(())
   6048         }
   6049         _ => Err("unknown advisory fixture scanner mode".to_owned()),
   6050     }
   6051 }
   6052 
   6053 fn verify_bounded_process_surface() -> Result<(), AdvisoryError> {
   6054     let executable = std::env::current_exe()
   6055         .map_err(|_| AdvisoryError::new(AdvisoryFailureKind::Unavailable))?;
   6056     let working =
   6057         tempfile::tempdir().map_err(|_| AdvisoryError::new(AdvisoryFailureKind::Unavailable))?;
   6058     let environment = private_process_environment(working.path())?;
   6059     let output = crate::bounded_process::run(
   6060         &crate::bounded_process::ProcessRequest::new(executable.clone())
   6061             .arg("advisory-snapshot-fixture-scanner")
   6062             .arg("--mode")
   6063             .arg("report")
   6064             .current_dir(working.path())
   6065             .environment(environment)
   6066             .deadline(Duration::from_secs(2))
   6067             .output_limits(1024, 1024),
   6068     )
   6069     .map_err(map_process_error)?;
   6070     if !output.status().success()
   6071         || output.stdout() != b"radroots-advisory-fixture-report-v1\n"
   6072         || !output.stderr().is_empty()
   6073     {
   6074         return Err(AdvisoryError::new(AdvisoryFailureKind::InvalidReport));
   6075     }
   6076     let error = crate::bounded_process::run(
   6077         &crate::bounded_process::ProcessRequest::new(executable)
   6078             .arg("advisory-snapshot-fixture-scanner")
   6079             .arg("--mode")
   6080             .arg("timeout")
   6081             .current_dir(working.path())
   6082             .environment(private_process_environment(working.path())?)
   6083             .deadline(Duration::from_millis(50))
   6084             .output_limits(1024, 1024),
   6085     )
   6086     .err()
   6087     .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InvalidSnapshot))?;
   6088     if error.kind() != crate::bounded_process::ProcessFailureKind::DeadlineExceeded {
   6089         return Err(map_process_error(error));
   6090     }
   6091     Ok(())
   6092 }
   6093 
   6094 fn private_process_environment(
   6095     root: &Path,
   6096 ) -> Result<crate::bounded_process::ReplacementEnvironment, AdvisoryError> {
   6097     let mut environment = crate::bounded_process::ReplacementEnvironment::default();
   6098     for (name, value) in [
   6099         ("CARGO_HOME", root.as_os_str().to_owned()),
   6100         ("GRADLE_USER_HOME", root.as_os_str().to_owned()),
   6101         ("HOME", root.as_os_str().to_owned()),
   6102         ("LC_ALL", OsString::from("C")),
   6103         ("PATH", OsString::from("/usr/bin:/bin")),
   6104         ("TMPDIR", root.as_os_str().to_owned()),
   6105         ("TZ", OsString::from("UTC")),
   6106     ] {
   6107         environment.insert(name, value).map_err(map_process_error)?;
   6108     }
   6109     Ok(environment)
   6110 }
   6111 
   6112 fn map_process_error(error: crate::bounded_process::ProcessError) -> AdvisoryError {
   6113     match error.kind() {
   6114         crate::bounded_process::ProcessFailureKind::DeadlineExceeded => {
   6115             AdvisoryError::new(AdvisoryFailureKind::TimedOut)
   6116         }
   6117         crate::bounded_process::ProcessFailureKind::Spawn => {
   6118             AdvisoryError::new(AdvisoryFailureKind::Unavailable)
   6119         }
   6120         _ => AdvisoryError::new(AdvisoryFailureKind::InvalidSnapshot),
   6121     }
   6122 }
   6123 
   6124 fn self_test_suite() -> Result<(), AdvisoryError> {
   6125     let fixture = SyntheticFixture::new()?;
   6126     let evidence = admit_snapshot(
   6127         fixture.root(),
   6128         fixture.materialization_parent.path(),
   6129         &fixture.request,
   6130     )?;
   6131     if evidence.candidate != fixture.request.candidate
   6132         || evidence.inventory_sha256 != inventory_digest(&fixture.request.inventory)?
   6133         || evidence.finding_count != 0
   6134         || evidence.suppression_count != 0
   6135         || evidence.rustsec_archive.compressed.byte_length == 0
   6136         || evidence.nvd_archive.compressed.byte_length == 0
   6137         || evidence.provider_freshness.len() != 2
   6138     {
   6139         return Err(AdvisoryError::new(AdvisoryFailureKind::InvalidSnapshot));
   6140     }
   6141 
   6142     missing_inventory_vector()?;
   6143     unavailable_provider_vector()?;
   6144     stale_snapshot_vector()?;
   6145     timed_out_operation_vector()?;
   6146     known_vulnerable_vector()?;
   6147     expired_suppression_vector()?;
   6148     Ok(())
   6149 }
   6150 
   6151 fn missing_inventory_vector() -> Result<(), AdvisoryError> {
   6152     let mut missing = SyntheticFixture::new()?;
   6153     missing.manifest.inventory.pop();
   6154     missing.seal()?;
   6155     require_failure(&missing, AdvisoryFailureKind::InventoryMismatch)
   6156 }
   6157 
   6158 fn unavailable_provider_vector() -> Result<(), AdvisoryError> {
   6159     let mut unavailable = SyntheticFixture::new()?;
   6160     unavailable.manifest.tool_state[0].state = OperationState::Unavailable;
   6161     unavailable.seal()?;
   6162     require_failure(&unavailable, AdvisoryFailureKind::Unavailable)
   6163 }
   6164 
   6165 fn stale_snapshot_vector() -> Result<(), AdvisoryError> {
   6166     let mut stale = SyntheticFixture::new()?;
   6167     stale.manifest.provider_snapshot[0].digest_time_epoch = stale
   6168         .request
   6169         .evaluation_epoch
   6170         .checked_sub(QUALIFICATION_FRESHNESS_SECONDS + 1)
   6171         .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InvalidSnapshot))?;
   6172     stale.manifest.provider_snapshot[0].acquired_at_epoch = stale.manifest.provider_snapshot[0]
   6173         .digest_time_epoch
   6174         .checked_sub(1)
   6175         .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InvalidSnapshot))?;
   6176     stale.seal()?;
   6177     require_failure(&stale, AdvisoryFailureKind::StaleSnapshot)
   6178 }
   6179 
   6180 fn timed_out_operation_vector() -> Result<(), AdvisoryError> {
   6181     let mut timeout = SyntheticFixture::new()?;
   6182     timeout.manifest.provider_snapshot[1].analysis_state = OperationState::TimedOut;
   6183     timeout.seal()?;
   6184     require_failure(&timeout, AdvisoryFailureKind::TimedOut)
   6185 }
   6186 
   6187 fn known_vulnerable_vector() -> Result<(), AdvisoryError> {
   6188     let mut vulnerable = SyntheticFixture::new()?;
   6189     vulnerable.set_rustsec_finding(NON_WAIVABLE_RUSTSEC, "lru", "0.16.4")?;
   6190     require_failure(&vulnerable, AdvisoryFailureKind::KnownVulnerability)
   6191 }
   6192 
   6193 fn expired_suppression_vector() -> Result<(), AdvisoryError> {
   6194     let mut expired = SyntheticFixture::new()?;
   6195     let expired_artifact = expired
   6196         .manifest
   6197         .gradle_projection
   6198         .first()
   6199         .and_then(|projection| projection.artifacts.first())
   6200         .cloned()
   6201         .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InvalidSnapshot))?;
   6202     expired.set_owasp_finding(
   6203         "CVE-2099-0001",
   6204         &expired_artifact.package_namespace,
   6205         &expired_artifact.package_name,
   6206         &expired_artifact.package_version,
   6207     )?;
   6208     expired.manifest.suppressions.push(Suppression {
   6209         provider: ProviderId::OwaspNvd,
   6210         advisory_id: "CVE-2099-0001".to_owned(),
   6211         id: "synthetic-expired".to_owned(),
   6212         package_ecosystem: "maven".to_owned(),
   6213         package_namespace: expired_artifact.package_namespace,
   6214         package_name: expired_artifact.package_name,
   6215         package_version: expired_artifact.package_version,
   6216         workload_id: "app_design_system".to_owned(),
   6217         owner: "security@example.invalid".to_owned(),
   6218         rationale: "synthetic expiry vector".to_owned(),
   6219         created_at_epoch: expired.request.evaluation_epoch - 100,
   6220         expires_at_epoch: expired.request.evaluation_epoch,
   6221     });
   6222     expired.seal()?;
   6223     require_failure(&expired, AdvisoryFailureKind::ExpiredSuppression)
   6224 }
   6225 
   6226 fn require_failure(
   6227     fixture: &SyntheticFixture,
   6228     expected: AdvisoryFailureKind,
   6229 ) -> Result<(), AdvisoryError> {
   6230     let error = admit_snapshot(
   6231         fixture.root(),
   6232         fixture.materialization_parent.path(),
   6233         &fixture.request,
   6234     )
   6235     .err()
   6236     .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InvalidSnapshot))?;
   6237     if error.kind() == expected {
   6238         Ok(())
   6239     } else {
   6240         Err(AdvisoryError::new(AdvisoryFailureKind::InvalidSnapshot))
   6241     }
   6242 }
   6243 
   6244 struct SyntheticFixture {
   6245     directory: tempfile::TempDir,
   6246     materialization_parent: tempfile::TempDir,
   6247     _gradle_fixture_root: tempfile::TempDir,
   6248     scanner_fixture_root: tempfile::TempDir,
   6249     scanner_generation: u64,
   6250     request: AdmissionRequest,
   6251     manifest: SnapshotManifest,
   6252     process_receipts: Vec<TrustedProcessReceipt>,
   6253 }
   6254 
   6255 impl SyntheticFixture {
   6256     fn new() -> Result<Self, AdvisoryError> {
   6257         use std::fs;
   6258 
   6259         let directory = tempfile::tempdir()
   6260             .map_err(|_| AdvisoryError::new(AdvisoryFailureKind::InvalidSnapshot))?;
   6261         let materialization_parent = trusted_tempdir(".rshr-advisory-materialization-")?;
   6262         let gradle_fixture_root = trusted_tempdir(".rshr-advisory-gradle-input-")?;
   6263         let scanner_fixture_root = trusted_tempdir(".rshr-advisory-scanner-output-")?;
   6264         let evaluation_epoch = 1_000_000;
   6265         let candidate = CandidateIdentity {
   6266             generation: 1,
   6267             digest: "9".repeat(64),
   6268         };
   6269         let sources = synthetic_sources();
   6270         let inventory = synthetic_inventory();
   6271         let producer_request = synthetic_producer_request(&candidate);
   6272         let producer_request_bytes = canonical_authority_bytes(&producer_request)?;
   6273         let producer_request_sha256 = sha256(&producer_request_bytes);
   6274         let rustsec_archive = deterministic_archive(b"synthetic-rustsec")?;
   6275         let nvd_archive = deterministic_archive(b"synthetic-nvd")?;
   6276         fs::write(
   6277             directory.path().join(RUSTSEC_ARCHIVE_NAME),
   6278             &rustsec_archive,
   6279         )
   6280         .map_err(|_| AdvisoryError::new(AdvisoryFailureKind::InvalidSnapshot))?;
   6281         fs::write(directory.path().join(NVD_ARCHIVE_NAME), &nvd_archive)
   6282             .map_err(|_| AdvisoryError::new(AdvisoryFailureKind::InvalidSnapshot))?;
   6283         let tool_acquisition = expected_tool_acquisitions()?;
   6284         let tool_state = synthetic_tool_states(evaluation_epoch);
   6285         let tool_observation_sha256 = tool_observation_digest(&tool_acquisition, &tool_state)?;
   6286         let (rustsec_tree, rustsec_archive_evidence) =
   6287             synthetic_archive_tree_digest(&rustsec_archive, materialization_parent.path())?;
   6288         let (nvd_tree, nvd_archive_evidence) =
   6289             synthetic_archive_tree_digest(&nvd_archive, materialization_parent.path())?;
   6290         let nvd_trace = synthetic_nvd_trace(&candidate, &producer_request_sha256);
   6291         let nvd_trace_bytes = canonical_authority_bytes(&nvd_trace)?;
   6292         let admitted_gradle_graphs = synthetic_admitted_gradle_graphs(
   6293             gradle_fixture_root.path(),
   6294             materialization_parent.path(),
   6295         )?;
   6296         let mut gradle_projection =
   6297             synthetic_gradle_projections(&admitted_gradle_graphs, &inventory, &sources)?;
   6298         let mut process_receipts = vec![synthetic_process_receipt(
   6299             "owasp_nvd_update",
   6300             TOOL_PINS[1].executable_sha256,
   6301             &strings(&OWASP_UPDATE_ARGUMENTS),
   6302             &producer_request_sha256,
   6303             &nvd_update_output_digest(&nvd_tree, &sha256(&nvd_trace_bytes))?,
   6304             vec![ProcessPathBinding {
   6305                 argument_index: 2,
   6306                 logical_role: "nvd_update_data".to_owned(),
   6307                 identity_sha256: nvd_tree.clone(),
   6308             }],
   6309             evaluation_epoch - 3_000,
   6310             evaluation_epoch - 2_990,
   6311             0,
   6312         )?];
   6313         attach_gradle_receipts(
   6314             &mut gradle_projection,
   6315             &mut process_receipts,
   6316             evaluation_epoch,
   6317         )?;
   6318         let rustsec_report = synthetic_report(
   6319             ProviderId::Rustsec,
   6320             &inventory,
   6321             &gradle_projection,
   6322             &sha256(&rustsec_archive),
   6323             &rustsec_tree,
   6324             &tool_observation_sha256,
   6325             &mut process_receipts,
   6326             evaluation_epoch,
   6327         )?;
   6328         let owasp_report = synthetic_report(
   6329             ProviderId::OwaspNvd,
   6330             &inventory,
   6331             &gradle_projection,
   6332             &sha256(&nvd_archive),
   6333             &nvd_tree,
   6334             &tool_observation_sha256,
   6335             &mut process_receipts,
   6336             evaluation_epoch,
   6337         )?;
   6338         fs::write(directory.path().join(RUSTSEC_REPORT_NAME), &rustsec_report)
   6339             .map_err(|_| AdvisoryError::new(AdvisoryFailureKind::InvalidSnapshot))?;
   6340         fs::write(directory.path().join(OWASP_REPORT_NAME), &owasp_report)
   6341             .map_err(|_| AdvisoryError::new(AdvisoryFailureKind::InvalidSnapshot))?;
   6342 
   6343         let provider_snapshot = vec![
   6344             synthetic_provider(
   6345                 ProviderId::Rustsec,
   6346                 &rustsec_archive,
   6347                 &rustsec_report,
   6348                 &rustsec_tree,
   6349                 &rustsec_archive_evidence,
   6350                 &producer_request_sha256,
   6351                 &nvd_trace_bytes,
   6352                 evaluation_epoch,
   6353             )?,
   6354             synthetic_provider(
   6355                 ProviderId::OwaspNvd,
   6356                 &nvd_archive,
   6357                 &owasp_report,
   6358                 &nvd_tree,
   6359                 &nvd_archive_evidence,
   6360                 &producer_request_sha256,
   6361                 &nvd_trace_bytes,
   6362                 evaluation_epoch,
   6363             )?,
   6364         ];
   6365         let mut manifest = SnapshotManifest {
   6366             schema: "radroots.advisory-snapshot.v1".to_owned(),
   6367             candidate: candidate.clone(),
   6368             sources: sources.clone(),
   6369             inventory_sha256: inventory_digest(&inventory)?,
   6370             inventory: inventory.clone(),
   6371             candidate_advisory_input_sha256: String::new(),
   6372             step_297_tool_manifest_sha256: String::new(),
   6373             fresh_tool_observation_sha256: String::new(),
   6374             tool_acquisition,
   6375             tool_state,
   6376             gradle_projection,
   6377             provider_snapshot,
   6378             suppressions: Vec::new(),
   6379         };
   6380         let request = AdmissionRequest {
   6381             candidate,
   6382             sources,
   6383             inventory,
   6384             evaluation_epoch,
   6385             freshness: FreshnessMode::Qualification,
   6386             producer_request: producer_request_bytes,
   6387             step_297_tool_manifest: Vec::new(),
   6388             fresh_tool_observation: Vec::new(),
   6389             nvd_network_trace: nvd_trace_bytes,
   6390             provider_execution_evidence: Vec::new(),
   6391             admitted_gradle_graphs,
   6392             admitted_scanner_outputs: Vec::new(),
   6393         };
   6394         manifest.candidate_advisory_input_sha256 = candidate_advisory_input_digest(&manifest)?;
   6395         let mut fixture = Self {
   6396             directory,
   6397             materialization_parent,
   6398             _gradle_fixture_root: gradle_fixture_root,
   6399             scanner_fixture_root,
   6400             scanner_generation: 0,
   6401             request,
   6402             manifest,
   6403             process_receipts,
   6404         };
   6405         fixture.seal()?;
   6406         Ok(fixture)
   6407     }
   6408 
   6409     fn root(&self) -> &Path {
   6410         self.directory.path()
   6411     }
   6412 
   6413     fn seal(&mut self) -> Result<(), AdvisoryError> {
   6414         let producer_request = sha256(&self.request.producer_request);
   6415         let tool_manifest = TrustedToolManifest {
   6416             schema: "radroots.advisory-tool-manifest-projection.v1".to_owned(),
   6417             candidate_generation: self.request.candidate.generation,
   6418             candidate_digest: self.request.candidate.digest.clone(),
   6419             platform: "macos_aarch64".to_owned(),
   6420             producer_request_sha256: producer_request.clone(),
   6421             tool_acquisition: self.manifest.tool_acquisition.clone(),
   6422         };
   6423         self.request.step_297_tool_manifest = canonical_authority_bytes(&tool_manifest)?;
   6424         let observation = TrustedToolObservation {
   6425             schema: "radroots.advisory-tool-observation.v1".to_owned(),
   6426             candidate_generation: self.request.candidate.generation,
   6427             candidate_digest: self.request.candidate.digest.clone(),
   6428             platform: "macos_aarch64".to_owned(),
   6429             producer_request_sha256: producer_request.clone(),
   6430             tool_manifest_sha256: sha256(&self.request.step_297_tool_manifest),
   6431             row_projection_sha256: tool_observation_digest(
   6432                 &self.manifest.tool_acquisition,
   6433                 &self.manifest.tool_state,
   6434             )?,
   6435             observed_at_epoch: self.request.evaluation_epoch - 4_000,
   6436             tool_state: self.manifest.tool_state.clone(),
   6437         };
   6438         self.request.fresh_tool_observation = canonical_authority_bytes(&observation)?;
   6439         self.manifest.step_297_tool_manifest_sha256 = sha256(&self.request.step_297_tool_manifest);
   6440         self.manifest.fresh_tool_observation_sha256 = sha256(&self.request.fresh_tool_observation);
   6441         self.refresh_gradle_receipts()?;
   6442         self.refresh_admitted_scanner_outputs()?;
   6443         self.manifest.candidate_advisory_input_sha256 =
   6444             candidate_advisory_input_digest(&self.manifest)?;
   6445         let provider_evidence = TrustedProviderEvidence {
   6446             schema: "radroots.advisory-provider-execution-evidence.v1".to_owned(),
   6447             candidate_generation: self.request.candidate.generation,
   6448             candidate_digest: self.request.candidate.digest.clone(),
   6449             platform: "macos_aarch64".to_owned(),
   6450             producer_request_sha256: producer_request,
   6451             candidate_advisory_input_sha256: self.manifest.candidate_advisory_input_sha256.clone(),
   6452             gradle_projection: self.manifest.gradle_projection.clone(),
   6453             provider_snapshot: self.manifest.provider_snapshot.clone(),
   6454             process_receipt: self.process_receipts.clone(),
   6455             suppressions: self.manifest.suppressions.clone(),
   6456         };
   6457         self.request.provider_execution_evidence = canonical_authority_bytes(&provider_evidence)?;
   6458         let value = serde_json::to_value(&self.manifest)
   6459             .map_err(|_| AdvisoryError::new(AdvisoryFailureKind::InvalidSnapshot))?;
   6460         std::fs::write(self.root().join(MANIFEST_NAME), canonical_json(&value))
   6461             .map_err(|_| AdvisoryError::new(AdvisoryFailureKind::InvalidSnapshot))
   6462     }
   6463 
   6464     fn refresh_admitted_scanner_outputs(&mut self) -> Result<(), AdvisoryError> {
   6465         self.scanner_generation = self
   6466             .scanner_generation
   6467             .checked_add(1)
   6468             .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InvalidSnapshot))?;
   6469         let generation_root = self
   6470             .scanner_fixture_root
   6471             .path()
   6472             .join(format!("generation-{}", self.scanner_generation));
   6473         std::fs::create_dir(&generation_root)
   6474             .map_err(|_| AdvisoryError::new(AdvisoryFailureKind::Unavailable))?;
   6475         set_private_directory_mode(&generation_root)?;
   6476         let mut admitted = Vec::with_capacity(WORKLOADS.len());
   6477         for provider in [ProviderId::Rustsec, ProviderId::OwaspNvd] {
   6478             let provider_root = generation_root.join(provider.as_str());
   6479             std::fs::create_dir(&provider_root)
   6480                 .map_err(|_| AdvisoryError::new(AdvisoryFailureKind::Unavailable))?;
   6481             set_private_directory_mode(&provider_root)?;
   6482             let report = self.read_report_value(provider)?;
   6483             let rows = report["workload_result"]
   6484                 .as_array()
   6485                 .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InvalidReport))?;
   6486             for row in rows {
   6487                 let workload_id = required_text(row.get("workload_id"))?;
   6488                 let raw = row
   6489                     .get("raw_scanner_output")
   6490                     .and_then(Value::as_str)
   6491                     .filter(|value| !value.is_empty())
   6492                     .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InvalidReport))?;
   6493                 let workload_root = provider_root.join(workload_id);
   6494                 std::fs::create_dir(&workload_root)
   6495                     .map_err(|_| AdvisoryError::new(AdvisoryFailureKind::Unavailable))?;
   6496                 set_private_directory_mode(&workload_root)?;
   6497                 let output = workload_root.join(RAW_SCANNER_OUTPUT_NAME);
   6498                 std::fs::write(&output, raw.as_bytes())
   6499                     .map_err(|_| AdvisoryError::new(AdvisoryFailureKind::Unavailable))?;
   6500                 set_private_file_mode(&output)?;
   6501                 admitted.push(admit_raw_scanner_output(
   6502                     &workload_root,
   6503                     provider,
   6504                     workload_id,
   6505                 )?);
   6506             }
   6507         }
   6508         self.request.admitted_scanner_outputs = admitted;
   6509         Ok(())
   6510     }
   6511 
   6512     fn refresh_gradle_receipts(&mut self) -> Result<(), AdvisoryError> {
   6513         let source = self
   6514             .request
   6515             .sources
   6516             .iter()
   6517             .find(|source| source.repository == "oss/harvestcircle")
   6518             .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InventoryMismatch))?;
   6519         for projection in &mut self.manifest.gradle_projection {
   6520             let inventory = self
   6521                 .request
   6522                 .inventory
   6523                 .iter()
   6524                 .find(|row| row.id == projection.workload_id)
   6525                 .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InventoryMismatch))?;
   6526             projection.input_sha256 =
   6527                 gradle_candidate_input_digest(&self.request, inventory, source, projection)?;
   6528             let receipt = self
   6529                 .process_receipts
   6530                 .iter_mut()
   6531                 .find(|receipt| receipt.id == format!("gradle:{}", projection.workload_id))
   6532                 .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InventoryMismatch))?;
   6533             receipt.input_sha256 = gradle_process_input_digest(projection)?;
   6534             receipt.output_sha256 = gradle_process_output_digest(projection)?;
   6535             receipt.path_binding = vec![
   6536                 ProcessPathBinding {
   6537                     argument_index: 5,
   6538                     logical_role: "gradle_build_root".to_owned(),
   6539                     identity_sha256: gradle_build_root_binding_digest(projection)?,
   6540                 },
   6541                 ProcessPathBinding {
   6542                     argument_index: 7,
   6543                     logical_role: "gradle_init_script".to_owned(),
   6544                     identity_sha256: projection.init_script_sha256.clone(),
   6545                 },
   6546                 ProcessPathBinding {
   6547                     argument_index: 10,
   6548                     logical_role: "gradle_projection_output".to_owned(),
   6549                     identity_sha256: projection.raw_graph_sha256.clone(),
   6550                 },
   6551             ];
   6552             projection.process_receipt_sha256 = process_receipt_digest(receipt)?;
   6553         }
   6554         Ok(())
   6555     }
   6556 
   6557     fn set_rustsec_finding(
   6558         &mut self,
   6559         advisory_id: &str,
   6560         package: &str,
   6561         version: &str,
   6562     ) -> Result<(), AdvisoryError> {
   6563         let mut report = self.read_report_value(ProviderId::Rustsec)?;
   6564         let raw = report["workload_result"][0]["raw_scanner_output"]
   6565             .as_str()
   6566             .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InvalidReport))?;
   6567         let mut raw: Value = serde_json::from_str(raw)
   6568             .map_err(|_| AdvisoryError::new(AdvisoryFailureKind::InvalidReport))?;
   6569         raw["vulnerabilities"] = serde_json::json!({
   6570             "count": 1,
   6571             "found": true,
   6572             "list": [{
   6573                 "advisory": {
   6574                     "aliases": [],
   6575                     "categories": [],
   6576                     "collection": "crates",
   6577                     "cvss": null,
   6578                     "date": "2099-01-01",
   6579                     "description": "synthetic known-vulnerable RustSec fixture",
   6580                     "expect-deleted": false,
   6581                     "id": advisory_id,
   6582                     "informational": null,
   6583                     "keywords": [],
   6584                     "license": "CC0-1.0",
   6585                     "package": package,
   6586                     "references": [],
   6587                     "related": [],
   6588                     "source": "registry+https://github.com/rust-lang/crates.io-index",
   6589                     "title": "synthetic known-vulnerable fixture",
   6590                     "url": null,
   6591                     "withdrawn": null
   6592                 },
   6593                 "affected": null,
   6594                 "package": {
   6595                     "checksum": null,
   6596                     "name": package,
   6597                     "replace": null,
   6598                     "source": "registry+https://github.com/rust-lang/crates.io-index",
   6599                     "version": version
   6600                 },
   6601                 "versions": {"patched": [], "unaffected": []}
   6602             }]
   6603         });
   6604         set_raw_output(&mut report["workload_result"][0], &raw, 1)?;
   6605         self.replace_report(ProviderId::Rustsec, report)
   6606     }
   6607 
   6608     fn set_owasp_finding(
   6609         &mut self,
   6610         advisory_id: &str,
   6611         package_namespace: &str,
   6612         package: &str,
   6613         version: &str,
   6614     ) -> Result<(), AdvisoryError> {
   6615         let mut report = self.read_report_value(ProviderId::OwaspNvd)?;
   6616         let raw = report["workload_result"][0]["raw_scanner_output"]
   6617             .as_str()
   6618             .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InvalidReport))?;
   6619         let mut raw: Value = serde_json::from_str(raw)
   6620             .map_err(|_| AdvisoryError::new(AdvisoryFailureKind::InvalidReport))?;
   6621         let dependency = raw["dependencies"]
   6622             .as_array_mut()
   6623             .and_then(|rows| rows.first_mut())
   6624             .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InvalidReport))?;
   6625         dependency["packages"] = serde_json::json!([{
   6626             "id": format!("pkg:maven/{package_namespace}/{package}@{version}")
   6627         }]);
   6628         let matched_cpe =
   6629             format!("cpe:2.3:a:{package_namespace}:{package}:{version}:*:*:*:*:*:*:*");
   6630         dependency["vulnerabilityIds"] = serde_json::json!([{
   6631             "id": matched_cpe
   6632         }]);
   6633         dependency["vulnerabilities"] = serde_json::json!([{
   6634             "description": "synthetic known-vulnerable fixture",
   6635             "name": advisory_id,
   6636             "notes": "synthetic unsuppressed scanner output",
   6637             "references": [],
   6638             "source": "NVD",
   6639             "vulnerableSoftware": [{"software": {"id": matched_cpe}}]
   6640         }]);
   6641         set_raw_output(&mut report["workload_result"][0], &raw, 0)?;
   6642         self.replace_report(ProviderId::OwaspNvd, report)
   6643     }
   6644 
   6645     fn read_report_value(&self, provider: ProviderId) -> Result<Value, AdvisoryError> {
   6646         let name = report_name(provider);
   6647         let bytes = std::fs::read(self.root().join(name))
   6648             .map_err(|_| AdvisoryError::new(AdvisoryFailureKind::InvalidSnapshot))?;
   6649         serde_json::from_slice(&bytes)
   6650             .map_err(|_| AdvisoryError::new(AdvisoryFailureKind::InvalidReport))
   6651     }
   6652 
   6653     fn replace_report(
   6654         &mut self,
   6655         provider: ProviderId,
   6656         mut report: Value,
   6657     ) -> Result<(), AdvisoryError> {
   6658         self.refresh_report_receipts(provider, &mut report)?;
   6659         let bytes = canonical_json(&report);
   6660         let name = report_name(provider);
   6661         std::fs::write(self.root().join(name), &bytes)
   6662             .map_err(|_| AdvisoryError::new(AdvisoryFailureKind::InvalidSnapshot))?;
   6663         let snapshot = self
   6664             .manifest
   6665             .provider_snapshot
   6666             .iter_mut()
   6667             .find(|snapshot| snapshot.provider == provider)
   6668             .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InvalidSnapshot))?;
   6669         snapshot.report = blob(name, &bytes)?;
   6670         self.seal()
   6671     }
   6672 
   6673     fn refresh_report_receipts(
   6674         &mut self,
   6675         provider: ProviderId,
   6676         report: &mut Value,
   6677     ) -> Result<(), AdvisoryError> {
   6678         let rows = report["workload_result"]
   6679             .as_array_mut()
   6680             .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InvalidReport))?;
   6681         for row in rows {
   6682             let workload_id = required_text(row.get("workload_id"))?;
   6683             let prefix = match provider {
   6684                 ProviderId::Rustsec => "cargo_audit",
   6685                 ProviderId::OwaspNvd => "owasp_analysis",
   6686             };
   6687             let receipt = self
   6688                 .process_receipts
   6689                 .iter_mut()
   6690                 .find(|receipt| receipt.id == format!("{prefix}:{workload_id}"))
   6691                 .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InventoryMismatch))?;
   6692             let raw = row
   6693                 .get("raw_scanner_output")
   6694                 .and_then(Value::as_str)
   6695                 .filter(|raw| !raw.is_empty() && raw.len() <= MAX_REPORT_BYTES as usize)
   6696                 .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InvalidReport))?;
   6697             receipt.exit_code = row["exit_code"]
   6698                 .as_i64()
   6699                 .and_then(|value| i32::try_from(value).ok())
   6700                 .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InvalidReport))?;
   6701             if provider == ProviderId::Rustsec {
   6702                 receipt.stdout_byte_length = raw.len() as u64;
   6703                 receipt.stdout_sha256 = sha256(raw.as_bytes());
   6704                 receipt.output_sha256 = receipt.stdout_sha256.clone();
   6705             } else {
   6706                 receipt.stdout_byte_length = 0;
   6707                 receipt.stdout_sha256 = sha256(&[]);
   6708                 receipt.output_sha256 = sha256(raw.as_bytes());
   6709                 let report_binding = receipt
   6710                     .path_binding
   6711                     .iter_mut()
   6712                     .find(|binding| binding.logical_role == "raw_report_output")
   6713                     .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InvalidSnapshot))?;
   6714                 report_binding.identity_sha256 = sha256(raw.as_bytes());
   6715             }
   6716             row["process_receipt_sha256"] = Value::from(process_receipt_digest(receipt)?);
   6717         }
   6718         Ok(())
   6719     }
   6720 }
   6721 
   6722 fn expected_tool_acquisitions() -> Result<Vec<ToolAcquisition>, AdvisoryError> {
   6723     let decision: Value = serde_json::from_slice(DECISION_BYTES)
   6724         .map_err(|_| AdvisoryError::new(AdvisoryFailureKind::InvalidContract))?;
   6725     let acquisitions = decision
   6726         .get("tool_acquisition")
   6727         .and_then(Value::as_object)
   6728         .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InvalidContract))?;
   6729     TOOL_PINS
   6730         .iter()
   6731         .map(|pin| {
   6732             acquisitions
   6733                 .get(pin.id)
   6734                 .cloned()
   6735                 .map(|projection| ToolAcquisition {
   6736                     id: pin.id.to_owned(),
   6737                     projection,
   6738                 })
   6739                 .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InvalidContract))
   6740         })
   6741         .collect()
   6742 }
   6743 
   6744 fn synthetic_sources() -> Vec<SourceIdentity> {
   6745     CANDIDATE_REPOSITORIES
   6746         .into_iter()
   6747         .map(|repository| SourceIdentity {
   6748             repository: repository.to_owned(),
   6749             revision: "1".repeat(40),
   6750             tree: "2".repeat(40),
   6751         })
   6752         .collect()
   6753 }
   6754 
   6755 fn synthetic_inventory() -> Vec<WorkloadInventory> {
   6756     WORKLOADS
   6757         .iter()
   6758         .enumerate()
   6759         .map(|(index, workload)| WorkloadInventory {
   6760             id: workload.id.to_owned(),
   6761             repository: workload.repository.to_owned(),
   6762             build_root: workload.build_root.to_owned(),
   6763             package_manager: workload.package_manager.to_owned(),
   6764             language: workload.language.to_owned(),
   6765             manifest_path: workload.manifest_path.map(str::to_owned),
   6766             lockfile_path: workload.lockfile_path.map(str::to_owned),
   6767             project_path: workload.project_path.map(str::to_owned),
   6768             configuration: workload.configuration.map(str::to_owned),
   6769             dependency_count: 1,
   6770             input_sha256: format!("{:064x}", index + 1),
   6771         })
   6772         .collect()
   6773 }
   6774 
   6775 #[allow(clippy::too_many_arguments)]
   6776 fn synthetic_provider(
   6777     provider: ProviderId,
   6778     archive: &[u8],
   6779     report: &[u8],
   6780     materialized_tree_sha256: &str,
   6781     archive_evidence: &ArchiveEvidence,
   6782     producer_request_sha256: &str,
   6783     nvd_trace: &[u8],
   6784     evaluation_epoch: u64,
   6785 ) -> Result<ProviderSnapshot, AdvisoryError> {
   6786     let (
   6787         acquisition_kind,
   6788         archive_name,
   6789         report_name,
   6790         acquisition_arguments,
   6791         analysis_environment,
   6792         analysis_arguments,
   6793     ) = match provider {
   6794         ProviderId::Rustsec => (
   6795             "externally_admitted_immutable_snapshot",
   6796             RUSTSEC_ARCHIVE_NAME,
   6797             RUSTSEC_REPORT_NAME,
   6798             Vec::new(),
   6799             "fresh_private_config_free_workdir_home_and_cargo_home",
   6800             strings(&CARGO_AUDIT_ARGUMENTS),
   6801         ),
   6802         ProviderId::OwaspNvd => (
   6803             "bounded_nvd_update_only",
   6804             NVD_ARCHIVE_NAME,
   6805             OWASP_REPORT_NAME,
   6806             strings(&OWASP_UPDATE_ARGUMENTS),
   6807             "replacement_allowlist_private_data_and_output",
   6808             strings(&OWASP_OFFLINE_ARGUMENTS),
   6809         ),
   6810     };
   6811     Ok(ProviderSnapshot {
   6812         provider,
   6813         acquisition_kind: acquisition_kind.to_owned(),
   6814         acquisition_count: 1,
   6815         acquisition_state: OperationState::Complete,
   6816         acquired_at_epoch: evaluation_epoch - 2_990,
   6817         digest_time_epoch: evaluation_epoch - 2_900,
   6818         database_identity_sha256: database_identity_from_report(
   6819             provider,
   6820             report,
   6821             materialized_tree_sha256,
   6822         )?,
   6823         materialized_tree_sha256: materialized_tree_sha256.to_owned(),
   6824         bounded_deadline_seconds: ANALYSIS_DEADLINE_SECONDS,
   6825         network_mode: match provider {
   6826             ProviderId::Rustsec => "external_snapshot_admission".to_owned(),
   6827             ProviderId::OwaspNvd => "contracted_fetch_only".to_owned(),
   6828         },
   6829         network_trace_sha256: match provider {
   6830             ProviderId::Rustsec => "0".repeat(64),
   6831             ProviderId::OwaspNvd => sha256(nvd_trace),
   6832         },
   6833         producer_request_sha256: producer_request_sha256.to_owned(),
   6834         acquisition_arguments,
   6835         archive_format: "deterministic_tar_gzip_exact_bytes_normalized_headers_sorted_members"
   6836             .to_owned(),
   6837         archive: blob(archive_name, archive)?,
   6838         archive_expanded_bytes: archive_evidence.expanded_bytes,
   6839         archive_member_count: archive_evidence.member_count,
   6840         archive_payload_bytes: archive_evidence.payload_bytes,
   6841         analysis_state: OperationState::Complete,
   6842         analyzed_at_epoch: evaluation_epoch
   6843             - match provider {
   6844                 ProviderId::Rustsec => 2_505,
   6845                 ProviderId::OwaspNvd => 2_245,
   6846             },
   6847         analysis_environment: analysis_environment.to_owned(),
   6848         analysis_arguments,
   6849         report: blob(report_name, report)?,
   6850     })
   6851 }
   6852 
   6853 fn blob(path: &str, bytes: &[u8]) -> Result<BlobBinding, AdvisoryError> {
   6854     let (media_type, logical_role) = match path {
   6855         RUSTSEC_ARCHIVE_NAME => ("application/gzip", "rustsec_database_snapshot"),
   6856         NVD_ARCHIVE_NAME => ("application/gzip", "owasp_nvd_database_snapshot"),
   6857         RUSTSEC_REPORT_NAME => ("application/json", "rustsec_unsuppressed_report"),
   6858         OWASP_REPORT_NAME => ("application/json", "owasp_unsuppressed_report"),
   6859         _ => return Err(AdvisoryError::new(AdvisoryFailureKind::InvalidSnapshot)),
   6860     };
   6861     let digest = sha256(bytes);
   6862     Ok(BlobBinding {
   6863         path: path.to_owned(),
   6864         byte_length: bytes.len() as u64,
   6865         logical_uri: format!("extbuild-cas://sha256/{digest}"),
   6866         media_type: media_type.to_owned(),
   6867         logical_role: logical_role.to_owned(),
   6868         sha256: digest,
   6869     })
   6870 }
   6871 
   6872 fn strings<const N: usize>(values: &[&str; N]) -> Vec<String> {
   6873     values.iter().map(|value| (*value).to_owned()).collect()
   6874 }
   6875 
   6876 fn report_name(provider: ProviderId) -> &'static str {
   6877     match provider {
   6878         ProviderId::Rustsec => RUSTSEC_REPORT_NAME,
   6879         ProviderId::OwaspNvd => OWASP_REPORT_NAME,
   6880     }
   6881 }
   6882 
   6883 fn canonical_authority_bytes<T: Serialize>(value: &T) -> Result<Vec<u8>, AdvisoryError> {
   6884     let value = serde_json::to_value(value)
   6885         .map_err(|_| AdvisoryError::new(AdvisoryFailureKind::InvalidSnapshot))?;
   6886     Ok(canonical_json(&value))
   6887 }
   6888 
   6889 fn synthetic_tool_states(evaluation_epoch: u64) -> Vec<ToolState> {
   6890     TOOL_PINS
   6891         .iter()
   6892         .map(|pin| ToolState {
   6893             id: pin.id.to_owned(),
   6894             state: OperationState::Complete,
   6895             reviewed_at_epoch: evaluation_epoch - 4_100,
   6896             normalized_version: pin.version.to_owned(),
   6897             executable_sha256: pin.executable_sha256.to_owned(),
   6898             source_sha256: pin.source_sha256.to_owned(),
   6899             package_receipt_sha256: pin.receipt_sha256.to_owned(),
   6900         })
   6901         .collect()
   6902 }
   6903 
   6904 fn synthetic_nvd_trace(
   6905     candidate: &CandidateIdentity,
   6906     producer_request_sha256: &str,
   6907 ) -> NvdNetworkTrace {
   6908     let enforcement_program_sha256 = "7".repeat(64);
   6909     let enforcement_configuration_sha256 = "8".repeat(64);
   6910     NvdNetworkTrace {
   6911         schema: "radroots.advisory-nvd-network-trace.v1".to_owned(),
   6912         candidate_generation: candidate.generation,
   6913         candidate_digest: candidate.digest.clone(),
   6914         producer_request_sha256: producer_request_sha256.to_owned(),
   6915         enforcement: "deny_by_default_nvd_api_get_only_proxy".to_owned(),
   6916         enforcement_program_sha256,
   6917         enforcement_configuration_sha256,
   6918         request: vec![NvdRequestTrace {
   6919             sequence: 1,
   6920             method: "GET".to_owned(),
   6921             scheme: "https".to_owned(),
   6922             authority: "services.nvd.nist.gov".to_owned(),
   6923             path: "/rest/json/cves/2.0".to_owned(),
   6924             started_at_epoch: 997_001,
   6925             completed_at_epoch: 997_009,
   6926             query: [
   6927                 ("lastModStartDate", "1970-01-11T10:00:00Z"),
   6928                 ("lastModEndDate", "1970-01-11T10:01:40Z"),
   6929                 ("startIndex", "0"),
   6930                 ("resultsPerPage", "2000"),
   6931             ]
   6932             .into_iter()
   6933             .map(|(name, value)| NvdQueryTrace {
   6934                 name: name.to_owned(),
   6935                 value: value.to_owned(),
   6936             })
   6937             .collect(),
   6938             response_status: 200,
   6939             response_byte_length: 128,
   6940             response_sha256: "5".repeat(64),
   6941             response_start_index: 0,
   6942             response_results_per_page: 2_000,
   6943             response_total_results: 1,
   6944         }],
   6945     }
   6946 }
   6947 
   6948 fn synthetic_producer_request(candidate: &CandidateIdentity) -> TrustedProducerRequest {
   6949     TrustedProducerRequest {
   6950         schema: "radroots.advisory-producer-request.v1".to_owned(),
   6951         candidate_generation: candidate.generation,
   6952         candidate_digest: candidate.digest.clone(),
   6953         platform: "macos_aarch64".to_owned(),
   6954         process_contract_sha256: BOUNDED_PROCESS_DECISION_SHA256.to_owned(),
   6955         process_runner_source_sha256: BOUNDED_PROCESS_SOURCE_SHA256.to_owned(),
   6956         nvd_endpoint: "/rest/json/cves/2.0".to_owned(),
   6957         nvd_enforcement_program_sha256: "7".repeat(64),
   6958         nvd_enforcement_configuration_sha256: "8".repeat(64),
   6959         nvd_query_keys: [
   6960             "lastModStartDate",
   6961             "lastModEndDate",
   6962             "startIndex",
   6963             "resultsPerPage",
   6964         ]
   6965         .into_iter()
   6966         .map(str::to_owned)
   6967         .collect(),
   6968         workload_ids: WORKLOADS
   6969             .iter()
   6970             .map(|workload| workload.id.to_owned())
   6971             .collect(),
   6972     }
   6973 }
   6974 
   6975 #[allow(clippy::too_many_arguments)]
   6976 fn synthetic_process_receipt(
   6977     id: &str,
   6978     program_sha256: &str,
   6979     arguments: &[String],
   6980     input_sha256: &str,
   6981     output_sha256: &str,
   6982     path_binding: Vec<ProcessPathBinding>,
   6983     started_at_epoch: u64,
   6984     completed_at_epoch: u64,
   6985     exit_code: i32,
   6986 ) -> Result<TrustedProcessReceipt, AdvisoryError> {
   6987     let environment = expected_process_environment(id)?;
   6988     let working_directory = expected_process_working_directory(id)?;
   6989     Ok(TrustedProcessReceipt {
   6990         id: id.to_owned(),
   6991         state: OperationState::Complete,
   6992         program_sha256: program_sha256.to_owned(),
   6993         runtime_sha256: expected_runtime_sha256(id),
   6994         arguments_sha256: domain_json_digest(
   6995             b"radroots-advisory-process-arguments-v1\0",
   6996             arguments,
   6997         )?,
   6998         environment_sha256: process_environment_digest(&environment)?,
   6999         environment,
   7000         working_directory_sha256: process_working_directory_digest(&working_directory)?,
   7001         working_directory,
   7002         path_binding,
   7003         stdin_closed: true,
   7004         deadline_seconds: ANALYSIS_DEADLINE_SECONDS,
   7005         started_at_epoch,
   7006         completed_at_epoch,
   7007         exit_code,
   7008         stdout_byte_length: 0,
   7009         stdout_sha256: sha256(&[]),
   7010         stderr_byte_length: 0,
   7011         stderr_sha256: sha256(&[]),
   7012         input_sha256: input_sha256.to_owned(),
   7013         output_sha256: output_sha256.to_owned(),
   7014     })
   7015 }
   7016 
   7017 fn attach_gradle_receipts(
   7018     projections: &mut [GradleProjection],
   7019     receipts: &mut Vec<TrustedProcessReceipt>,
   7020     evaluation_epoch: u64,
   7021 ) -> Result<(), AdvisoryError> {
   7022     for (index, projection) in projections.iter_mut().enumerate() {
   7023         let receipt = synthetic_process_receipt(
   7024             &format!("gradle:{}", projection.workload_id),
   7025             TOOL_PINS[3].executable_sha256,
   7026             &projection.wrapper_arguments,
   7027             &gradle_process_input_digest(projection)?,
   7028             &gradle_process_output_digest(projection)?,
   7029             vec![
   7030                 ProcessPathBinding {
   7031                     argument_index: 5,
   7032                     logical_role: "gradle_build_root".to_owned(),
   7033                     identity_sha256: gradle_build_root_binding_digest(projection)?,
   7034                 },
   7035                 ProcessPathBinding {
   7036                     argument_index: 7,
   7037                     logical_role: "gradle_init_script".to_owned(),
   7038                     identity_sha256: projection.init_script_sha256.clone(),
   7039                 },
   7040                 ProcessPathBinding {
   7041                     argument_index: 10,
   7042                     logical_role: "gradle_projection_output".to_owned(),
   7043                     identity_sha256: projection.raw_graph_sha256.clone(),
   7044                 },
   7045             ],
   7046             evaluation_epoch - 2_800 + index as u64 * 10,
   7047             evaluation_epoch - 2_795 + index as u64 * 10,
   7048             0,
   7049         )?;
   7050         projection.environment_sha256 = receipt.environment_sha256.clone();
   7051         projection.process_receipt_sha256 = process_receipt_digest(&receipt)?;
   7052         receipts.push(receipt);
   7053     }
   7054     Ok(())
   7055 }
   7056 
   7057 fn synthetic_gradle_projections(
   7058     admitted_graphs: &[AdmittedGradleGraph],
   7059     inventory: &[WorkloadInventory],
   7060     sources: &[SourceIdentity],
   7061 ) -> Result<Vec<GradleProjection>, AdvisoryError> {
   7062     let source = sources
   7063         .iter()
   7064         .find(|source| source.repository == "oss/harvestcircle")
   7065         .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InventoryMismatch))?;
   7066     WORKLOADS
   7067         .iter()
   7068         .enumerate()
   7069         .filter(|(_, workload)| workload.package_manager == "gradle")
   7070         .zip(admitted_graphs)
   7071         .map(|((index, workload), admitted)| {
   7072             let inventory = inventory
   7073                 .iter()
   7074                 .find(|row| row.id == workload.id)
   7075                 .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InventoryMismatch))?;
   7076             admitted.revalidate()?;
   7077             Ok(GradleProjection {
   7078                 workload_id: workload.id.to_owned(),
   7079                 state: OperationState::Complete,
   7080                 raw_graph_byte_length: admitted.raw.byte_length,
   7081                 raw_graph_sha256: admitted.raw.sha256.clone(),
   7082                 init_script_sha256: GRADLE_INIT_SCRIPT_SHA256.to_owned(),
   7083                 wrapper_arguments: expected_gradle_arguments(workload)?,
   7084                 environment_keys: [
   7085                     "GRADLE_USER_HOME",
   7086                     "HOME",
   7087                     "JAVA_HOME",
   7088                     "LC_ALL",
   7089                     "PATH",
   7090                     "TMPDIR",
   7091                     "TZ",
   7092                 ]
   7093                 .into_iter()
   7094                 .map(str::to_owned)
   7095                 .collect(),
   7096                 environment_sha256: format!("{:064x}", index + 4_000),
   7097                 exit_code: 0,
   7098                 source_revision: source.revision.clone(),
   7099                 source_tree: source.tree.clone(),
   7100                 input_sha256: inventory.input_sha256.clone(),
   7101                 dependency_count: inventory.dependency_count,
   7102                 component_count: admitted.components.len() as u64,
   7103                 edge_count: admitted.edges.len() as u64,
   7104                 artifact_count: admitted.artifacts.len() as u64,
   7105                 components: admitted.components.clone(),
   7106                 edges: admitted.edges.clone(),
   7107                 artifacts: admitted.artifacts.clone(),
   7108                 canonical_graph_sha256: admitted.canonical_graph_sha256.clone(),
   7109                 materialized_tree_sha256: admitted.materialized_tree_sha256.clone(),
   7110                 normalization_receipt_sha256: admitted.normalization_receipt_sha256.clone(),
   7111                 artifact_source_roots_sha256: admitted.artifact_source_roots_sha256.clone(),
   7112                 seed_cache_inventory_sha256: format!("{:064x}", index + 6_000),
   7113                 wrapper_distribution_sha256:
   7114                     "553c78f50dafcd54d65b9a444649057857469edf836431389695608536d6b746".to_owned(),
   7115                 process_receipt_sha256: format!("{:064x}", index + 7_000),
   7116             })
   7117         })
   7118         .collect()
   7119 }
   7120 
   7121 #[allow(clippy::too_many_arguments)]
   7122 fn synthetic_report(
   7123     provider: ProviderId,
   7124     inventory: &[WorkloadInventory],
   7125     gradle_projections: &[GradleProjection],
   7126     archive_sha256: &str,
   7127     materialized_tree_sha256: &str,
   7128     tool_observation_sha256: &str,
   7129     receipts: &mut Vec<TrustedProcessReceipt>,
   7130     evaluation_epoch: u64,
   7131 ) -> Result<Vec<u8>, AdvisoryError> {
   7132     let mut workload_result = Vec::new();
   7133     for workload in WORKLOADS.iter().filter(|workload| match provider {
   7134         ProviderId::Rustsec => workload.package_manager == "cargo",
   7135         ProviderId::OwaspNvd => workload.package_manager == "gradle",
   7136     }) {
   7137         let inventory = inventory
   7138             .iter()
   7139             .find(|row| row.id == workload.id)
   7140             .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InventoryMismatch))?;
   7141         let raw = match provider {
   7142             ProviderId::Rustsec => serde_json::json!({
   7143                 "database": {
   7144                     "advisory-count": 1,
   7145                     "last-commit": "6".repeat(40),
   7146                     "last-updated": format_report_epoch(evaluation_epoch - 2_900)?
   7147                 },
   7148                 "lockfile": {"dependency-count": inventory.dependency_count},
   7149                 "settings": {
   7150                     "ignore": [],
   7151                     "informational_warnings": ["unmaintained", "unsound", "notice"],
   7152                     "severity": null,
   7153                     "target_arch": [],
   7154                     "target_os": []
   7155                 },
   7156                 "vulnerabilities": {"count": 0, "found": false, "list": []},
   7157                 "warnings": {}
   7158             }),
   7159             ProviderId::OwaspNvd => {
   7160                 let projection = gradle_projections
   7161                     .iter()
   7162                     .find(|projection| projection.workload_id == workload.id)
   7163                     .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InventoryMismatch))?;
   7164                 let dependencies = projection
   7165                     .artifacts
   7166                     .iter()
   7167                     .map(|artifact| {
   7168                         serde_json::json!({
   7169                             "evidenceCollected": {
   7170                                 "productEvidence": [],
   7171                                 "vendorEvidence": [],
   7172                                 "versionEvidence": []
   7173                             },
   7174                             "fileName": artifact.materialized_name,
   7175                             "filePath": format!(
   7176                                 "/private/advisory/scan/{}/{}",
   7177                                 workload.id,
   7178                                 artifact.materialized_name,
   7179                             ),
   7180                             "isVirtual": false,
   7181                             "md5": "0".repeat(32),
   7182                             "packages": [{
   7183                                 "id": format!(
   7184                                     "pkg:maven/{}/{}@{}",
   7185                                     artifact.package_namespace,
   7186                                     artifact.package_name,
   7187                                     artifact.package_version,
   7188                                 )
   7189                             }],
   7190                             "sha1": "0".repeat(40),
   7191                             "sha256": artifact.artifact_sha256
   7192                         })
   7193                     })
   7194                     .collect::<Vec<_>>();
   7195                 serde_json::json!({
   7196                     "dependencies": dependencies,
   7197                     "projectInfo": {
   7198                         "credits": {
   7199                             "CISA": "This report may contain data retrieved from the CISA Known Exploited Vulnerability Catalog: https://www.cisa.gov/known-exploited-vulnerabilities-catalog",
   7200                             "NPM": "This report may contain data retrieved from the Github Advisory Database (via NPM Audit API): https://github.com/advisories/",
   7201                             "NVD": "This product uses the NVD API but is not endorsed or certified by the NVD. This report contains data retrieved from the National Vulnerability Database: https://nvd.nist.gov",
   7202                             "OSSINDEX": "This report may contain data retrieved from the Sonatype Guide OSS Index API: https://www.sonatype.com/products/sonatype-guide/oss-index-users",
   7203                             "RETIREJS": "This report may contain data retrieved from the RetireJS community: https://retirejs.github.io/retire.js/"
   7204                         },
   7205                         "name": workload.id,
   7206                         "reportDate": format_report_epoch(
   7207                             evaluation_epoch - 2_300 + workload_result.len() as u64 * 10,
   7208                         )?
   7209                     },
   7210                     "reportSchema": "1.1",
   7211                     "scanInfo": {
   7212                         "dataSource": [{
   7213                             "name": "NVD CVE Checked",
   7214                             "timestamp": format_report_epoch(evaluation_epoch - 2_900)?
   7215                         }],
   7216                         "engineVersion": "12.2.2"
   7217                     }
   7218                 })
   7219             }
   7220         };
   7221         let raw_scanner_output = serde_json::to_string(&raw)
   7222             .map_err(|_| AdvisoryError::new(AdvisoryFailureKind::InvalidReport))?;
   7223         let arguments = synthetic_scanner_arguments(provider, workload.id);
   7224         let prefix = match provider {
   7225             ProviderId::Rustsec => "cargo_audit",
   7226             ProviderId::OwaspNvd => "owasp_analysis",
   7227         };
   7228         let program_sha256 = match provider {
   7229             ProviderId::Rustsec => TOOL_PINS[0].executable_sha256,
   7230             ProviderId::OwaspNvd => TOOL_PINS[1].executable_sha256,
   7231         };
   7232         let exit_code = 0;
   7233         let database_copy = (provider == ProviderId::OwaspNvd).then(|| ScannerDatabaseCopy {
   7234             root_identity_sha256: format!("{:064x}", workload_result.len() + 30_000),
   7235             source_tree_sha256: materialized_tree_sha256.to_owned(),
   7236             pre_scan_tree_sha256: materialized_tree_sha256.to_owned(),
   7237             post_scan_tree_sha256: materialized_tree_sha256.to_owned(),
   7238         });
   7239         let path_binding = match provider {
   7240             ProviderId::Rustsec => vec![
   7241                 ProcessPathBinding {
   7242                     argument_index: 2,
   7243                     logical_role: "rustsec_database".to_owned(),
   7244                     identity_sha256: materialized_tree_sha256.to_owned(),
   7245                 },
   7246                 ProcessPathBinding {
   7247                     argument_index: 6,
   7248                     logical_role: "cargo_lock".to_owned(),
   7249                     identity_sha256: inventory.input_sha256.clone(),
   7250                 },
   7251             ],
   7252             ProviderId::OwaspNvd => {
   7253                 let projection = gradle_projections
   7254                     .iter()
   7255                     .find(|projection| projection.workload_id == workload.id)
   7256                     .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::InventoryMismatch))?;
   7257                 vec![
   7258                     ProcessPathBinding {
   7259                         argument_index: 2,
   7260                         logical_role: "nvd_database".to_owned(),
   7261                         identity_sha256: scanner_database_copy_digest(
   7262                             database_copy.as_ref().ok_or_else(|| {
   7263                                 AdvisoryError::new(AdvisoryFailureKind::InvalidSnapshot)
   7264                             })?,
   7265                         )?,
   7266                     },
   7267                     ProcessPathBinding {
   7268                         argument_index: 4,
   7269                         logical_role: "gradle_scan_projection".to_owned(),
   7270                         identity_sha256: projection.materialized_tree_sha256.clone(),
   7271                     },
   7272                     ProcessPathBinding {
   7273                         argument_index: 10,
   7274                         logical_role: "raw_report_output".to_owned(),
   7275                         identity_sha256: sha256(raw_scanner_output.as_bytes()),
   7276                     },
   7277                 ]
   7278             }
   7279         };
   7280         let timing_offset = match provider {
   7281             ProviderId::Rustsec => 2_600,
   7282             ProviderId::OwaspNvd => 2_300,
   7283         };
   7284         let started_at_epoch = evaluation_epoch - timing_offset + workload_result.len() as u64 * 10;
   7285         let mut receipt = synthetic_process_receipt(
   7286             &format!("{prefix}:{}", workload.id),
   7287             program_sha256,
   7288             &arguments,
   7289             &scanner_process_input_digest_fields(
   7290                 provider,
   7291                 workload.id,
   7292                 &inventory.input_sha256,
   7293                 inventory.dependency_count,
   7294                 archive_sha256,
   7295                 materialized_tree_sha256,
   7296                 &database_copy
   7297                     .as_ref()
   7298                     .map(scanner_database_copy_digest)
   7299                     .transpose()?
   7300                     .unwrap_or_else(|| "none".to_owned()),
   7301                 tool_observation_sha256,
   7302             )?,
   7303             &sha256(raw_scanner_output.as_bytes()),
   7304             path_binding,
   7305             started_at_epoch,
   7306             started_at_epoch + 5,
   7307             exit_code,
   7308         )?;
   7309         if provider == ProviderId::Rustsec {
   7310             receipt.stdout_byte_length = raw_scanner_output.len() as u64;
   7311             receipt.stdout_sha256 = sha256(raw_scanner_output.as_bytes());
   7312         }
   7313         let process_receipt_sha256 = process_receipt_digest(&receipt)?;
   7314         let environment_sha256 = receipt.environment_sha256.clone();
   7315         receipts.push(receipt);
   7316         workload_result.push(WorkloadResultFixture {
   7317             workload_id: workload.id.to_owned(),
   7318             input_sha256: inventory.input_sha256.clone(),
   7319             dependency_count: inventory.dependency_count,
   7320             provider_archive_sha256: archive_sha256.to_owned(),
   7321             materialized_tree_sha256: materialized_tree_sha256.to_owned(),
   7322             tool_observation_sha256: tool_observation_sha256.to_owned(),
   7323             arguments,
   7324             environment_sha256,
   7325             process_receipt_sha256,
   7326             database_copy,
   7327             exit_code,
   7328             raw_output_byte_length: raw_scanner_output.len() as u64,
   7329             raw_output_sha256: sha256(raw_scanner_output.as_bytes()),
   7330             raw_scanner_output,
   7331         });
   7332     }
   7333     let value = serde_json::json!({
   7334         "provider": provider,
   7335         "schema": "radroots.advisory-unsuppressed-report.v1",
   7336         "workload_result": workload_result
   7337     });
   7338     serde_json::from_value::<ReportEnvelope>(value.clone())
   7339         .map_err(|_| AdvisoryError::new(AdvisoryFailureKind::InvalidReport))?;
   7340     Ok(canonical_json(&value))
   7341 }
   7342 
   7343 #[derive(Serialize)]
   7344 struct WorkloadResultFixture {
   7345     workload_id: String,
   7346     input_sha256: String,
   7347     dependency_count: u64,
   7348     provider_archive_sha256: String,
   7349     materialized_tree_sha256: String,
   7350     tool_observation_sha256: String,
   7351     arguments: Vec<String>,
   7352     environment_sha256: String,
   7353     process_receipt_sha256: String,
   7354     database_copy: Option<ScannerDatabaseCopy>,
   7355     exit_code: i32,
   7356     raw_output_byte_length: u64,
   7357     raw_output_sha256: String,
   7358     raw_scanner_output: String,
   7359 }
   7360 
   7361 fn synthetic_scanner_arguments(provider: ProviderId, workload_id: &str) -> Vec<String> {
   7362     match provider {
   7363         ProviderId::Rustsec => [
   7364             "audit".to_owned(),
   7365             "--db".to_owned(),
   7366             "/private/advisory/rustsec-db".to_owned(),
   7367             "--no-fetch".to_owned(),
   7368             "--json".to_owned(),
   7369             "--file".to_owned(),
   7370             format!("/private/advisory/locks/{workload_id}/Cargo.lock"),
   7371         ]
   7372         .into_iter()
   7373         .collect(),
   7374         ProviderId::OwaspNvd => {
   7375             let mut arguments = strings(&OWASP_OFFLINE_ARGUMENTS);
   7376             arguments[2] = "/private/advisory/nvd-data".to_owned();
   7377             arguments[4] = format!("/private/advisory/scan/{workload_id}");
   7378             arguments[6] = workload_id.to_owned();
   7379             arguments[10] = format!("/private/advisory/report/{workload_id}");
   7380             arguments
   7381         }
   7382     }
   7383 }
   7384 
   7385 fn set_raw_output(result: &mut Value, raw: &Value, exit_code: i32) -> Result<(), AdvisoryError> {
   7386     let raw = serde_json::to_string(raw)
   7387         .map_err(|_| AdvisoryError::new(AdvisoryFailureKind::InvalidReport))?;
   7388     result["exit_code"] = Value::from(exit_code);
   7389     result["raw_output_byte_length"] = Value::from(raw.len() as u64);
   7390     result["raw_output_sha256"] = Value::from(sha256(raw.as_bytes()));
   7391     result["raw_scanner_output"] = Value::from(raw);
   7392     Ok(())
   7393 }
   7394 
   7395 fn synthetic_archive_tree_digest(
   7396     archive: &[u8],
   7397     materialization_parent: &Path,
   7398 ) -> Result<(String, ArchiveEvidence), AdvisoryError> {
   7399     let source = tempfile::tempdir()
   7400         .map_err(|_| AdvisoryError::new(AdvisoryFailureKind::InvalidSnapshot))?;
   7401     std::fs::write(source.path().join("snapshot.tar.gz"), archive)
   7402         .map_err(|_| AdvisoryError::new(AdvisoryFailureKind::InvalidSnapshot))?;
   7403     let snapshot = safe_artifact_io::traverse_regular_files(
   7404         source.path(),
   7405         TraversalLimits {
   7406             max_entries: 1,
   7407             max_files: 1,
   7408             max_total_bytes: archive.len() as u64,
   7409             max_file_bytes: archive.len() as u64,
   7410             max_depth: 1,
   7411             max_path_bytes: 64,
   7412         },
   7413         &[],
   7414     )
   7415     .map_err(map_archive_error)?;
   7416     let file = snapshot
   7417         .files()
   7418         .first()
   7419         .ok_or_else(|| AdvisoryError::new(AdvisoryFailureKind::ArchiveRejected))?;
   7420     let materialized = snapshot
   7421         .materialize_deterministic_tar_gzip(file, materialization_parent, archive_limits())
   7422         .map_err(map_archive_error)?;
   7423     let digest = materialized_tree_digest(materialized.snapshot())?;
   7424     let evidence = materialized.evidence().clone();
   7425     materialized.revalidate().map_err(map_binding_error)?;
   7426     snapshot.revalidate().map_err(map_binding_error)?;
   7427     Ok((digest, evidence))
   7428 }
   7429 
   7430 fn deterministic_archive(payload: &[u8]) -> Result<Vec<u8>, AdvisoryError> {
   7431     use flate2::{Compression, GzBuilder};
   7432     use tar::{Builder, EntryType, Header};
   7433 
   7434     let encoder = GzBuilder::new()
   7435         .mtime(0)
   7436         .operating_system(255)
   7437         .write(Vec::new(), Compression::new(6));
   7438     let mut builder = Builder::new(encoder);
   7439     let mut directory = Header::new_gnu();
   7440     directory
   7441         .set_path("snapshot/")
   7442         .map_err(|_| AdvisoryError::new(AdvisoryFailureKind::InvalidSnapshot))?;
   7443     directory.set_entry_type(EntryType::Directory);
   7444     directory.set_mode(0o755);
   7445     directory.set_uid(0);
   7446     directory.set_gid(0);
   7447     directory.set_mtime(0);
   7448     directory.set_size(0);
   7449     directory.set_cksum();
   7450     builder
   7451         .append(&directory, &[][..])
   7452         .map_err(|_| AdvisoryError::new(AdvisoryFailureKind::InvalidSnapshot))?;
   7453     let mut header = Header::new_gnu();
   7454     header
   7455         .set_path("snapshot/data.json")
   7456         .map_err(|_| AdvisoryError::new(AdvisoryFailureKind::InvalidSnapshot))?;
   7457     header.set_entry_type(EntryType::Regular);
   7458     header.set_mode(0o644);
   7459     header.set_uid(0);
   7460     header.set_gid(0);
   7461     header.set_mtime(0);
   7462     header.set_size(payload.len() as u64);
   7463     header.set_cksum();
   7464     builder
   7465         .append(&header, payload)
   7466         .map_err(|_| AdvisoryError::new(AdvisoryFailureKind::InvalidSnapshot))?;
   7467     let encoder = builder
   7468         .into_inner()
   7469         .map_err(|_| AdvisoryError::new(AdvisoryFailureKind::InvalidSnapshot))?;
   7470     encoder
   7471         .finish()
   7472         .map_err(|_| AdvisoryError::new(AdvisoryFailureKind::InvalidSnapshot))
   7473 }
   7474 
   7475 fn trusted_tempdir(prefix: &str) -> Result<tempfile::TempDir, AdvisoryError> {
   7476     let current = std::env::current_dir()
   7477         .map_err(|_| AdvisoryError::new(AdvisoryFailureKind::Unavailable))?;
   7478     tempfile::Builder::new()
   7479         .prefix(prefix)
   7480         .tempdir_in(current)
   7481         .map_err(|_| AdvisoryError::new(AdvisoryFailureKind::Unavailable))
   7482 }
   7483 
   7484 fn set_private_directory_mode(path: &Path) -> Result<(), AdvisoryError> {
   7485     #[cfg(unix)]
   7486     {
   7487         use std::os::unix::fs::PermissionsExt as _;
   7488         std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o700))
   7489             .map_err(|_| AdvisoryError::new(AdvisoryFailureKind::Unavailable))
   7490     }
   7491     #[cfg(not(unix))]
   7492     {
   7493         let _ = path;
   7494         Err(AdvisoryError::new(AdvisoryFailureKind::Unavailable))
   7495     }
   7496 }
   7497 
   7498 fn set_private_file_mode(path: &Path) -> Result<(), AdvisoryError> {
   7499     #[cfg(unix)]
   7500     {
   7501         use std::os::unix::fs::PermissionsExt as _;
   7502         std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600))
   7503             .map_err(|_| AdvisoryError::new(AdvisoryFailureKind::Unavailable))
   7504     }
   7505     #[cfg(not(unix))]
   7506     {
   7507         let _ = path;
   7508         Err(AdvisoryError::new(AdvisoryFailureKind::Unavailable))
   7509     }
   7510 }
   7511 
   7512 #[cfg(test)]
   7513 mod tests {
   7514     use super::*;
   7515 
   7516     #[test]
   7517     fn known_vulnerable_fixture() {
   7518         let fixture = SyntheticFixture::new().expect("construct baseline advisory fixture");
   7519         let provider: TrustedProviderEvidence =
   7520             parse_canonical_authority(&fixture.request.provider_execution_evidence)
   7521                 .expect("parse provider evidence");
   7522         validate_process_receipts(&provider.process_receipt, fixture.request.evaluation_epoch)
   7523             .expect("validate process receipts");
   7524         validate_trusted_authority(&fixture.request).expect("validate trusted authority");
   7525         validate_request(&fixture.request).expect("validate baseline request");
   7526         validate_manifest(&fixture.manifest, &fixture.request).expect("validate baseline manifest");
   7527         admit_snapshot(
   7528             fixture.root(),
   7529             fixture.materialization_parent.path(),
   7530             &fixture.request,
   7531         )
   7532         .expect("admit baseline advisory fixture");
   7533 
   7534         let mut fixture = SyntheticFixture::new().expect("construct vulnerable fixture");
   7535         fixture
   7536             .set_rustsec_finding(NON_WAIVABLE_RUSTSEC, "lru", "0.16.4")
   7537             .expect("install known vulnerability");
   7538         let error = admit_snapshot(
   7539             fixture.root(),
   7540             fixture.materialization_parent.path(),
   7541             &fixture.request,
   7542         )
   7543         .expect_err("known vulnerability must fail closed");
   7544         assert_eq!(error.kind(), AdvisoryFailureKind::KnownVulnerability);
   7545     }
   7546 
   7547     #[test]
   7548     fn missing_inventory_is_rejected() {
   7549         missing_inventory_vector().expect("missing inventory must fail closed");
   7550     }
   7551 
   7552     #[test]
   7553     fn unavailable_provider_is_nonpass() {
   7554         unavailable_provider_vector().expect("unavailable provider must be non-pass");
   7555     }
   7556 
   7557     #[test]
   7558     fn stale_snapshot_is_rejected() {
   7559         stale_snapshot_vector().expect("stale snapshot must fail closed");
   7560     }
   7561 
   7562     #[test]
   7563     fn timed_out_operation_is_nonpass() {
   7564         timed_out_operation_vector().expect("timed-out operation must be non-pass");
   7565     }
   7566 
   7567     #[test]
   7568     fn expired_suppression_is_rejected() {
   7569         expired_suppression_vector().expect("expired suppression must fail closed");
   7570     }
   7571 }
   7572 
   7573 #[cfg(test)]
   7574 mod step_296_tests {
   7575     use super::*;
   7576 
   7577     #[test]
   7578     fn checked_in_semantically_ordered_decision_is_accepted() {
   7579         assert!(DECISION_BYTES.starts_with(b"{\n  \"schema\""));
   7580         let root = Path::new(env!("CARGO_MANIFEST_DIR"))
   7581             .parent()
   7582             .and_then(Path::parent)
   7583             .expect("xtask manifest must be beneath the workspace root");
   7584         validate_decision(root).expect("checked-in advisory decision must be accepted");
   7585     }
   7586 }
   7587 
   7588 #[cfg(test)]
   7589 mod admission_boundary_tests {
   7590     use super::*;
   7591 
   7592     #[test]
   7593     fn snapshot_inventory_rejects_missing_nonfile_and_unrecognized_members() {
   7594         for case in ["missing", "directory", "unknown"] {
   7595             let directory = trusted_tempdir("radroots-snapshot-inventory-").unwrap();
   7596             for name in SNAPSHOT_FILE_NAMES {
   7597                 std::fs::write(directory.path().join(name), b"{}").unwrap();
   7598             }
   7599             let limits = TraversalLimits {
   7600                 max_entries: 16,
   7601                 max_files: 16,
   7602                 max_total_bytes: 1024,
   7603                 max_file_bytes: 1024,
   7604                 max_depth: 2,
   7605                 max_path_bytes: 128,
   7606             };
   7607             let original =
   7608                 safe_artifact_io::traverse_regular_files(directory.path(), limits, &[]).unwrap();
   7609             exact_snapshot_files(&original).unwrap();
   7610             let member = directory.path().join(MANIFEST_NAME);
   7611             match case {
   7612                 "missing" => std::fs::remove_file(member).unwrap(),
   7613                 "directory" => {
   7614                     std::fs::remove_file(&member).unwrap();
   7615                     std::fs::create_dir(member).unwrap();
   7616                 }
   7617                 "unknown" => {
   7618                     std::fs::rename(member, directory.path().join("unknown.json")).unwrap()
   7619                 }
   7620                 _ => unreachable!(),
   7621             }
   7622             let changed =
   7623                 safe_artifact_io::traverse_regular_files(directory.path(), limits, &[]).unwrap();
   7624             assert!(exact_snapshot_files(&changed).is_err(), "{case}");
   7625         }
   7626         let mut binding = blob(RUSTSEC_REPORT_NAME, b"{}").unwrap();
   7627         assert!(valid_blob_binding(&binding));
   7628         binding.path = "other.json".into();
   7629         assert!(!valid_blob_binding(&binding));
   7630     }
   7631 
   7632     #[test]
   7633     fn reports_bind_one_database_across_all_workloads_and_the_exact_network_trace() {
   7634         let fixture = SyntheticFixture::new().unwrap();
   7635         for provider in &fixture.manifest.provider_snapshot {
   7636             let original = fixture.read_report_value(provider.provider).unwrap();
   7637             let check = |value: &Value| {
   7638                 database_identity_from_report(
   7639                     provider.provider,
   7640                     &canonical_json(value),
   7641                     &provider.materialized_tree_sha256,
   7642                 )
   7643             };
   7644             assert_eq!(check(&original).unwrap(), provider.database_identity_sha256);
   7645             let mut empty = original.clone();
   7646             empty["workload_result"] = json!([]);
   7647             assert!(check(&empty).is_err());
   7648             let mut wrong_provider = original.clone();
   7649             wrong_provider["provider"] = json!(if provider.provider == ProviderId::Rustsec {
   7650                 "owasp_nvd"
   7651             } else {
   7652                 "rustsec"
   7653             });
   7654             assert!(check(&wrong_provider).is_err());
   7655             let mut changed = original.clone();
   7656             let mut second = original["workload_result"][0].clone();
   7657             let mut raw: Value =
   7658                 serde_json::from_str(second["raw_scanner_output"].as_str().unwrap()).unwrap();
   7659             match provider.provider {
   7660                 ProviderId::Rustsec => raw["database"]["last-commit"] = json!("f".repeat(40)),
   7661                 ProviderId::OwaspNvd => {
   7662                     raw["scanInfo"]["dataSource"][0]["name"] = json!("NVD CVE substituted")
   7663                 }
   7664             }
   7665             second["raw_scanner_output"] = json!(String::from_utf8(canonical_json(&raw)).unwrap());
   7666             changed["workload_result"]
   7667                 .as_array_mut()
   7668                 .unwrap()
   7669                 .push(second);
   7670             assert_eq!(
   7671                 check(&changed).unwrap_err().kind(),
   7672                 AdvisoryFailureKind::BindingChanged
   7673             );
   7674         }
   7675         let provider = &fixture.manifest.provider_snapshot[1];
   7676         let trace: NvdNetworkTrace =
   7677             parse_canonical_authority(&fixture.request.nvd_network_trace).unwrap();
   7678         validate_provider(
   7679             provider,
   7680             &fixture.request,
   7681             &trace,
   7682             &fixture.process_receipts,
   7683         )
   7684         .unwrap();
   7685         let mut changed = provider.clone();
   7686         changed.network_trace_sha256 = "f".repeat(64);
   7687         assert_eq!(
   7688             validate_provider(
   7689                 &changed,
   7690                 &fixture.request,
   7691                 &trace,
   7692                 &fixture.process_receipts
   7693             )
   7694             .unwrap_err()
   7695             .kind(),
   7696             AdvisoryFailureKind::BindingChanged
   7697         );
   7698         let mut changed_trace = trace;
   7699         changed_trace.producer_request_sha256 = "f".repeat(64);
   7700         assert_eq!(
   7701             validate_provider(
   7702                 provider,
   7703                 &fixture.request,
   7704                 &changed_trace,
   7705                 &fixture.process_receipts
   7706             )
   7707             .unwrap_err()
   7708             .kind(),
   7709             AdvisoryFailureKind::BindingChanged
   7710         );
   7711     }
   7712 
   7713     #[test]
   7714     fn related_artifact_digests_and_exact_package_tokens_are_bound() {
   7715         let fixture = SyntheticFixture::new().unwrap();
   7716         let artifact = &fixture.manifest.gradle_projection[0].artifacts[0];
   7717         let expected = BTreeMap::from([(artifact.materialized_name.as_str(), vec![artifact])]);
   7718         let original = json!([{"fileName":artifact.materialized_name,"filePath":format!("/fixture/{}",artifact.materialized_name),"isVirtual":false,
   7719             "md5":"1".repeat(32),"sha1":"2".repeat(40),"sha256":artifact.artifact_sha256}]);
   7720         parse_owasp_related(Some(&original), &expected).unwrap();
   7721         let mut changed = original.clone();
   7722         changed[0]["sha256"] = json!("f".repeat(64));
   7723         assert!(parse_owasp_related(Some(&changed), &expected).is_err());
   7724         let id = format!(
   7725             "pkg:maven/{}/{}@{}",
   7726             artifact.package_namespace, artifact.package_name, artifact.package_version
   7727         );
   7728         assert!(package_identifier_compatible(&id, artifact));
   7729         let mut changed = artifact.clone();
   7730         changed.package_ecosystem = "cargo".into();
   7731         assert!(!package_identifier_compatible(&id, &changed));
   7732         changed = artifact.clone();
   7733         changed.package_namespace = "has space".into();
   7734         assert!(!package_identifier_compatible(&id, &changed));
   7735         let package = json!({"name":"example","version":"1.0.0","checksum":null,"replace":null,"source":null});
   7736         validate_rustsec_package(&package).unwrap();
   7737         for field in ["name", "version"] {
   7738             let mut changed = package.clone();
   7739             changed[field] = json!("has space");
   7740             assert!(validate_rustsec_package(&changed).is_err());
   7741         }
   7742         assert!(!valid_rustsec_dependency(
   7743             &json!({"name":"example","version":"1.0.0","source":null,"unexpected":true})
   7744         ));
   7745         assert!(!valid_text_allow_empty("too long", 3));
   7746     }
   7747 
   7748     #[test]
   7749     fn artifact_source_roots_and_project_versions_have_closed_identity() {
   7750         let digest = "1".repeat(64);
   7751         let make = |role| GradleArtifactSourceRoot {
   7752             path: Path::new("/fixture"),
   7753             logical_role: role,
   7754             identity_sha256: &digest,
   7755         };
   7756         artifact_source_roots_digest(&[
   7757             make("candidate_build_output"),
   7758             make("governed_seed_cache"),
   7759         ])
   7760         .unwrap();
   7761         for roots in [
   7762             vec![],
   7763             vec![
   7764                 make("candidate_build_output"),
   7765                 make("governed_seed_cache"),
   7766                 make("candidate_build_output"),
   7767             ],
   7768             vec![make("unknown")],
   7769             vec![
   7770                 make("candidate_build_output"),
   7771                 make("candidate_build_output"),
   7772             ],
   7773             vec![GradleArtifactSourceRoot {
   7774                 path: Path::new("relative"),
   7775                 ..make("candidate_build_output")
   7776             }],
   7777             vec![GradleArtifactSourceRoot {
   7778                 identity_sha256: "invalid",
   7779                 ..make("candidate_build_output")
   7780             }],
   7781         ] {
   7782             assert!(artifact_source_roots_digest(&roots).is_err());
   7783         }
   7784         let version = RawGradleModuleVersion {
   7785             group: "harvestcircle.app".into(),
   7786             name: "shared".into(),
   7787             version: "unspecified".into(),
   7788         };
   7789         assert!(valid_project_module_version(
   7790             ".",
   7791             ":app:shared",
   7792             &version,
   7793             &"1".repeat(40)
   7794         ));
   7795         assert!(!valid_project_module_version(
   7796             ".",
   7797             ":app:shared",
   7798             &version,
   7799             "invalid"
   7800         ));
   7801         assert!(!valid_project_module_version(
   7802             ".",
   7803             ":app:other",
   7804             &version,
   7805             &"1".repeat(40)
   7806         ));
   7807         assert!(!valid_gradle_project_path(":app:"));
   7808         for value in ["CVE-2026", "CVE-26-1234", "CVE-2026-123"] {
   7809             assert!(!valid_advisory_id(ProviderId::OwaspNvd, value));
   7810         }
   7811         for value in ["a\0b", "a\nb"] {
   7812             assert!(!valid_text_allow_empty(value, 128));
   7813         }
   7814         assert!(parse_report_epoch("2026-04-01T00:00:00Z").is_ok());
   7815         assert_eq!(
   7816             format_report_epoch(parse_report_epoch("2026-01-01T00:00:00Z").unwrap()).unwrap(),
   7817             "2026-01-01T00:00:00Z"
   7818         );
   7819         assert!(parse_exact_package_url("pkg:maven/example/bad name@1.0").is_err());
   7820         assert!(!valid_owasp_software_identifier(""));
   7821     }
   7822 
   7823     #[test]
   7824     fn process_environment_is_private_per_attempt_and_shared_tools_are_consistent() {
   7825         let id = "gradle:harvestcircle.android.app.debugRuntimeClasspath";
   7826         let fixture = SyntheticFixture::new().unwrap();
   7827         let original = &fixture
   7828             .process_receipts
   7829             .iter()
   7830             .find(|row| row.id.starts_with("gradle:"))
   7831             .unwrap()
   7832             .environment;
   7833         let validate = |environment: &[ProcessEnvironmentRow]| {
   7834             validate_process_environment(
   7835                 id,
   7836                 environment,
   7837                 &mut BTreeSet::new(),
   7838                 &mut BTreeMap::new(),
   7839             )
   7840         };
   7841         validate(original).unwrap();
   7842         assert!(validate(&original[..original.len() - 1]).is_err());
   7843         for name in ["LC_ALL", "TZ"] {
   7844             let mut changed = original.clone();
   7845             changed
   7846                 .iter_mut()
   7847                 .find(|row| row.name == name)
   7848                 .unwrap()
   7849                 .value_sha256 = "f".repeat(64);
   7850             assert!(validate(&changed).is_err(), "{name}");
   7851         }
   7852         let mut changed = original.clone();
   7853         let home = changed
   7854             .iter()
   7855             .find(|row| row.name == "HOME")
   7856             .unwrap()
   7857             .value_sha256
   7858             .clone();
   7859         changed
   7860             .iter_mut()
   7861             .find(|row| row.name == "TMPDIR")
   7862             .unwrap()
   7863             .value_sha256 = home;
   7864         assert!(validate(&changed).is_err());
   7865         let mut shared = BTreeMap::from([("PATH".into(), "f".repeat(64))]);
   7866         assert!(
   7867             validate_process_environment(id, original, &mut BTreeSet::new(), &mut shared).is_err()
   7868         );
   7869         let mut receipts = fixture.process_receipts.clone();
   7870         receipts[1].working_directory.identity_sha256 =
   7871             receipts[0].working_directory.identity_sha256.clone();
   7872         receipts[1].working_directory_sha256 =
   7873             process_working_directory_digest(&receipts[1].working_directory).unwrap();
   7874         assert!(validate_process_receipts(&receipts, fixture.request.evaluation_epoch).is_err());
   7875         let mut receipts = fixture.process_receipts.clone();
   7876         receipts[0].working_directory.identity_sha256 = receipts[0]
   7877             .environment
   7878             .iter()
   7879             .find(|row| row.name == "HOME")
   7880             .unwrap()
   7881             .value_sha256
   7882             .clone();
   7883         receipts[0].working_directory_sha256 =
   7884             process_working_directory_digest(&receipts[0].working_directory).unwrap();
   7885         assert!(validate_process_receipts(&receipts, fixture.request.evaluation_epoch).is_err());
   7886     }
   7887 
   7888     #[test]
   7889     fn gradle_projection_bindings_reject_independent_metadata_and_receipt_substitution() {
   7890         let fixture = SyntheticFixture::new().unwrap();
   7891         let original = &fixture.manifest.gradle_projection[0];
   7892         let authority = WORKLOADS
   7893             .iter()
   7894             .find(|row| row.id == original.workload_id)
   7895             .unwrap();
   7896         let inventory = fixture
   7897             .request
   7898             .inventory
   7899             .iter()
   7900             .find(|row| row.id == original.workload_id)
   7901             .unwrap();
   7902         let source = fixture
   7903             .request
   7904             .sources
   7905             .iter()
   7906             .find(|row| row.repository == "oss/harvestcircle")
   7907             .unwrap();
   7908         let receipt = process_receipt(
   7909             &fixture.process_receipts,
   7910             &format!("gradle:{}", authority.id),
   7911         )
   7912         .unwrap();
   7913         let validate = |projection: &GradleProjection, receipt: &TrustedProcessReceipt| {
   7914             validate_gradle_projection_bindings(
   7915                 projection,
   7916                 authority,
   7917                 inventory,
   7918                 source,
   7919                 &fixture.request,
   7920                 receipt,
   7921             )
   7922         };
   7923         validate(original, receipt).unwrap();
   7924         for (pointer, replacement) in [
   7925             ("/workload_id", json!("unknown")),
   7926             ("/raw_graph_byte_length", json!(0)),
   7927             ("/raw_graph_byte_length", json!(MAX_GRADLE_GRAPH_BYTES + 1)),
   7928             ("/raw_graph_sha256", json!("invalid")),
   7929             ("/init_script_sha256", json!("f".repeat(64))),
   7930             ("/wrapper_arguments", json!([])),
   7931             ("/environment_keys", json!([])),
   7932             ("/environment_sha256", json!("invalid")),
   7933             ("/exit_code", json!(1)),
   7934             ("/source_revision", json!("f".repeat(40))),
   7935             ("/source_tree", json!("f".repeat(40))),
   7936             ("/input_sha256", json!("f".repeat(64))),
   7937             ("/dependency_count", json!(0)),
   7938             ("/component_count", json!(0)),
   7939             ("/edge_count", json!(0)),
   7940             ("/artifact_count", json!(0)),
   7941             ("/materialized_tree_sha256", json!("invalid")),
   7942             ("/artifact_source_roots_sha256", json!("invalid")),
   7943             ("/seed_cache_inventory_sha256", json!("invalid")),
   7944             ("/wrapper_distribution_sha256", json!("f".repeat(64))),
   7945             ("/process_receipt_sha256", json!("f".repeat(64))),
   7946             ("/canonical_graph_sha256", json!("f".repeat(64))),
   7947             ("/normalization_receipt_sha256", json!("f".repeat(64))),
   7948         ] {
   7949             let mut value = serde_json::to_value(original).unwrap();
   7950             assert_ne!(value.pointer(pointer).unwrap(), &replacement);
   7951             *value.pointer_mut(pointer).unwrap() = replacement;
   7952             let mut projection: GradleProjection = serde_json::from_value(value).unwrap();
   7953             if [
   7954                 "/artifact_source_roots_sha256",
   7955                 "/seed_cache_inventory_sha256",
   7956                 "/wrapper_distribution_sha256",
   7957             ]
   7958             .contains(&pointer)
   7959             {
   7960                 projection.input_sha256 =
   7961                     gradle_candidate_input_digest(&fixture.request, inventory, source, &projection)
   7962                         .unwrap();
   7963             }
   7964             assert!(validate(&projection, receipt).is_err(), "{pointer}");
   7965         }
   7966         let mut empty = original.clone();
   7967         empty.components.clear();
   7968         empty.component_count = 0;
   7969         assert!(validate(&empty, receipt).is_err());
   7970         for (field, value) in [
   7971             ("program_sha256", json!("f".repeat(64))),
   7972             ("arguments_sha256", json!("f".repeat(64))),
   7973             ("environment_sha256", json!("f".repeat(64))),
   7974             ("input_sha256", json!("f".repeat(64))),
   7975             ("output_sha256", json!("f".repeat(64))),
   7976             ("path_binding", json!([])),
   7977             ("exit_code", json!(1)),
   7978         ] {
   7979             let mut changed = serde_json::to_value(receipt).unwrap();
   7980             changed[field] = value;
   7981             let changed: TrustedProcessReceipt = serde_json::from_value(changed).unwrap();
   7982             let mut projection = original.clone();
   7983             projection.process_receipt_sha256 = process_receipt_digest(&changed).unwrap();
   7984             assert!(validate(&projection, &changed).is_err(), "{field}");
   7985         }
   7986     }
   7987 
   7988     #[test]
   7989     fn raw_scanner_files_and_archive_bindings_reject_changed_bytes_and_counters() {
   7990         let directory = trusted_tempdir("radroots-scanner-boundary-").unwrap();
   7991         let path = directory.path().join(RAW_SCANNER_OUTPUT_NAME);
   7992         for bytes in [b"".as_slice(), b"invalid"] {
   7993             std::fs::write(&path, bytes).unwrap();
   7994             assert!(
   7995                 admit_raw_scanner_output(directory.path(), ProviderId::Rustsec, "fixture").is_err()
   7996             );
   7997         }
   7998         std::fs::write(&path, b"{}").unwrap();
   7999         let admitted =
   8000             admit_raw_scanner_output(directory.path(), ProviderId::Rustsec, "fixture").unwrap();
   8001         admitted.revalidate().unwrap();
   8002         std::fs::write(&path, b"{\"changed\":true}").unwrap();
   8003         assert!(admitted.revalidate().is_err());
   8004         let binding = blob(RUSTSEC_REPORT_NAME, b"{}").unwrap();
   8005         validate_blob(
   8006             &binding,
   8007             &FileEvidence {
   8008                 byte_length: 2,
   8009                 sha256: sha256(b"{}"),
   8010             },
   8011         )
   8012         .unwrap();
   8013         for evidence in [
   8014             FileEvidence {
   8015                 byte_length: 3,
   8016                 sha256: sha256(b"{}"),
   8017             },
   8018             FileEvidence {
   8019                 byte_length: 2,
   8020                 sha256: sha256(b"[]"),
   8021             },
   8022         ] {
   8023             assert!(validate_blob(&binding, &evidence).is_err());
   8024         }
   8025         for field in [
   8026             "archive_expanded_bytes",
   8027             "archive_member_count",
   8028             "archive_payload_bytes",
   8029             "materialized_tree_sha256",
   8030         ] {
   8031             let mut fixture = SyntheticFixture::new().unwrap();
   8032             let mut value = serde_json::to_value(&fixture.manifest.provider_snapshot[0]).unwrap();
   8033             value[field] = if field == "materialized_tree_sha256" {
   8034                 json!("f".repeat(64))
   8035             } else {
   8036                 json!(value[field].as_u64().unwrap() + 1)
   8037             };
   8038             fixture.manifest.provider_snapshot[0] = serde_json::from_value(value).unwrap();
   8039             fixture.seal().unwrap();
   8040             assert_eq!(
   8041                 admit_snapshot(
   8042                     fixture.root(),
   8043                     fixture.materialization_parent.path(),
   8044                     &fixture.request
   8045                 )
   8046                 .unwrap_err()
   8047                 .kind(),
   8048                 AdvisoryFailureKind::BindingChanged,
   8049                 "{field}"
   8050             );
   8051         }
   8052         let fixture = SyntheticFixture::new().unwrap();
   8053         let path = fixture.root().join(MANIFEST_NAME);
   8054         let mut bytes = std::fs::read(&path).unwrap();
   8055         bytes.push(b' ');
   8056         std::fs::write(path, bytes).unwrap();
   8057         assert_eq!(
   8058             admit_snapshot(
   8059                 fixture.root(),
   8060                 fixture.materialization_parent.path(),
   8061                 &fixture.request
   8062             )
   8063             .unwrap_err()
   8064             .kind(),
   8065             AdvisoryFailureKind::InvalidSnapshot
   8066         );
   8067     }
   8068 
   8069     #[test]
   8070     fn normalized_graph_structure_rejects_each_independent_link_and_artifact_fault() {
   8071         let fixture = SyntheticFixture::new().unwrap();
   8072         let original = &fixture.manifest.gradle_projection[0];
   8073         let authority = WORKLOADS
   8074             .iter()
   8075             .find(|row| row.id == original.workload_id)
   8076             .unwrap();
   8077         let revision = "1".repeat(40);
   8078         let validate = |projection: &GradleProjection| {
   8079             validate_gradle_graph_structure(projection, authority, &revision).is_ok()
   8080         };
   8081         rejects_typed_changes(
   8082             original,
   8083             &[
   8084                 ("/components/0/identity_sha256", json!("invalid")),
   8085                 ("/components/0/variant_sha256", json!("invalid")),
   8086                 ("/edges/0/from_identity_sha256", json!("invalid")),
   8087                 ("/edges/0/to_identity_sha256", json!("invalid")),
   8088                 ("/edges/0/requested_sha256", json!("invalid")),
   8089                 ("/edges/0/selected_variant_sha256", json!("invalid")),
   8090                 ("/edges", json!([])),
   8091                 ("/artifacts/0/component_identity_sha256", json!("invalid")),
   8092                 ("/artifacts/0/component", json!("")),
   8093                 ("/artifacts/0/package_ecosystem", json!("cargo")),
   8094                 ("/artifacts/0/package_namespace", json!("")),
   8095                 ("/artifacts/0/package_name", json!("")),
   8096                 ("/artifacts/0/package_version", json!("")),
   8097                 ("/artifacts/0/artifact_sha256", json!("invalid")),
   8098                 ("/artifacts/0/variant_sha256", json!("invalid")),
   8099                 ("/artifacts/0/byte_length", json!(0)),
   8100                 ("/artifacts/0/artifact_name", json!("")),
   8101                 ("/artifacts/0/logical_name", json!("")),
   8102                 ("/artifacts/0/artifact_type", json!("")),
   8103                 ("/artifacts/0/classifier", json!("")),
   8104                 ("/artifacts/0/extension", json!("unknown")),
   8105                 ("/artifacts/0/materialized_name", json!("invalid")),
   8106                 ("/artifacts/0/package_name", json!("wrong-binding")),
   8107             ],
   8108             validate,
   8109         );
   8110         for collection in ["components", "edges", "artifacts"] {
   8111             let mut changed = serde_json::to_value(original).unwrap();
   8112             let duplicate = changed[collection][0].clone();
   8113             changed[collection].as_array_mut().unwrap().push(duplicate);
   8114             assert!(
   8115                 !validate(&serde_json::from_value(changed).unwrap()),
   8116                 "duplicate {collection}"
   8117             );
   8118         }
   8119         let mut changed = original.clone();
   8120         changed.edges[0].to_identity_sha256 = changed.edges[0].from_identity_sha256.clone();
   8121         assert!(!validate(&changed));
   8122         let mut changed = original.clone();
   8123         // Reversing the sole edge leaves the non-root component unreachable.
   8124         changed.edges[0].from_identity_sha256 = original.edges[0].to_identity_sha256.clone();
   8125         changed.edges[0].to_identity_sha256 = original.edges[0].from_identity_sha256.clone();
   8126         assert!(!validate(&changed));
   8127         for case in 0..6 {
   8128             let mut changed = original.clone();
   8129             match case {
   8130                 0 => {
   8131                     changed.components[0].variant = json!({"selected": []});
   8132                     changed.components[0].variant_sha256 =
   8133                         gradle_variant_digest(&changed.components[0].variant).unwrap();
   8134                     changed.components[0].identity_sha256 =
   8135                         gradle_component_digest(&changed.components[0]).unwrap();
   8136                     changed
   8137                         .components
   8138                         .sort_by(|a, b| a.identity_sha256.cmp(&b.identity_sha256));
   8139                 }
   8140                 1 => {
   8141                     changed.edges[0].requested = json!({});
   8142                     changed.edges[0].requested_sha256 =
   8143                         gradle_request_digest(&changed.edges[0].requested).unwrap();
   8144                 }
   8145                 2 => {
   8146                     changed.edges[0].selected_variant = json!({});
   8147                     changed.edges[0].selected_variant_sha256 =
   8148                         gradle_variant_digest(&changed.edges[0].selected_variant).unwrap();
   8149                 }
   8150                 3 => {
   8151                     changed.artifacts[0].variant = json!({});
   8152                     changed.artifacts[0].variant_sha256 =
   8153                         gradle_variant_digest(&changed.artifacts[0].variant).unwrap();
   8154                 }
   8155                 4 => {
   8156                     changed.artifacts[0].variant["attributes"][0]["value"] =
   8157                         json!("different-selection");
   8158                     changed.artifacts[0].variant_sha256 =
   8159                         gradle_variant_digest(&changed.artifacts[0].variant).unwrap();
   8160                 }
   8161                 _ => {
   8162                     for component in &mut changed.components {
   8163                         component.root = false;
   8164                         component.identity_sha256 = gradle_component_digest(component).unwrap();
   8165                     }
   8166                     changed
   8167                         .components
   8168                         .sort_by(|a, b| a.identity_sha256.cmp(&b.identity_sha256));
   8169                 }
   8170             }
   8171             assert!(!validate(&changed), "graph structure case {case}");
   8172         }
   8173         let mut duplicates = original.clone();
   8174         let mut second = duplicates.artifacts[0].clone();
   8175         second.classifier = Some("sources".to_owned());
   8176         duplicates.artifacts.push(second);
   8177         duplicates
   8178             .artifacts
   8179             .sort_by_key(|row| canonical_row_key(row).unwrap());
   8180         assert!(
   8181             validate(&duplicates),
   8182             "same materialized bytes can support distinct artifacts"
   8183         );
   8184         duplicates.artifacts[1].byte_length += 1;
   8185         duplicates
   8186             .artifacts
   8187             .sort_by_key(|row| canonical_row_key(row).unwrap());
   8188         assert!(
   8189             !validate(&duplicates),
   8190             "one content digest cannot bind different lengths"
   8191         );
   8192     }
   8193 
   8194     #[test]
   8195     fn process_directories_bind_identity_kind_and_before_after_state() {
   8196         let fixture = SyntheticFixture::new().unwrap();
   8197         for prefix in [
   8198             "cargo_audit:",
   8199             "gradle:",
   8200             "owasp_analysis:",
   8201             "owasp_nvd_update",
   8202         ] {
   8203             let receipt = fixture
   8204                 .process_receipts
   8205                 .iter()
   8206                 .find(|row| row.id.starts_with(prefix))
   8207                 .unwrap();
   8208             let directory = &receipt.working_directory;
   8209             let mut changes = vec![
   8210                 ("/logical_uri", json!("unknown")),
   8211                 ("/kind", json!("unknown")),
   8212                 ("/identity_sha256", json!("invalid")),
   8213                 ("/pre_execution_tree_sha256", json!("invalid")),
   8214                 ("/post_execution_tree_sha256", json!("invalid")),
   8215                 (
   8216                     "/pre_execution_entry_count",
   8217                     json!(if directory.pre_execution_entry_count == 0 {
   8218                         1
   8219                     } else {
   8220                         0
   8221                     }),
   8222                 ),
   8223                 (
   8224                     "/post_execution_entry_count",
   8225                     json!(if directory.post_execution_entry_count == 0 {
   8226                         1
   8227                     } else {
   8228                         0
   8229                     }),
   8230                 ),
   8231                 ("/pre_execution_tree_sha256", json!("f".repeat(64))),
   8232             ];
   8233             if prefix != "owasp_nvd_update" {
   8234                 changes.push(("/post_execution_tree_sha256", json!("f".repeat(64))));
   8235             }
   8236             rejects_typed_changes(directory, &changes, |changed| {
   8237                 validate_process_working_directory(&receipt.id, changed).is_ok()
   8238             });
   8239         }
   8240         assert!(expected_process_working_directory("unknown").is_err());
   8241         assert!(expected_environment_logical_value("UNKNOWN").is_err());
   8242     }
   8243 
   8244     #[test]
   8245     fn report_timestamps_and_json_bounds_reject_malformed_or_oversized_values() {
   8246         assert!(parse_report_epoch("1970-01-01T00:00:00Z").is_err());
   8247         assert_eq!(parse_report_epoch("1970-01-01T00:00:01.125Z").unwrap(), 1);
   8248         for year in [2000, 2024] {
   8249             let timestamp = format!("{year}-02-29T12:30:45Z");
   8250             let epoch = parse_report_epoch(&timestamp).unwrap();
   8251             assert_eq!(format_report_epoch(epoch).unwrap(), timestamp);
   8252         }
   8253         for value in [
   8254             "1970-01-00T00:00:00Z",
   8255             "1969-01-01T00:00:00Z",
   8256             "2100-02-29T00:00:00Z",
   8257             "2023-02-29T00:00:00Z",
   8258             "2026-04-31T00:00:00Z",
   8259             "2026-13-01T00:00:00Z",
   8260             "2026-01-01T24:00:00Z",
   8261             "2026-01-01T00:60:00Z",
   8262             "2026-01-01T00:00:60Z",
   8263             "2026-01-01T00:00:00.xZ",
   8264             "2026-01-01T00:00:00,1Z",
   8265         ] {
   8266             assert!(parse_report_epoch(value).is_err(), "{value}");
   8267         }
   8268         let original = b"2026-03-01T00:00:00Z";
   8269         for index in [4, 7, 10, 13, 16, 19, 0, 5, 8, 11, 14, 17] {
   8270             let mut bytes = original.to_vec();
   8271             bytes[index] = b'x';
   8272             assert!(parse_report_epoch(std::str::from_utf8(&bytes).unwrap()).is_err());
   8273         }
   8274         validate_json_bounds(&json!({"rows": [null, true, 1, "safe"]})).unwrap();
   8275         validate_json_bounds(&json!([])).unwrap();
   8276         for value in [json!("x".repeat(1_048_577)), json!("a\nb")] {
   8277             assert!(validate_json_bounds(&value).is_err());
   8278         }
   8279         let mut deep = Value::Null;
   8280         for _ in 0..33 {
   8281             deep = Value::Array(vec![deep]);
   8282         }
   8283         assert!(validate_json_bounds(&deep).is_err());
   8284         assert!(validate_json_bounds(&Value::Array(vec![Value::Null; 65_537])).is_err());
   8285         let object = (0..1_025)
   8286             .map(|index| (format!("key{index}"), Value::Null))
   8287             .collect();
   8288         assert!(validate_json_bounds(&Value::Object(object)).is_err());
   8289         assert!(validate_json_bounds(&json!({"bad\nkey": null})).is_err());
   8290         let million = Value::Array(vec![Value::Array(vec![Value::Null; 65_536]); 16]);
   8291         assert!(validate_json_bounds(&million).is_err());
   8292         for id in ["CVE-2026", "CVE-26-0001", "CVE-2026-1"] {
   8293             assert!(!valid_advisory_id(ProviderId::OwaspNvd, id));
   8294         }
   8295     }
   8296 
   8297     #[test]
   8298     fn workload_reports_bind_raw_bytes_counts_database_and_process_receipts() {
   8299         let fixture = SyntheticFixture::new().unwrap();
   8300         for provider in &fixture.manifest.provider_snapshot {
   8301             let original = fixture.read_report_value(provider.provider).unwrap();
   8302             let envelope: ReportEnvelope = serde_json::from_value(original.clone()).unwrap();
   8303             let result = &envelope.workload_result[0];
   8304             let inventory = fixture
   8305                 .request
   8306                 .inventory
   8307                 .iter()
   8308                 .find(|row| row.id == result.workload_id)
   8309                 .unwrap();
   8310             let mut changes = vec![
   8311                 ("/raw_scanner_output", json!("")),
   8312                 (
   8313                     "/raw_scanner_output",
   8314                     json!("x".repeat(MAX_RAW_WORKLOAD_REPORT_BYTES as usize + 1)),
   8315                 ),
   8316                 ("/input_sha256", json!("f".repeat(64))),
   8317                 ("/dependency_count", json!(0)),
   8318                 ("/provider_archive_sha256", json!("f".repeat(64))),
   8319                 ("/materialized_tree_sha256", json!("f".repeat(64))),
   8320                 ("/tool_observation_sha256", json!("f".repeat(64))),
   8321                 ("/raw_output_byte_length", json!(0)),
   8322                 ("/raw_output_sha256", json!("f".repeat(64))),
   8323                 ("/environment_sha256", json!("invalid")),
   8324                 ("/environment_sha256", json!("f".repeat(64))),
   8325                 ("/process_receipt_sha256", json!("f".repeat(64))),
   8326                 ("/arguments", json!([])),
   8327                 ("/exit_code", json!(2)),
   8328             ];
   8329             if provider.provider == ProviderId::OwaspNvd {
   8330                 changes.extend([
   8331                     ("/database_copy", json!(null)),
   8332                     ("/database_copy/root_identity_sha256", json!("invalid")),
   8333                     ("/database_copy/source_tree_sha256", json!("f".repeat(64))),
   8334                     ("/database_copy/pre_scan_tree_sha256", json!("f".repeat(64))),
   8335                     (
   8336                         "/database_copy/post_scan_tree_sha256",
   8337                         json!("f".repeat(64)),
   8338                     ),
   8339                 ]);
   8340             }
   8341             validate_workload_execution(provider, result, inventory, &fixture.request).unwrap();
   8342             for (pointer, replacement) in changes {
   8343                 let mut changed = original["workload_result"][0].clone();
   8344                 *changed.pointer_mut(pointer).unwrap() = replacement;
   8345                 let changed: WorkloadResult = serde_json::from_value(changed).unwrap();
   8346                 assert!(
   8347                     validate_workload_execution(provider, &changed, inventory, &fixture.request)
   8348                         .is_err(),
   8349                     "{pointer}"
   8350                 );
   8351             }
   8352             let bytes = canonical_json(&original);
   8353             assert!(
   8354                 parse_unsuppressed_report(
   8355                     provider,
   8356                     &bytes,
   8357                     &fixture.request,
   8358                     &fixture.manifest.gradle_projection
   8359                 )
   8360                 .is_ok()
   8361             );
   8362             for (pointer, replacement) in [
   8363                 ("/schema", json!("unknown")),
   8364                 (
   8365                     "/provider",
   8366                     json!(if provider.provider == ProviderId::Rustsec {
   8367                         "owasp_nvd"
   8368                     } else {
   8369                         "rustsec"
   8370                     }),
   8371                 ),
   8372                 ("/workload_result", json!([])),
   8373                 ("/workload_result/0/workload_id", json!("unknown")),
   8374             ] {
   8375                 let mut changed = original.clone();
   8376                 *changed.pointer_mut(pointer).unwrap() = replacement;
   8377                 assert!(
   8378                     parse_unsuppressed_report(
   8379                         provider,
   8380                         &canonical_json(&changed),
   8381                         &fixture.request,
   8382                         &fixture.manifest.gradle_projection
   8383                     )
   8384                     .is_err()
   8385                 );
   8386             }
   8387             let mut noncanonical = bytes;
   8388             noncanonical.push(b' ');
   8389             assert!(
   8390                 parse_unsuppressed_report(
   8391                     provider,
   8392                     &noncanonical,
   8393                     &fixture.request,
   8394                     &fixture.manifest.gradle_projection
   8395                 )
   8396                 .is_err()
   8397             );
   8398         }
   8399     }
   8400 
   8401     #[test]
   8402     fn raw_gradle_graph_rejects_identity_variant_artifact_and_source_drift() {
   8403         let fixture = SyntheticFixture::new().unwrap();
   8404         let authority = WORKLOADS
   8405             .iter()
   8406             .find(|row| row.package_manager == "gradle")
   8407             .unwrap();
   8408         let workload_root = fixture._gradle_fixture_root.path().join(authority.id);
   8409         let raw: RawGradleGraph = serde_json::from_slice(
   8410             &std::fs::read(workload_root.join("raw").join(GRADLE_RAW_GRAPH_NAME)).unwrap(),
   8411         )
   8412         .unwrap();
   8413         let source = workload_root.join("source");
   8414         let source_identity = "a".repeat(64);
   8415         let roots = [GradleArtifactSourceRoot {
   8416             path: &source,
   8417             logical_role: "candidate_build_output",
   8418             identity_sha256: &source_identity,
   8419         }];
   8420         let revision = "1".repeat(40);
   8421         let normalize =
   8422             |raw: &RawGradleGraph| normalize_raw_gradle_graph(raw, authority, &revision, 1, &roots);
   8423         assert_eq!(normalize(&raw).unwrap().artifacts.len(), 1);
   8424         rejects_typed_changes(
   8425             &raw,
   8426             &[
   8427                 ("/schema", json!("unknown")),
   8428                 ("/workload_id", json!("unknown")),
   8429                 ("/build_root", json!("unknown")),
   8430                 ("/project_path", json!(":other")),
   8431                 ("/configuration", json!("unknown")),
   8432                 ("/components", json!([])),
   8433                 ("/edges", json!([])),
   8434                 (
   8435                     "/edges/0/selected_variant/attributes/0/value",
   8436                     json!("unselected"),
   8437                 ),
   8438                 (
   8439                     "/artifacts/0/variant/attributes/0/value",
   8440                     json!("unselected"),
   8441                 ),
   8442                 ("/artifacts/0/group", json!("other")),
   8443                 ("/artifacts/0/name", json!("other")),
   8444                 ("/artifacts/0/version", json!("2.0")),
   8445                 ("/artifacts/0/module_version/group", json!("")),
   8446                 ("/artifacts/0/module_version/name", json!("")),
   8447                 ("/artifacts/0/module_version/version", json!("")),
   8448                 ("/artifacts/0/module_version/group", json!("other")),
   8449                 ("/artifacts/0/module_version/name", json!("other")),
   8450                 ("/artifacts/0/module_version/version", json!("other")),
   8451                 ("/artifacts/0/observed_byte_length", json!(0)),
   8452                 (
   8453                     "/artifacts/0/observed_byte_length",
   8454                     json!(MAX_GRADLE_ARTIFACT_BYTES + 1),
   8455                 ),
   8456                 ("/artifacts/0/observed_byte_length", json!(1)),
   8457                 ("/artifacts/0/observed_sha256", json!("invalid")),
   8458                 ("/artifacts/0/observed_sha256", json!("0".repeat(64))),
   8459                 ("/artifacts/0/artifact_name", json!("")),
   8460                 ("/artifacts/0/artifact_name", json!("nested/unsafe.jar")),
   8461                 ("/artifacts/0/artifact_name", json!("wrong.extension")),
   8462                 ("/artifacts/0/logical_name", json!("")),
   8463                 ("/artifacts/0/artifact_type", json!("")),
   8464                 ("/artifacts/0/extension", json!("unknown")),
   8465                 ("/artifacts/0/classifier", json!("")),
   8466                 ("/artifacts/0/source_path", json!("")),
   8467                 ("/artifacts/0/source_path", json!("relative/path")),
   8468                 ("/artifacts/0/source_path", json!("/outside/root")),
   8469                 (
   8470                     "/edges/0/from",
   8471                     serde_json::to_value(&raw.edges[0].to).unwrap(),
   8472                 ),
   8473             ],
   8474             |changed| normalize(changed).is_ok(),
   8475         );
   8476         assert!(normalize_raw_gradle_graph(&raw, authority, &revision, 2, &roots).is_err());
   8477         assert!(normalize_raw_gradle_graph(&raw, &WORKLOADS[0], &revision, 1, &roots).is_err());
   8478         assert!(normalize_raw_gradle_graph(&raw, authority, &revision, 1, &[]).is_err());
   8479         for slot in ["components", "edges", "artifacts"] {
   8480             let mut changed = serde_json::to_value(&raw).unwrap();
   8481             let repeated = changed[slot][0].clone();
   8482             changed[slot].as_array_mut().unwrap().push(repeated);
   8483             assert!(normalize(&serde_json::from_value(changed).unwrap()).is_err());
   8484         }
   8485         let mut no_root = raw.clone();
   8486         for component in &mut no_root.components {
   8487             component.root = false;
   8488         }
   8489         assert!(normalize(&no_root).is_err());
   8490         let mut cycle = raw.clone();
   8491         cycle.edges[0].from = raw.edges[0].to.clone();
   8492         cycle.edges[0].to = raw.edges[0].from.clone();
   8493         assert!(normalize(&cycle).is_err());
   8494     }
   8495 
   8496     #[test]
   8497     fn normalized_gradle_components_and_artifacts_keep_their_kind_specific_identity() {
   8498         let fixture = SyntheticFixture::new().unwrap();
   8499         let projection = &fixture.manifest.gradle_projection[0];
   8500         let artifact = &projection.artifacts[0];
   8501         let module = projection
   8502             .components
   8503             .iter()
   8504             .find(|row| row.kind == "module")
   8505             .unwrap();
   8506         let project = projection
   8507             .components
   8508             .iter()
   8509             .find(|row| row.kind == "project")
   8510             .unwrap();
   8511         rejects_typed_changes(
   8512             module,
   8513             &[
   8514                 ("/group", json!(null)),
   8515                 ("/name", json!(null)),
   8516                 ("/version", json!(null)),
   8517                 ("/build_root", json!(".")),
   8518                 ("/project_path", json!(":app")),
   8519                 ("/root", json!(true)),
   8520             ],
   8521             valid_gradle_component,
   8522         );
   8523         rejects_typed_changes(
   8524             project,
   8525             &[
   8526                 ("/group", json!("extra")),
   8527                 ("/name", json!("extra")),
   8528                 ("/version", json!("extra")),
   8529                 ("/build_root", json!(null)),
   8530                 ("/project_path", json!(null)),
   8531             ],
   8532             valid_gradle_component,
   8533         );
   8534         rejects_typed_changes(
   8535             artifact,
   8536             &[
   8537                 ("/component", json!("unknown")),
   8538                 ("/package_ecosystem", json!("cargo")),
   8539                 ("/package_namespace", json!("other")),
   8540                 ("/package_name", json!("other")),
   8541                 ("/package_version", json!("other")),
   8542             ],
   8543             |changed| artifact_matches_component(changed, module, &"1".repeat(40)),
   8544         );
   8545         for missing in ["group", "name", "version"] {
   8546             let mut changed = serde_json::to_value(module).unwrap();
   8547             changed[missing] = Value::Null;
   8548             assert!(!artifact_matches_component(
   8549                 artifact,
   8550                 &serde_json::from_value(changed).unwrap(),
   8551                 &"1".repeat(40)
   8552             ));
   8553         }
   8554         let mut project_artifact = artifact.clone();
   8555         project_artifact.component = format!(
   8556             "{}:{}",
   8557             project.build_root.as_deref().unwrap(),
   8558             project.project_path.as_deref().unwrap()
   8559         );
   8560         project_artifact.package_namespace = "harvestcircle.app".to_owned();
   8561         project_artifact.package_name = "design_system".to_owned();
   8562         project_artifact.package_version = "unspecified".to_owned();
   8563         project_artifact.classifier = None;
   8564         assert!(artifact_matches_component(
   8565             &project_artifact,
   8566             project,
   8567             &"1".repeat(40)
   8568         ));
   8569         rejects_typed_changes(
   8570             &project_artifact,
   8571             &[
   8572                 ("/component", json!("unknown")),
   8573                 ("/package_ecosystem", json!("cargo")),
   8574                 ("/package_namespace", json!("other")),
   8575                 ("/classifier", json!("classified")),
   8576             ],
   8577             |changed| artifact_matches_component(changed, project, &"1".repeat(40)),
   8578         );
   8579         for missing in ["build_root", "project_path"] {
   8580             let mut changed = serde_json::to_value(project).unwrap();
   8581             changed[missing] = Value::Null;
   8582             assert!(!artifact_matches_component(
   8583                 &project_artifact,
   8584                 &serde_json::from_value(changed).unwrap(),
   8585                 &"1".repeat(40)
   8586             ));
   8587         }
   8588     }
   8589 
   8590     #[test]
   8591     fn request_requires_complete_ordered_sources_graphs_and_scanner_outputs() {
   8592         for case in 0..13 {
   8593             let mut fixture = SyntheticFixture::new().unwrap();
   8594             match case {
   8595                 0 => fixture.request.candidate.generation = 0,
   8596                 1 => fixture.request.candidate.digest = "invalid".to_owned(),
   8597                 2 => fixture.request.evaluation_epoch = 0,
   8598                 3 => {
   8599                     fixture.request.inventory.pop();
   8600                 }
   8601                 4 => {
   8602                     fixture.request.admitted_gradle_graphs.pop();
   8603                 }
   8604                 5 => {
   8605                     fixture.request.admitted_scanner_outputs.pop();
   8606                 }
   8607                 6 => {
   8608                     fixture.request.sources.pop();
   8609                 }
   8610                 7 => fixture.request.sources.swap(0, 1),
   8611                 8 => fixture.request.sources[0].revision = "invalid".to_owned(),
   8612                 9 => fixture.request.sources[0].tree = "invalid".to_owned(),
   8613                 10 => fixture.request.admitted_gradle_graphs.swap(0, 1),
   8614                 11 => fixture.request.admitted_scanner_outputs.swap(0, 1),
   8615                 _ => fixture.request.sources.clear(),
   8616             }
   8617             assert!(
   8618                 validate_request(&fixture.request).is_err(),
   8619                 "request case {case}"
   8620             );
   8621         }
   8622         for (slot, maximum) in [
   8623             MAX_PRODUCER_REQUEST_BYTES,
   8624             MAX_TOOL_MANIFEST_BYTES,
   8625             MAX_TOOL_OBSERVATION_BYTES,
   8626             MAX_NVD_TRACE_BYTES,
   8627             MAX_PROVIDER_EVIDENCE_BYTES,
   8628         ]
   8629         .into_iter()
   8630         .enumerate()
   8631         {
   8632             let mut fixture = SyntheticFixture::new().unwrap();
   8633             let field = match slot {
   8634                 0 => &mut fixture.request.producer_request,
   8635                 1 => &mut fixture.request.step_297_tool_manifest,
   8636                 2 => &mut fixture.request.fresh_tool_observation,
   8637                 3 => &mut fixture.request.nvd_network_trace,
   8638                 _ => &mut fixture.request.provider_execution_evidence,
   8639             };
   8640             field.resize(maximum + 1, b' ');
   8641             assert_eq!(
   8642                 validate_trusted_authority(&fixture.request)
   8643                     .unwrap_err()
   8644                     .kind(),
   8645                 AdvisoryFailureKind::InvalidSnapshot
   8646             );
   8647         }
   8648     }
   8649 
   8650     #[test]
   8651     fn pinned_tools_and_scanner_arguments_reject_stale_or_substituted_inputs() {
   8652         let fixture = SyntheticFixture::new().unwrap();
   8653         let states = &fixture.manifest.tool_state;
   8654         rejects_typed_changes(
   8655             states,
   8656             &[
   8657                 ("/0/id", json!("unknown")),
   8658                 ("/0/normalized_version", json!("0")),
   8659                 ("/0/executable_sha256", json!("f".repeat(64))),
   8660                 ("/0/source_sha256", json!("f".repeat(64))),
   8661                 ("/0/package_receipt_sha256", json!("f".repeat(64))),
   8662                 ("/0/reviewed_at_epoch", json!(0)),
   8663                 (
   8664                     "/0/reviewed_at_epoch",
   8665                     json!(fixture.request.evaluation_epoch + 1),
   8666                 ),
   8667                 (
   8668                     "/0/reviewed_at_epoch",
   8669                     json!(fixture.request.evaluation_epoch - TOOL_REVIEW_SECONDS - 1),
   8670                 ),
   8671                 ("/0/state", json!("unavailable")),
   8672             ],
   8673             |changed| validate_tool_states(changed, fixture.request.evaluation_epoch).is_ok(),
   8674         );
   8675         assert!(validate_tool_states(&[], fixture.request.evaluation_epoch).is_err());
   8676         let mut boundary = states.clone();
   8677         boundary[0].reviewed_at_epoch = fixture.request.evaluation_epoch - TOOL_REVIEW_SECONDS;
   8678         validate_tool_states(&boundary, fixture.request.evaluation_epoch).unwrap();
   8679         rejects_typed_changes(
   8680             &fixture.manifest.tool_acquisition,
   8681             &[("/0/id", json!("unknown")), ("/0/projection", json!({}))],
   8682             |changed| validate_tool_acquisitions(changed).is_ok(),
   8683         );
   8684         assert!(validate_tool_acquisitions(&[]).is_err());
   8685         for (provider, workload) in [
   8686             (ProviderId::Rustsec, "lib"),
   8687             (ProviderId::OwaspNvd, "app_design_system"),
   8688         ] {
   8689             let original = synthetic_scanner_arguments(provider, workload);
   8690             assert!(valid_scanner_arguments(provider, &original));
   8691             assert!(!valid_scanner_arguments(provider, &[]));
   8692             for index in 0..original.len() {
   8693                 let mut changed = original.clone();
   8694                 changed[index] = "untrusted".to_owned();
   8695                 assert!(
   8696                     !valid_scanner_arguments(provider, &changed),
   8697                     "{provider:?} argument {index}"
   8698                 );
   8699             }
   8700         }
   8701         for value in ["", "relative", "/contains\0nul"] {
   8702             assert!(!absolute_path(value));
   8703         }
   8704         assert!(absolute_path("/valid/path"));
   8705     }
   8706 
   8707     #[test]
   8708     fn suppression_requires_unique_exact_finding_and_unexpired_owner_rationale() {
   8709         let suppression = Suppression {
   8710             id: "review-1".to_owned(),
   8711             provider: ProviderId::Rustsec,
   8712             advisory_id: "RUSTSEC-2099-0001".to_owned(),
   8713             workload_id: WORKLOADS[0].id.to_owned(),
   8714             package_ecosystem: "cargo".to_owned(),
   8715             package_namespace: String::new(),
   8716             package_name: "example".to_owned(),
   8717             package_version: "1.0.0".to_owned(),
   8718             owner: "security@example.invalid".to_owned(),
   8719             rationale: "Reviewed fixture".to_owned(),
   8720             created_at_epoch: 100,
   8721             expires_at_epoch: 200,
   8722         };
   8723         let finding = Finding {
   8724             provider: suppression.provider,
   8725             advisory_id: suppression.advisory_id.clone(),
   8726             package_ecosystem: suppression.package_ecosystem.clone(),
   8727             package_namespace: suppression.package_namespace.clone(),
   8728             package_name: suppression.package_name.clone(),
   8729             package_version: suppression.package_version.clone(),
   8730             workload_id: suppression.workload_id.clone(),
   8731         };
   8732         let mut findings = vec![finding.clone()];
   8733         apply_suppressions(&mut findings, std::slice::from_ref(&suppression), 150).unwrap();
   8734         assert!(findings.is_empty());
   8735         for mut findings in [vec![], vec![finding.clone(), finding.clone()]] {
   8736             assert!(
   8737                 apply_suppressions(&mut findings, std::slice::from_ref(&suppression), 150).is_err()
   8738             );
   8739         }
   8740         rejects_typed_changes(
   8741             &suppression,
   8742             &[
   8743                 ("/advisory_id", json!(NON_WAIVABLE_RUSTSEC)),
   8744                 ("/advisory_id", json!("unknown")),
   8745                 ("/id", json!("..")),
   8746                 ("/package_ecosystem", json!("npm")),
   8747                 ("/package_namespace", json!("unexpected")),
   8748                 ("/package_name", json!("")),
   8749                 ("/package_version", json!("")),
   8750                 ("/workload_id", json!("unknown")),
   8751                 ("/owner", json!("")),
   8752                 ("/rationale", json!("")),
   8753                 ("/created_at_epoch", json!(0)),
   8754                 ("/created_at_epoch", json!(200)),
   8755                 ("/created_at_epoch", json!(151)),
   8756                 ("/expires_at_epoch", json!(150)),
   8757             ],
   8758             |changed| validate_suppression_inventory(std::slice::from_ref(changed), 150).is_ok(),
   8759         );
   8760         for field in [
   8761             "provider",
   8762             "advisory_id",
   8763             "package_ecosystem",
   8764             "package_namespace",
   8765             "package_name",
   8766             "package_version",
   8767             "workload_id",
   8768         ] {
   8769             let mut changed = serde_json::to_value(&suppression).unwrap();
   8770             changed[field] = if field == "provider" {
   8771                 json!("owasp_nvd")
   8772             } else {
   8773                 json!("mismatch")
   8774             };
   8775             let changed: Suppression = serde_json::from_value(changed).unwrap();
   8776             assert!(!suppression_matches(&changed, &finding), "{field}");
   8777         }
   8778         assert!(
   8779             validate_suppression_inventory(&[suppression.clone(), suppression.clone()], 150)
   8780                 .is_err()
   8781         );
   8782         let mut second = suppression.clone();
   8783         second.id = "review-2".to_owned();
   8784         validate_suppression_inventory(&[suppression.clone(), second.clone()], 150).unwrap();
   8785         assert!(validate_suppression_inventory(&[second, suppression.clone()], 150).is_err());
   8786         let mut maven = suppression;
   8787         maven.provider = ProviderId::OwaspNvd;
   8788         maven.advisory_id = "CVE-2099-0001".to_owned();
   8789         maven.package_ecosystem = "maven".to_owned();
   8790         assert!(validate_suppression_inventory(&[maven.clone()], 150).is_err());
   8791         maven.package_namespace = "com.example".to_owned();
   8792         validate_suppression_inventory(&[maven], 150).unwrap();
   8793     }
   8794 
   8795     #[test]
   8796     fn temporal_evidence_orders_review_acquisition_normalization_and_scans() {
   8797         let fixture = SyntheticFixture::new().unwrap();
   8798         let observation: TrustedToolObservation =
   8799             parse_canonical_authority(&fixture.request.fresh_tool_observation).unwrap();
   8800         let providers = &fixture.manifest.provider_snapshot;
   8801         let receipts = &fixture.process_receipts;
   8802         validate_temporal_order(&observation, providers, receipts).unwrap();
   8803         rejects_typed_changes(
   8804             &observation,
   8805             &[
   8806                 ("/observed_at_epoch", json!(0)),
   8807                 (
   8808                     "/tool_state/0/reviewed_at_epoch",
   8809                     json!(observation.observed_at_epoch + 1),
   8810                 ),
   8811             ],
   8812             |changed| validate_temporal_order(changed, providers, receipts).is_ok(),
   8813         );
   8814         rejects_typed_changes(
   8815             providers,
   8816             &[
   8817                 (
   8818                     "/0/acquired_at_epoch",
   8819                     json!(observation.observed_at_epoch - 1),
   8820                 ),
   8821                 ("/0/digest_time_epoch", json!(u64::MAX)),
   8822                 ("/0/analyzed_at_epoch", json!(u64::MAX)),
   8823                 (
   8824                     "/1/acquired_at_epoch",
   8825                     json!(providers[1].acquired_at_epoch + 1),
   8826                 ),
   8827             ],
   8828             |changed| validate_temporal_order(&observation, changed, receipts).is_ok(),
   8829         );
   8830         assert!(validate_temporal_order(&observation, providers, &[]).is_err());
   8831         let update = receipts
   8832             .iter()
   8833             .position(|row| row.id == "owasp_nvd_update")
   8834             .unwrap();
   8835         let gradle = receipts
   8836             .iter()
   8837             .position(|row| row.id.starts_with("gradle:"))
   8838             .unwrap();
   8839         for (index, started) in [(update, true), (gradle, false)] {
   8840             let mut changed = receipts.clone();
   8841             if started {
   8842                 changed[index].started_at_epoch = observation.observed_at_epoch - 1;
   8843             } else {
   8844                 changed[index].completed_at_epoch = u64::MAX;
   8845             }
   8846             assert!(validate_temporal_order(&observation, providers, &changed).is_err());
   8847         }
   8848     }
   8849     use serde_json::json;
   8850 
   8851     #[test]
   8852     fn complete_rustsec_reports_reject_nested_type_count_and_scanner_policy_drift() {
   8853         let mut fixture = SyntheticFixture::new().unwrap();
   8854         fixture
   8855             .set_rustsec_finding("RUSTSEC-2099-0001", "example", "1.0.0")
   8856             .unwrap();
   8857         let report = fixture.read_report_value(ProviderId::Rustsec).unwrap();
   8858         let result: WorkloadResult =
   8859             serde_json::from_value(report["workload_result"][0].clone()).unwrap();
   8860         let original: Value = serde_json::from_str(&result.raw_scanner_output).unwrap();
   8861         let provider = &fixture.manifest.provider_snapshot[0];
   8862         let parse = |value: &Value| {
   8863             parse_rustsec_output(
   8864                 &result.workload_id,
   8865                 value,
   8866                 result.dependency_count,
   8867                 &provider.database_identity_sha256,
   8868                 &provider.materialized_tree_sha256,
   8869                 provider.digest_time_epoch,
   8870                 &mut Vec::new(),
   8871             )
   8872         };
   8873         rejects_wrong_node_types(&original, |value| parse(value).is_ok());
   8874         rejects_unknown_object_fields(
   8875             &original,
   8876             &[
   8877                 "",
   8878                 "/database",
   8879                 "/lockfile",
   8880                 "/settings",
   8881                 "/vulnerabilities",
   8882                 "/vulnerabilities/list/0",
   8883                 "/vulnerabilities/list/0/advisory",
   8884                 "/vulnerabilities/list/0/package",
   8885                 "/vulnerabilities/list/0/versions",
   8886             ],
   8887             |value| parse(value).is_ok(),
   8888         );
   8889         let mut substituted_database = original.clone();
   8890         substituted_database["database"]["last-commit"] = json!("f".repeat(40));
   8891         assert!(parse(&substituted_database).is_err());
   8892         for (pointer, replacement) in [
   8893             ("/database/advisory-count", json!(0)),
   8894             ("/database/last-commit", json!("invalid")),
   8895             ("/database/last-updated", json!("invalid")),
   8896             ("/lockfile/dependency-count", json!(0)),
   8897             ("/settings/ignore", json!(["RUSTSEC-2099-0001"])),
   8898             ("/settings/target_arch", json!(["x86_64"])),
   8899             ("/settings/target_os", json!(["linux"])),
   8900             ("/settings/severity", json!("high")),
   8901             ("/settings/informational_warnings", json!([])),
   8902             ("/vulnerabilities/found", json!(false)),
   8903             ("/vulnerabilities/count", json!(0)),
   8904         ] {
   8905             let mut changed = original.clone();
   8906             *changed.pointer_mut(pointer).unwrap() = replacement;
   8907             assert!(parse(&changed).is_err(), "{pointer}");
   8908         }
   8909         for key in original.as_object().unwrap().keys() {
   8910             let mut changed = original.clone();
   8911             changed.as_object_mut().unwrap().remove(key);
   8912             assert!(parse(&changed).is_err(), "missing {key}");
   8913         }
   8914         for kind in ["notice", "unmaintained", "unsound"] {
   8915             let mut changed = original.clone();
   8916             let mut warning = original["vulnerabilities"]["list"][0].clone();
   8917             warning["kind"] = json!(kind);
   8918             changed["warnings"][kind] = json!([warning]);
   8919             rejects_wrong_node_types(&changed, |value| parse(value).is_ok());
   8920             rejects_unknown_object_fields(&changed, &[&format!("/warnings/{kind}/0")], |value| {
   8921                 parse(value).is_ok()
   8922             });
   8923             changed["warnings"][kind][0]["kind"] = json!("other");
   8924             assert!(parse(&changed).is_err());
   8925         }
   8926         for (kind, rows) in [("unknown", json!([])), ("yanked", json!([{}]))] {
   8927             let mut changed = original.clone();
   8928             changed["warnings"][kind] = rows;
   8929             assert!(parse(&changed).is_err());
   8930         }
   8931         let mut empty_yanked = original.clone();
   8932         empty_yanked["warnings"]["yanked"] = json!([]);
   8933         assert!(parse(&empty_yanked).is_ok());
   8934     }
   8935 
   8936     #[test]
   8937     fn complete_owasp_reports_bind_database_project_file_and_package_identity() {
   8938         let mut fixture = SyntheticFixture::new().unwrap();
   8939         let artifact = fixture.manifest.gradle_projection[0].artifacts[0].clone();
   8940         fixture
   8941             .set_owasp_finding(
   8942                 "CVE-2099-0001",
   8943                 &artifact.package_namespace,
   8944                 &artifact.package_name,
   8945                 &artifact.package_version,
   8946             )
   8947             .unwrap();
   8948         let report = fixture.read_report_value(ProviderId::OwaspNvd).unwrap();
   8949         let result: WorkloadResult =
   8950             serde_json::from_value(report["workload_result"][0].clone()).unwrap();
   8951         let mut original: Value = serde_json::from_str(&result.raw_scanner_output).unwrap();
   8952         for key in ["artifactID", "groupID", "version"] {
   8953             original["projectInfo"][key] = json!("fixture");
   8954         }
   8955         original["dependencies"][0]["description"] = json!("fixture");
   8956         original["dependencies"][0]["license"] = json!("Apache-2.0");
   8957         original["dependencies"][0]["projectReferences"] = json!(["fixture"]);
   8958         let provider = &fixture.manifest.provider_snapshot[1];
   8959         let projection = &fixture.manifest.gradle_projection[0];
   8960         let receipt = process_receipt(
   8961             &fixture.process_receipts,
   8962             &format!("owasp_analysis:{}", result.workload_id),
   8963         )
   8964         .unwrap();
   8965         let parse = |value: &Value| {
   8966             parse_owasp_output(
   8967                 &result.workload_id,
   8968                 value,
   8969                 projection,
   8970                 &provider.database_identity_sha256,
   8971                 &provider.materialized_tree_sha256,
   8972                 &result.arguments[4],
   8973                 receipt.started_at_epoch,
   8974                 receipt.completed_at_epoch,
   8975                 provider.digest_time_epoch,
   8976                 &mut Vec::new(),
   8977             )
   8978         };
   8979         rejects_wrong_node_types(&original, |value| parse(value).is_ok());
   8980         rejects_unknown_object_fields(
   8981             &original,
   8982             &[
   8983                 "",
   8984                 "/scanInfo",
   8985                 "/scanInfo/dataSource/0",
   8986                 "/projectInfo",
   8987                 "/projectInfo/credits",
   8988                 "/dependencies/0",
   8989                 "/dependencies/0/evidenceCollected",
   8990                 "/dependencies/0/packages/0",
   8991                 "/dependencies/0/vulnerabilities/0",
   8992             ],
   8993             |value| parse(value).is_ok(),
   8994         );
   8995         for timestamp in [
   8996             provider.digest_time_epoch - 1,
   8997             provider.digest_time_epoch + 1,
   8998         ] {
   8999             let mut changed = original.clone();
   9000             changed["scanInfo"]["dataSource"][0]["timestamp"] =
   9001                 json!(format_report_epoch(timestamp).unwrap());
   9002             assert!(parse(&changed).is_err());
   9003         }
   9004         for timestamp in [receipt.started_at_epoch - 1, receipt.completed_at_epoch + 1] {
   9005             let mut changed = original.clone();
   9006             changed["projectInfo"]["reportDate"] = json!(format_report_epoch(timestamp).unwrap());
   9007             assert!(parse(&changed).is_err());
   9008         }
   9009         for count in [2, 65] {
   9010             let mut changed = original.clone();
   9011             changed["scanInfo"]["dataSource"] =
   9012                 json!(vec![original["scanInfo"]["dataSource"][0].clone(); count]);
   9013             assert!(parse(&changed).is_err());
   9014         }
   9015         let mut database_name = original.clone();
   9016         database_name["scanInfo"]["dataSource"][0]["name"] = json!("NVD CVE substituted");
   9017         assert!(parse(&database_name).is_err());
   9018         let mut unsorted_references = original.clone();
   9019         unsorted_references["dependencies"][0]["projectReferences"] = json!(["z", "a"]);
   9020         assert!(parse(&unsorted_references).is_err());
   9021         for (pointer, replacement) in [
   9022             ("/reportSchema", json!("2.0")),
   9023             ("/scanInfo/engineVersion", json!("unknown")),
   9024             ("/scanInfo/dataSource", json!([])),
   9025             ("/scanInfo/dataSource/0/name", json!("other")),
   9026             (
   9027                 "/scanInfo/dataSource/0/timestamp",
   9028                 json!("2099-01-01T00:00:00Z"),
   9029             ),
   9030             ("/projectInfo/name", json!("another-workload")),
   9031             ("/projectInfo/reportDate", json!("1970-01-01T00:00:00Z")),
   9032             ("/dependencies", json!([])),
   9033             ("/dependencies/0/fileName", json!("other.jar")),
   9034             ("/dependencies/0/filePath", json!("/other/path")),
   9035             ("/dependencies/0/md5", json!("invalid")),
   9036             ("/dependencies/0/sha1", json!("invalid")),
   9037             ("/dependencies/0/sha256", json!("invalid")),
   9038             ("/dependencies/0/sha256", json!("f".repeat(64))),
   9039             (
   9040                 "/dependencies/0/packages/0/id",
   9041                 json!("pkg:maven/other/package@1.0"),
   9042             ),
   9043             ("/dependencies/0/vulnerabilityIds/0/id", json!("invalid")),
   9044             (
   9045                 "/dependencies/0/vulnerabilityIds/0/id",
   9046                 json!("cpe:/a:other:package:1.0"),
   9047             ),
   9048         ] {
   9049             let mut changed = original.clone();
   9050             *changed.pointer_mut(pointer).unwrap() = replacement;
   9051             assert!(parse(&changed).is_err(), "{pointer}");
   9052         }
   9053         for key in [
   9054             "suppressedVulnerabilities",
   9055             "suppressedVulnerabilityIds",
   9056             "unknown",
   9057         ] {
   9058             let mut changed = original.clone();
   9059             changed["dependencies"][0][key] = json!([]);
   9060             assert!(parse(&changed).is_err());
   9061         }
   9062         let mut missing_findings = original.clone();
   9063         missing_findings["dependencies"][0]
   9064             .as_object_mut()
   9065             .unwrap()
   9066             .remove("vulnerabilities");
   9067         assert!(parse(&missing_findings).is_err());
   9068         let mut duplicated = original.clone();
   9069         duplicated["dependencies"]
   9070             .as_array_mut()
   9071             .unwrap()
   9072             .push(original["dependencies"][0].clone());
   9073         assert!(parse(&duplicated).is_err());
   9074     }
   9075 
   9076     fn rejects_unknown_object_fields(
   9077         original: &Value,
   9078         pointers: &[&str],
   9079         validate: impl Fn(&Value) -> bool,
   9080     ) {
   9081         assert!(validate(original), "valid closed-schema fixture");
   9082         for pointer in pointers {
   9083             let mut changed = original.clone();
   9084             let object = changed
   9085                 .pointer_mut(pointer)
   9086                 .expect(pointer)
   9087                 .as_object_mut()
   9088                 .expect(pointer);
   9089             assert!(
   9090                 object
   9091                     .insert("unexpected_field".into(), json!(true))
   9092                     .is_none()
   9093             );
   9094             assert!(!validate(&changed), "unknown field at {pointer}");
   9095         }
   9096     }
   9097 
   9098     #[test]
   9099     fn virtual_owasp_dependencies_require_rooted_identity_and_emit_exact_findings() {
   9100         let mut fixture = SyntheticFixture::new().unwrap();
   9101         let artifact = fixture.manifest.gradle_projection[0].artifacts[0].clone();
   9102         fixture
   9103             .set_owasp_finding(
   9104                 "CVE-2099-0001",
   9105                 &artifact.package_namespace,
   9106                 &artifact.package_name,
   9107                 &artifact.package_version,
   9108             )
   9109             .unwrap();
   9110         let report = fixture.read_report_value(ProviderId::OwaspNvd).unwrap();
   9111         let result: WorkloadResult =
   9112             serde_json::from_value(report["workload_result"][0].clone()).unwrap();
   9113         let mut original: Value = serde_json::from_str(&result.raw_scanner_output).unwrap();
   9114         let provider = &fixture.manifest.provider_snapshot[1];
   9115         let projection = &fixture.manifest.gradle_projection[0];
   9116         let receipt = process_receipt(
   9117             &fixture.process_receipts,
   9118             &format!("owasp_analysis:{}", result.workload_id),
   9119         )
   9120         .unwrap();
   9121         let parse = |value: &Value, scan_root: &str| {
   9122             let mut findings = Vec::new();
   9123             parse_owasp_output(
   9124                 &result.workload_id,
   9125                 value,
   9126                 projection,
   9127                 &provider.database_identity_sha256,
   9128                 &provider.materialized_tree_sha256,
   9129                 scan_root,
   9130                 receipt.started_at_epoch,
   9131                 receipt.completed_at_epoch,
   9132                 provider.digest_time_epoch,
   9133                 &mut findings,
   9134             )?;
   9135             Ok::<_, AdvisoryError>(findings)
   9136         };
   9137         let mut virtual_row = original["dependencies"][0].clone();
   9138         for key in ["md5", "sha1", "sha256"] {
   9139             virtual_row.as_object_mut().unwrap().remove(key);
   9140         }
   9141         virtual_row["isVirtual"] = json!(true);
   9142         virtual_row["fileName"] = json!("embedded-module");
   9143         virtual_row["filePath"] = json!(format!("{}/embedded-module", result.arguments[4]));
   9144         virtual_row["packages"] = json!([{"id":"pkg:maven/embedded/library@2.0"}]);
   9145         virtual_row["includedBy"] =
   9146             json!([{"reference":format!("project:{}", result.workload_id)}]);
   9147         original["dependencies"]
   9148             .as_array_mut()
   9149             .unwrap()
   9150             .push(virtual_row);
   9151         let findings = parse(&original, &result.arguments[4]).unwrap();
   9152         assert_eq!(findings.len(), 2);
   9153         assert_eq!(findings[1].package_namespace, "embedded");
   9154         assert_eq!(findings[1].package_name, "library");
   9155         assert_eq!(findings[1].package_version, "2.0");
   9156         assert_eq!(findings[1].advisory_id, "CVE-2099-0001");
   9157         for (key, value) in [
   9158             ("md5", json!("1".repeat(32))),
   9159             ("sha1", json!("1".repeat(40))),
   9160             ("sha256", json!("1".repeat(64))),
   9161             ("filePath", json!("/outside/module")),
   9162             ("fileName", json!(artifact.materialized_name)),
   9163             ("packages", json!([])),
   9164             ("includedBy", json!([])),
   9165         ] {
   9166             let mut changed = original.clone();
   9167             changed["dependencies"][1][key] = value;
   9168             assert!(parse(&changed, &result.arguments[4]).is_err(), "{key}");
   9169         }
   9170         for key in ["packages", "includedBy"] {
   9171             let mut changed = original.clone();
   9172             changed["dependencies"][1]
   9173                 .as_object_mut()
   9174                 .unwrap()
   9175                 .remove(key);
   9176             assert!(parse(&changed, &result.arguments[4]).is_err());
   9177         }
   9178         let mut missing_physical = original.clone();
   9179         missing_physical["dependencies"]
   9180             .as_array_mut()
   9181             .unwrap()
   9182             .remove(0);
   9183         assert!(parse(&missing_physical, &result.arguments[4]).is_err());
   9184         for root in ["relative", "/trailing/"] {
   9185             assert!(parse(&original, root).is_err());
   9186         }
   9187     }
   9188 
   9189     #[test]
   9190     fn scanner_execution_rejects_independently_substituted_trusted_receipts() {
   9191         let mut fixture = SyntheticFixture::new().unwrap();
   9192         for provider in &fixture.manifest.provider_snapshot {
   9193             let original =
   9194                 fixture.read_report_value(provider.provider).unwrap()["workload_result"][0].clone();
   9195             let original_result: WorkloadResult = serde_json::from_value(original.clone()).unwrap();
   9196             let inventory = fixture
   9197                 .request
   9198                 .inventory
   9199                 .iter()
   9200                 .find(|row| row.id == original_result.workload_id)
   9201                 .unwrap()
   9202                 .clone();
   9203             let id = format!(
   9204                 "{}:{}",
   9205                 if provider.provider == ProviderId::Rustsec {
   9206                     "cargo_audit"
   9207                 } else {
   9208                     "owasp_analysis"
   9209                 },
   9210                 original_result.workload_id
   9211             );
   9212             let authority: TrustedProviderEvidence =
   9213                 parse_canonical_authority(&fixture.request.provider_execution_evidence).unwrap();
   9214             validate_workload_execution(provider, &original_result, &inventory, &fixture.request)
   9215                 .unwrap();
   9216             let original_bytes = fixture.request.provider_execution_evidence.clone();
   9217             let authority_value = serde_json::to_value(&authority).unwrap();
   9218             let index = authority
   9219                 .process_receipt
   9220                 .iter()
   9221                 .position(|row| row.id == id)
   9222                 .unwrap();
   9223             for (field, replacement) in [
   9224                 ("program_sha256", json!("f".repeat(64))),
   9225                 ("arguments_sha256", json!("f".repeat(64))),
   9226                 ("input_sha256", json!("f".repeat(64))),
   9227                 ("output_sha256", json!("f".repeat(64))),
   9228                 ("stdout_byte_length", json!(1)),
   9229                 ("stdout_sha256", json!("f".repeat(64))),
   9230                 ("exit_code", json!(2)),
   9231                 ("path_binding", json!([])),
   9232             ] {
   9233                 let mut changed = authority_value.clone();
   9234                 assert_ne!(
   9235                     changed["process_receipt"][index][field], replacement,
   9236                     "mutation {field}"
   9237                 );
   9238                 changed["process_receipt"][index][field] = replacement;
   9239                 let changed: TrustedProviderEvidence = serde_json::from_value(changed).unwrap();
   9240                 fixture.request.provider_execution_evidence =
   9241                     canonical_authority_bytes(&changed).unwrap();
   9242                 validate_trusted_authority(&fixture.request)
   9243                     .expect("receipt remains structurally trusted");
   9244                 let mut result = original.clone();
   9245                 result["process_receipt_sha256"] =
   9246                     json!(process_receipt_digest(&changed.process_receipt[index]).unwrap());
   9247                 let result: WorkloadResult = serde_json::from_value(result).unwrap();
   9248                 assert!(
   9249                     validate_workload_execution(provider, &result, &inventory, &fixture.request)
   9250                         .is_err(),
   9251                     "{field}"
   9252                 );
   9253             }
   9254             fixture.request.provider_execution_evidence = original_bytes;
   9255         }
   9256     }
   9257 
   9258     fn rejects_typed_changes<T: for<'de> Deserialize<'de> + Serialize>(
   9259         original: &T,
   9260         changes: &[(&str, Value)],
   9261         validate: impl Fn(&T) -> bool,
   9262     ) {
   9263         assert!(validate(original), "accepted typed fixture");
   9264         let value = serde_json::to_value(original).unwrap();
   9265         for (pointer, replacement) in changes {
   9266             let mut changed = value.clone();
   9267             *changed.pointer_mut(pointer).expect(pointer) = replacement.clone();
   9268             assert_ne!(changed, value, "mutation must change {pointer}");
   9269             let changed: T =
   9270                 serde_json::from_value(changed).expect("mutation preserves wire types");
   9271             assert!(
   9272                 !validate(&changed),
   9273                 "invalid typed field accepted: {pointer}"
   9274             );
   9275         }
   9276     }
   9277 
   9278     #[test]
   9279     fn provider_admission_binds_artifacts_times_and_execution_receipts() {
   9280         let fixture = SyntheticFixture::new().unwrap();
   9281         let trace: NvdNetworkTrace =
   9282             parse_canonical_authority(&fixture.request.nvd_network_trace).unwrap();
   9283         for provider in &fixture.manifest.provider_snapshot {
   9284             let mut changes = vec![
   9285                 ("/acquisition_state", json!("failed")),
   9286                 ("/analysis_state", json!("unavailable")),
   9287                 ("/acquisition_count", json!(0)),
   9288                 ("/bounded_deadline_seconds", json!(0)),
   9289                 ("/network_trace_sha256", json!("invalid")),
   9290                 ("/producer_request_sha256", json!("invalid")),
   9291                 ("/producer_request_sha256", json!("f".repeat(64))),
   9292                 ("/database_identity_sha256", json!("invalid")),
   9293                 ("/archive_format", json!("zip")),
   9294                 ("/archive_expanded_bytes", json!(0)),
   9295                 ("/archive_member_count", json!(0)),
   9296                 ("/archive_payload_bytes", json!(0)),
   9297                 ("/acquired_at_epoch", json!(0)),
   9298                 ("/digest_time_epoch", json!(0)),
   9299                 ("/analyzed_at_epoch", json!(0)),
   9300                 ("/acquired_at_epoch", json!(u64::MAX)),
   9301                 ("/digest_time_epoch", json!(u64::MAX)),
   9302                 ("/analyzed_at_epoch", json!(u64::MAX)),
   9303                 ("/acquisition_kind", json!("untrusted")),
   9304                 ("/network_mode", json!("unrestricted")),
   9305                 ("/archive/path", json!("other.gz")),
   9306                 ("/report/path", json!("other.json")),
   9307                 ("/analysis_environment", json!("ambient")),
   9308                 ("/analysis_arguments", json!([])),
   9309                 ("/acquisition_arguments", json!(["untrusted"])),
   9310                 ("/archive/byte_length", json!(0)),
   9311                 ("/report/byte_length", json!(0)),
   9312                 ("/archive/byte_length", json!(2_147_483_649u64)),
   9313                 ("/report/byte_length", json!(MAX_REPORT_BYTES + 1)),
   9314                 ("/archive/sha256", json!("invalid")),
   9315                 ("/report/sha256", json!("invalid")),
   9316                 ("/archive/logical_uri", json!("file:///untrusted")),
   9317                 ("/report/logical_uri", json!("file:///untrusted")),
   9318                 ("/archive/media_type", json!("application/zip")),
   9319                 ("/report/media_type", json!("text/plain")),
   9320                 ("/archive/logical_role", json!("unknown")),
   9321                 ("/report/logical_role", json!("unknown")),
   9322             ];
   9323             if provider.provider == ProviderId::Rustsec {
   9324                 changes.push(("/network_trace_sha256", json!("a".repeat(64))));
   9325             }
   9326             rejects_typed_changes(provider, &changes, |changed| {
   9327                 validate_provider(changed, &fixture.request, &trace, &fixture.process_receipts)
   9328                     .is_ok()
   9329             });
   9330         }
   9331         let provider = &fixture.manifest.provider_snapshot[1];
   9332         let receipts = &fixture.process_receipts;
   9333         rejects_typed_changes(
   9334             receipts,
   9335             &[
   9336                 ("/0/program_sha256", json!("a".repeat(64))),
   9337                 ("/0/arguments_sha256", json!("a".repeat(64))),
   9338                 ("/0/output_sha256", json!("a".repeat(64))),
   9339                 ("/0/input_sha256", json!("a".repeat(64))),
   9340                 ("/0/path_binding", json!([])),
   9341                 ("/0/completed_at_epoch", json!(0)),
   9342                 ("/0/exit_code", json!(1)),
   9343             ],
   9344             |changed| validate_provider(provider, &fixture.request, &trace, changed).is_ok(),
   9345         );
   9346     }
   9347 
   9348     #[test]
   9349     fn request_inventory_and_projection_fields_are_independently_bound() {
   9350         let mut fixture = SyntheticFixture::new().unwrap();
   9351         let inventory = fixture.request.inventory.clone();
   9352         let original = serde_json::to_value(&inventory[0]).unwrap();
   9353         for (key, value) in original.as_object().unwrap() {
   9354             let mut changed = original.clone();
   9355             changed[key] = match value {
   9356                 Value::Number(_) => json!(0),
   9357                 Value::String(_) | Value::Null => json!("untrusted"),
   9358                 _ => panic!("unexpected inventory field {key}"),
   9359             };
   9360             fixture.request.inventory[0] = serde_json::from_value(changed).unwrap();
   9361             assert!(validate_request(&fixture.request).is_err(), "{key}");
   9362         }
   9363         fixture.request.inventory = inventory;
   9364         validate_request(&fixture.request).unwrap();
   9365         let projections = &fixture.manifest.gradle_projection;
   9366         let original = serde_json::to_value(&projections[0]).unwrap();
   9367         for (key, value) in original.as_object().unwrap() {
   9368             let mut changed = original.clone();
   9369             changed[key] = match value {
   9370                 Value::Number(_) => {
   9371                     if key == "exit_code" {
   9372                         json!(1)
   9373                     } else {
   9374                         json!(0)
   9375                     }
   9376                 }
   9377                 Value::String(_) => {
   9378                     if key == "state" {
   9379                         json!("failed")
   9380                     } else {
   9381                         json!("untrusted")
   9382                     }
   9383                 }
   9384                 Value::Array(_) => json!([]),
   9385                 _ => panic!("unexpected projection field {key}"),
   9386             };
   9387             let mut candidate = projections.clone();
   9388             candidate[0] = serde_json::from_value(changed).unwrap();
   9389             assert!(
   9390                 validate_gradle_projections(
   9391                     &candidate,
   9392                     &fixture.request,
   9393                     &fixture.process_receipts
   9394                 )
   9395                 .is_err(),
   9396                 "{key}"
   9397             );
   9398         }
   9399         assert!(
   9400             validate_gradle_projections(&[], &fixture.request, &fixture.process_receipts).is_err()
   9401         );
   9402         fixture.request.sources.clear();
   9403         assert!(validate_request(&fixture.request).is_err());
   9404         assert!(
   9405             validate_gradle_projections(projections, &fixture.request, &fixture.process_receipts)
   9406                 .is_err()
   9407         );
   9408     }
   9409 
   9410     #[test]
   9411     fn raw_gradle_variants_bound_attributes_capabilities_and_recursion() {
   9412         let variant: RawGradleVariant = serde_json::from_value(json!({
   9413             "attributes":[{"name":"usage", "value":"runtime"}],
   9414             "capabilities":[{"group":"example", "name":"library", "version":"1.0"}],
   9415             "external_variant":null,
   9416         }))
   9417         .unwrap();
   9418         rejects_typed_changes(
   9419             &variant,
   9420             &[
   9421                 ("/attributes/0/name", json!("")),
   9422                 ("/attributes/0/value", json!("\0")),
   9423                 ("/capabilities/0/group", json!("\0")),
   9424                 ("/capabilities/0/name", json!("")),
   9425                 ("/capabilities/0/version", json!("\0")),
   9426             ],
   9427             |changed| validate_raw_variant(changed, 0).is_ok(),
   9428         );
   9429         let mut duplicate = variant.clone();
   9430         duplicate.attributes.push(duplicate.attributes[0].clone());
   9431         assert!(validate_raw_variant(&duplicate, 0).is_err());
   9432         duplicate.attributes[1].value = "z".into();
   9433         assert!(validate_raw_variant(&duplicate, 0).is_err());
   9434         let mut oversized = variant.clone();
   9435         oversized.attributes = vec![variant.attributes[0].clone(); MAX_GRADLE_ATTRIBUTES + 1];
   9436         assert!(validate_raw_variant(&oversized, 0).is_err());
   9437         oversized = variant.clone();
   9438         oversized.capabilities = vec![variant.capabilities[0].clone(); MAX_GRADLE_CAPABILITIES + 1];
   9439         assert!(validate_raw_variant(&oversized, 0).is_err());
   9440         let mut nested = variant.clone();
   9441         for _ in 0..MAX_GRADLE_EXTERNAL_VARIANT_DEPTH {
   9442             let mut parent = variant.clone();
   9443             parent.external_variant = Some(Box::new(nested));
   9444             nested = parent;
   9445         }
   9446         validate_raw_variant(&nested, 0).unwrap();
   9447         let mut too_deep = variant.clone();
   9448         too_deep.external_variant = Some(Box::new(nested));
   9449         assert!(validate_raw_variant(&too_deep, 0).is_err());
   9450         assert!(
   9451             validate_raw_variant_envelope(&RawGradleVariantEnvelope { selected: vec![] }).is_err()
   9452         );
   9453         assert!(
   9454             validate_raw_variant_envelope(&RawGradleVariantEnvelope {
   9455                 selected: vec![variant.clone(); MAX_GRADLE_VARIANTS + 1]
   9456             })
   9457             .is_err()
   9458         );
   9459         assert!(
   9460             validate_raw_variant_envelope(&RawGradleVariantEnvelope {
   9461                 selected: vec![variant.clone(), variant]
   9462             })
   9463             .is_err()
   9464         );
   9465     }
   9466 
   9467     #[test]
   9468     fn raw_gradle_selectors_reject_mixed_identity_and_invalid_constraints() {
   9469         let module: RawGradleSelector = serde_json::from_value(json!({
   9470             "kind":"module", "group":"example", "name":"library", "version":"1.0",
   9471             "build_root":null, "project_path":null, "attributes":[], "capabilities":[],
   9472             "version_constraint":{"branch":null, "preferred":"", "required":"1.0", "strict":"", "rejected":["0.9"]}
   9473         })).unwrap();
   9474         rejects_typed_changes(
   9475             &module,
   9476             &[
   9477                 ("/kind", json!("unknown")),
   9478                 ("/group", json!(null)),
   9479                 ("/name", json!(null)),
   9480                 ("/version", json!(null)),
   9481                 ("/group", json!("")),
   9482                 ("/name", json!("")),
   9483                 ("/version", json!("\0")),
   9484                 ("/build_root", json!(".")),
   9485                 ("/project_path", json!(":app")),
   9486                 ("/version_constraint", json!(null)),
   9487                 ("/version_constraint/branch", json!("\0")),
   9488                 ("/version_constraint/preferred", json!("\0")),
   9489                 ("/version_constraint/required", json!("\0")),
   9490                 ("/version_constraint/strict", json!("\0")),
   9491                 ("/version_constraint/rejected", json!([""])),
   9492                 ("/version_constraint/rejected", json!(["b", "a"])),
   9493                 ("/version_constraint/rejected", json!(["a", "a"])),
   9494                 (
   9495                     "/version_constraint/rejected",
   9496                     json!(vec!["x"; MAX_GRADLE_REJECTED_VERSIONS + 1]),
   9497                 ),
   9498             ],
   9499             |changed| validate_raw_selector(changed).is_ok(),
   9500         );
   9501         let project: RawGradleSelector = serde_json::from_value(json!({
   9502             "kind":"project", "group":null, "name":null, "version":null, "version_constraint":null,
   9503             "build_root":".", "project_path":":app:shared", "attributes":[], "capabilities":[]
   9504         }))
   9505         .unwrap();
   9506         rejects_typed_changes(
   9507             &project,
   9508             &[
   9509                 ("/group", json!("example")),
   9510                 ("/name", json!("library")),
   9511                 ("/version", json!("1.0")),
   9512                 (
   9513                     "/version_constraint",
   9514                     serde_json::to_value(&module.version_constraint).unwrap(),
   9515                 ),
   9516                 ("/build_root", json!(null)),
   9517                 ("/project_path", json!(null)),
   9518                 ("/build_root", json!("../other")),
   9519                 ("/project_path", json!("app")),
   9520                 ("/project_path", json!("::app")),
   9521             ],
   9522             |changed| validate_raw_selector(changed).is_ok(),
   9523         );
   9524         for selector in [&module, &project] {
   9525             let core = RawGradleComponentCore {
   9526                 kind: selector.kind.clone(),
   9527                 group: selector.group.clone(),
   9528                 name: selector.name.clone(),
   9529                 version: selector.version.clone(),
   9530                 build_root: selector.build_root.clone(),
   9531                 project_path: selector.project_path.clone(),
   9532             };
   9533             validate_raw_component_core(&core).unwrap();
   9534             let original = serde_json::to_value(&core).unwrap();
   9535             for (key, value) in original.as_object().unwrap() {
   9536                 let mut changed = original.clone();
   9537                 changed[key] = if value.is_null() {
   9538                     json!("untrusted")
   9539                 } else {
   9540                     json!("")
   9541                 };
   9542                 assert!(
   9543                     validate_raw_component_core(&serde_json::from_value(changed).unwrap()).is_err(),
   9544                     "{key}"
   9545                 );
   9546             }
   9547         }
   9548     }
   9549 
   9550     #[test]
   9551     fn owasp_virtual_lineage_rejects_orphans_cycles_and_alias_collisions() {
   9552         fn dependency(alias: &str, parents: &[&str], is_virtual: bool) -> ParsedOwaspDependency {
   9553             ParsedOwaspDependency {
   9554                 aliases: vec![alias.into()],
   9555                 included_by: parents.iter().map(|value| (*value).into()).collect(),
   9556                 is_virtual,
   9557                 file_name: alias.into(),
   9558                 package_ids: vec![],
   9559                 vulnerabilities: vec![],
   9560             }
   9561         }
   9562         assert!(
   9563             validate_owasp_lineage(
   9564                 "root",
   9565                 &[
   9566                     dependency("physical", &[], false),
   9567                     dependency("parent", &["project:root"], true),
   9568                     dependency("child", &["parent", "physical"], true),
   9569                     dependency("sibling", &["parent"], true),
   9570                 ]
   9571             )
   9572             .is_ok()
   9573         );
   9574         for rows in [
   9575             vec![dependency("orphan", &[], true)],
   9576             vec![dependency("child", &["absent"], true)],
   9577             vec![dependency("a", &["b"], true), dependency("b", &["a"], true)],
   9578             vec![
   9579                 dependency("duplicate", &[], false),
   9580                 dependency("duplicate", &[], false),
   9581             ],
   9582             vec![dependency("root", &[], false)],
   9583         ] {
   9584             assert!(validate_owasp_lineage("root", &rows).is_err());
   9585         }
   9586     }
   9587 
   9588     #[test]
   9589     fn rustsec_affected_missing_required_fields_returns_a_typed_error() {
   9590         for field in ["arch", "functions", "os"] {
   9591             let mut value = json!({"arch":[], "functions":{}, "os":[]});
   9592             value.as_object_mut().unwrap().remove(field);
   9593             let error = validate_rustsec_affected(&value).expect_err(field);
   9594             assert_eq!(error.kind(), AdvisoryFailureKind::InvalidReport);
   9595         }
   9596     }
   9597 
   9598     // Exercise a complete accepted wire object, then corrupt each node's JSON
   9599     // type independently. A malformed nested row must never be ignored merely
   9600     // because another part of the report remains valid.
   9601     fn rejects_wrong_node_types(value: &Value, validate: impl Fn(&Value) -> bool) {
   9602         fn paths(value: &Value, prefix: String, out: &mut Vec<String>) {
   9603             out.push(prefix.clone());
   9604             match value {
   9605                 Value::Object(object) => {
   9606                     for (key, child) in object {
   9607                         let key = key.replace('~', "~0").replace('/', "~1");
   9608                         paths(child, format!("{prefix}/{key}"), out);
   9609                     }
   9610                 }
   9611                 Value::Array(rows) => {
   9612                     for (index, child) in rows.iter().enumerate() {
   9613                         paths(child, format!("{prefix}/{index}"), out);
   9614                     }
   9615                 }
   9616                 _ => {}
   9617             }
   9618         }
   9619         assert!(validate(value), "positive wire fixture must be accepted");
   9620         let mut pointers = Vec::new();
   9621         paths(value, String::new(), &mut pointers);
   9622         for pointer in pointers {
   9623             let mut changed = value.clone();
   9624             let node = changed.pointer_mut(&pointer).expect("fixture pointer");
   9625             *node = match node {
   9626                 Value::Object(_) | Value::Null => json!([]),
   9627                 Value::Array(_) => json!({}),
   9628                 Value::String(_) => json!(false),
   9629                 Value::Number(_) => json!("invalid-number"),
   9630                 Value::Bool(_) => json!(0),
   9631             };
   9632             assert!(!validate(&changed), "wrong type accepted at {pointer}");
   9633         }
   9634     }
   9635 
   9636     #[test]
   9637     fn rustsec_nested_package_affected_and_version_rows_fail_closed() {
   9638         let dependency =
   9639             json!({"name":"dep", "source":"registry+https://example.test", "version":"1.2.3"});
   9640         rejects_wrong_node_types(&dependency, valid_rustsec_dependency);
   9641         let package = json!({
   9642             "name":"example", "version":"1.0.0", "source":"registry+https://example.test",
   9643             "checksum":"a".repeat(64), "replace":dependency, "dependencies":[dependency]
   9644         });
   9645         rejects_wrong_node_types(&package, |value| validate_rustsec_package(value).is_ok());
   9646         for (pointer, replacement) in [
   9647             ("/name", json!("")),
   9648             ("/version", json!("..")),
   9649             ("/checksum", json!("g".repeat(64))),
   9650             ("/dependencies", json!([])),
   9651         ] {
   9652             let mut invalid = package.clone();
   9653             *invalid.pointer_mut(pointer).unwrap() = replacement;
   9654             assert!(validate_rustsec_package(&invalid).is_err(), "{pointer}");
   9655         }
   9656         assert!(
   9657             validate_rustsec_package(&json!({
   9658                 "name":"example", "version":"1.0.0", "source":null, "checksum":null, "replace":null
   9659             }))
   9660             .is_ok()
   9661         );
   9662         let affected = json!({"arch":["aarch64"], "os":["linux"], "functions":{"example::Thing<T,U>::_method":["<1.0.0"]}});
   9663         rejects_wrong_node_types(&affected, |value| validate_rustsec_affected(value).is_ok());
   9664         assert!(validate_rustsec_affected(&Value::Null).is_ok());
   9665         for path in [
   9666             "",
   9667             "example",
   9668             "::method",
   9669             "example::",
   9670             "0crate::method",
   9671             "crate::bad-name",
   9672             "crate::méthod",
   9673         ] {
   9674             assert!(!valid_rust_function_path(path), "{path}");
   9675         }
   9676         rejects_wrong_node_types(
   9677             &json!({"patched":[">=1.0.0"], "unaffected":["<0.5.0"]}),
   9678             |value| validate_rustsec_versions(value).is_ok(),
   9679         );
   9680     }
   9681 
   9682     #[test]
   9683     fn rustsec_advisory_optional_metadata_is_validated_when_present() {
   9684         let mut fixture = SyntheticFixture::new().unwrap();
   9685         fixture
   9686             .set_rustsec_finding("RUSTSEC-2099-0001", "example", "1.0.0")
   9687             .unwrap();
   9688         let report = fixture.read_report_value(ProviderId::Rustsec).unwrap();
   9689         let raw: Value = serde_json::from_str(
   9690             report["workload_result"][0]["raw_scanner_output"]
   9691                 .as_str()
   9692                 .unwrap(),
   9693         )
   9694         .unwrap();
   9695         let mut advisory = raw["vulnerabilities"]["list"][0]["advisory"].clone();
   9696         for key in ["aliases", "categories", "keywords", "references", "related"] {
   9697             advisory[key] = json!(["example"]);
   9698         }
   9699         for key in ["cvss", "informational", "url"] {
   9700             advisory[key] = json!("example");
   9701         }
   9702         advisory["withdrawn"] = json!("2099-02-28");
   9703         rejects_wrong_node_types(&advisory, |value| {
   9704             validate_rustsec_advisory(value, "example").is_ok()
   9705         });
   9706         for (key, invalid) in [
   9707             ("package", "another"),
   9708             ("id", "CVE-2099-0001"),
   9709             ("license", ""),
   9710             ("date", "2099-02-29"),
   9711             ("withdrawn", "2099/01/01"),
   9712         ] {
   9713             let mut changed = advisory.clone();
   9714             changed[key] = json!(invalid);
   9715             assert!(
   9716                 validate_rustsec_advisory(&changed, "example").is_err(),
   9717                 "{key}"
   9718             );
   9719         }
   9720         for date in [
   9721             "",
   9722             "2099-01001",
   9723             "2099/01-01",
   9724             "2099-01/01",
   9725             "209x-01-01",
   9726             "2099-13-01",
   9727             "2099-01-32",
   9728         ] {
   9729             assert!(!valid_rustsec_date(&json!(date)), "{date}");
   9730         }
   9731         assert!(valid_rustsec_date(&json!("2000-02-29")));
   9732     }
   9733 
   9734     #[test]
   9735     fn manifest_rejects_each_independently_changed_binding() {
   9736         let fixture = SyntheticFixture::new().unwrap();
   9737         validate_manifest(&fixture.manifest, &fixture.request).unwrap();
   9738         let original = serde_json::to_value(&fixture.manifest).unwrap();
   9739         for (key, value) in original.as_object().unwrap() {
   9740             let mut changed = original.clone();
   9741             changed[key] = match value {
   9742                 Value::String(_) => json!("wrong-binding"),
   9743                 Value::Array(_) => json!([]),
   9744                 Value::Object(_) => {
   9745                     let mut candidate = value.clone();
   9746                     candidate["digest"] = json!("f".repeat(64));
   9747                     candidate
   9748                 }
   9749                 _ => panic!("unexpected manifest field {key}"),
   9750             };
   9751             if changed == original {
   9752                 continue;
   9753             }
   9754             let manifest: SnapshotManifest = serde_json::from_value(changed).unwrap();
   9755             assert!(
   9756                 validate_manifest(&manifest, &fixture.request).is_err(),
   9757                 "unbound manifest field {key}"
   9758             );
   9759         }
   9760     }
   9761 
   9762     #[test]
   9763     fn trusted_authority_rejects_each_changed_envelope_field() {
   9764         let mut fixture = SyntheticFixture::new().unwrap();
   9765         for slot in 0..5 {
   9766             let original = match slot {
   9767                 0 => &fixture.request.producer_request,
   9768                 1 => &fixture.request.step_297_tool_manifest,
   9769                 2 => &fixture.request.fresh_tool_observation,
   9770                 3 => &fixture.request.nvd_network_trace,
   9771                 _ => &fixture.request.provider_execution_evidence,
   9772             }
   9773             .clone();
   9774             let value: Value = serde_json::from_slice(&original).unwrap();
   9775             for (key, field) in value.as_object().unwrap() {
   9776                 // These two values are bound by manifest admission after the
   9777                 // trusted envelope has been admitted; that boundary is above.
   9778                 if matches!(
   9779                     key.as_str(),
   9780                     "candidate_advisory_input_sha256" | "suppressions"
   9781                 ) {
   9782                     continue;
   9783                 }
   9784                 let mut changed = value.clone();
   9785                 changed[key] = match field {
   9786                     Value::String(_) => json!("wrong-binding"),
   9787                     Value::Number(_) => json!(u64::MAX),
   9788                     Value::Array(_) => json!([]),
   9789                     _ => panic!("unexpected authority field {key}"),
   9790                 };
   9791                 let bytes = canonical_authority_bytes(&changed).unwrap();
   9792                 match slot {
   9793                     0 => fixture.request.producer_request = bytes,
   9794                     1 => fixture.request.step_297_tool_manifest = bytes,
   9795                     2 => fixture.request.fresh_tool_observation = bytes,
   9796                     3 => fixture.request.nvd_network_trace = bytes,
   9797                     _ => fixture.request.provider_execution_evidence = bytes,
   9798                 }
   9799                 assert!(
   9800                     validate_trusted_authority(&fixture.request).is_err(),
   9801                     "slot {slot} field {key}"
   9802                 );
   9803                 match slot {
   9804                     0 => fixture.request.producer_request = original.clone(),
   9805                     1 => fixture.request.step_297_tool_manifest = original.clone(),
   9806                     2 => fixture.request.fresh_tool_observation = original.clone(),
   9807                     3 => fixture.request.nvd_network_trace = original.clone(),
   9808                     _ => fixture.request.provider_execution_evidence = original.clone(),
   9809                 }
   9810             }
   9811         }
   9812         fixture.request.producer_request.push(b' ');
   9813         assert!(validate_trusted_authority(&fixture.request).is_err());
   9814     }
   9815 
   9816     #[test]
   9817     fn process_receipts_reject_identity_environment_bounds_and_order_faults() {
   9818         let fixture = SyntheticFixture::new().unwrap();
   9819         let original = serde_json::to_value(&fixture.process_receipts).unwrap();
   9820         validate_process_receipts(&fixture.process_receipts, fixture.request.evaluation_epoch)
   9821             .unwrap();
   9822         for (pointer, replacement) in [
   9823             ("/0/id", json!("unknown")),
   9824             ("/0/state", json!("failed")),
   9825             ("/0/program_sha256", json!("invalid")),
   9826             ("/0/runtime_sha256", json!(["invalid"])),
   9827             ("/0/arguments_sha256", json!("invalid")),
   9828             ("/0/environment_sha256", json!("invalid")),
   9829             ("/0/working_directory_sha256", json!("invalid")),
   9830             ("/0/working_directory/logical_uri", json!("untrusted")),
   9831             ("/0/path_binding/0/logical_role", json!("..")),
   9832             ("/0/path_binding/0/identity_sha256", json!("invalid")),
   9833             ("/0/stdin_closed", json!(false)),
   9834             ("/0/deadline_seconds", json!(0)),
   9835             ("/0/started_at_epoch", json!(0)),
   9836             ("/0/completed_at_epoch", json!(0)),
   9837             ("/0/completed_at_epoch", json!(u64::MAX)),
   9838             ("/1/started_at_epoch", json!(1)),
   9839             ("/0/stdout_byte_length", json!(67_108_865u64)),
   9840             ("/0/stderr_byte_length", json!(67_108_865u64)),
   9841             ("/0/stdout_sha256", json!("invalid")),
   9842             ("/0/stderr_sha256", json!("invalid")),
   9843             ("/0/input_sha256", json!("invalid")),
   9844             ("/0/output_sha256", json!("invalid")),
   9845             ("/0/environment/0/name", json!("UNTRUSTED")),
   9846             ("/0/environment/0/logical_value", json!("untrusted")),
   9847             ("/0/environment/0/value_sha256", json!("invalid")),
   9848         ] {
   9849             let mut changed = original.clone();
   9850             *changed.pointer_mut(pointer).expect(pointer) = replacement;
   9851             let receipts: Vec<TrustedProcessReceipt> = serde_json::from_value(changed).unwrap();
   9852             assert!(
   9853                 validate_process_receipts(&receipts, fixture.request.evaluation_epoch).is_err(),
   9854                 "{pointer}"
   9855             );
   9856         }
   9857         let mut duplicate_binding = fixture.process_receipts.clone();
   9858         let repeated = duplicate_binding[0].path_binding[0].clone();
   9859         duplicate_binding[0].path_binding.push(repeated);
   9860         assert!(
   9861             validate_process_receipts(&duplicate_binding, fixture.request.evaluation_epoch)
   9862                 .is_err()
   9863         );
   9864         for state in [
   9865             OperationState::Failed,
   9866             OperationState::TimedOut,
   9867             OperationState::Unavailable,
   9868         ] {
   9869             assert!(require_complete(state).is_err());
   9870         }
   9871     }
   9872 
   9873     #[test]
   9874     fn bounded_tokens_dates_and_canonical_counters_accept_only_their_grammar() {
   9875         for value in [
   9876             "",
   9877             "01",
   9878             "-1",
   9879             "+1",
   9880             "1.0",
   9881             "1e2",
   9882             " 1",
   9883             "١",
   9884             "18446744073709551616",
   9885         ] {
   9886             assert_eq!(parse_canonical_u64(value), None, "{value}");
   9887         }
   9888         assert_eq!(parse_canonical_u64("0"), Some(0));
   9889         assert_eq!(parse_canonical_u64("18446744073709551615"), Some(u64::MAX));
   9890         for value in [
   9891             "",
   9892             "x",
   9893             "RUSTSEC-2026",
   9894             "RUSTSEC-26-0001",
   9895             "RUSTSEC-202x-0001",
   9896             "RUSTSEC-2026-abcd",
   9897             "CVE-2026-0001",
   9898         ] {
   9899             assert!(!valid_advisory_id(ProviderId::Rustsec, value), "{value}");
   9900         }
   9901         assert!(valid_advisory_id(ProviderId::OwaspNvd, "CVE-2026-12345"));
   9902         assert!(valid_hex("0123456789abcdef", 16));
   9903         for value in ["G", "g", "é", "\0"] {
   9904             assert!(!valid_hex(value, value.len()));
   9905         }
   9906         assert!(valid_identifier("relative/normal"));
   9907         for value in [
   9908             "",
   9909             "/absolute",
   9910             "../parent",
   9911             "a/../b",
   9912             "a\\b",
   9913             "a\0b",
   9914             "a\nb",
   9915         ] {
   9916             assert!(!valid_identifier(value), "{value:?}");
   9917         }
   9918         assert!(valid_exact_identity_token("a-Z_1./:@+", 32));
   9919         for value in ["..", "with space", "é", "\n", "long"] {
   9920             assert!(!valid_exact_identity_token(value, 3));
   9921         }
   9922         assert!(valid_ascii_text("", 0, true));
   9923         assert!(valid_ascii_text("abc", 3, false));
   9924         for value in ["", "abcd", "é", "a\nb"] {
   9925             assert!(!valid_ascii_text(value, 3, false));
   9926         }
   9927         assert!(valid_exact_string_array(&json!(["abc"]), 1, 3));
   9928         for value in [
   9929             json!(["a", "b"]),
   9930             json!(["abcd"]),
   9931             json!([""]),
   9932             json!([null]),
   9933             json!({}),
   9934         ] {
   9935             assert!(!valid_exact_string_array(&value, 1, 3));
   9936         }
   9937         assert!(require_strict_canonical_order(&[1, 2]).is_ok());
   9938         assert!(require_strict_canonical_order(&[2, 1]).is_err());
   9939         assert!(require_strict_canonical_order(&[1, 1]).is_err());
   9940     }
   9941 
   9942     #[test]
   9943     fn owasp_optional_rows_are_validated_and_duplicates_are_rejected() {
   9944         let identifiers = json!([{"id":"cpe:/a:example:library:1.0", "notes":"note", "url":"https://example.test", "confidence":"HIGH"}]);
   9945         rejects_wrong_node_types(&identifiers, |value| {
   9946             parse_owasp_identifiers(Some(value), true).is_ok()
   9947         });
   9948         assert!(parse_owasp_identifiers(Some(&identifiers), false).is_err());
   9949         assert!(parse_owasp_identifiers(None, false).unwrap().is_empty());
   9950         let included = json!([{"reference":"project:root", "type":"DIRECT"}]);
   9951         rejects_wrong_node_types(&included, |value| {
   9952             parse_owasp_included_by(Some(value)).is_ok()
   9953         });
   9954         rejects_unknown_object_fields(&included, &["/0"], |value| {
   9955             parse_owasp_included_by(Some(value)).is_ok()
   9956         });
   9957         assert!(parse_owasp_included_by(None).unwrap().is_empty());
   9958         for value in [
   9959             json!([]),
   9960             json!([{"id":"b"},{"id":"a"}]),
   9961             json!([{"id":"a"},{"id":"a"}]),
   9962         ] {
   9963             assert!(parse_owasp_identifiers(Some(&value), false).is_err());
   9964         }
   9965         for value in [
   9966             json!([]),
   9967             json!([{"reference":"b"},{"reference":"a"}]),
   9968             json!([{"reference":"a"},{"reference":"a"}]),
   9969         ] {
   9970             assert!(parse_owasp_included_by(Some(&value)).is_err());
   9971         }
   9972         let mut evidence = json!({});
   9973         for (key, kind) in [
   9974             ("productEvidence", "product"),
   9975             ("vendorEvidence", "vendor"),
   9976             ("versionEvidence", "version"),
   9977         ] {
   9978             evidence[key] = json!([{"confidence":"HIGH", "name":"name", "source":"manifest", "type":kind, "value":"value"}]);
   9979         }
   9980         rejects_wrong_node_types(&evidence, valid_owasp_evidence);
   9981         rejects_unknown_object_fields(
   9982             &evidence,
   9983             &[
   9984                 "",
   9985                 "/productEvidence/0",
   9986                 "/vendorEvidence/0",
   9987                 "/versionEvidence/0",
   9988             ],
   9989             valid_owasp_evidence,
   9990         );
   9991         evidence["productEvidence"][0]["type"] = json!("vendor");
   9992         assert!(!valid_owasp_evidence(&evidence));
   9993         let references = json!([{"source":"NVD", "name":"advisory", "url":"https://example.test"}]);
   9994         rejects_wrong_node_types(&references, valid_owasp_references);
   9995         rejects_unknown_object_fields(&references, &["/0"], valid_owasp_references);
   9996         assert!(valid_owasp_references(&json!([])));
   9997         let software = json!([{"software":{
   9998             "id":"cpe:/a:example:library:1.0", "versionEndExcluding":"2.0", "versionEndIncluding":"1.9",
   9999             "versionStartExcluding":"0.1", "versionStartIncluding":"0.2", "vulnerabilityIdMatched":"true", "vulnerable":"false"
  10000         }}]);
  10001         rejects_wrong_node_types(&software, |value| {
  10002             parse_owasp_vulnerable_software(value).is_ok()
  10003         });
  10004         rejects_unknown_object_fields(&software, &["/0", "/0/software"], |value| {
  10005             parse_owasp_vulnerable_software(value).is_ok()
  10006         });
  10007         let mut duplicated = software.clone();
  10008         duplicated.as_array_mut().unwrap().push(software[0].clone());
  10009         assert!(parse_owasp_vulnerable_software(&duplicated).is_err());
  10010         for (key, value) in [
  10011             ("vulnerabilityIdMatched", "false"),
  10012             ("vulnerable", "true"),
  10013             ("id", "invalid"),
  10014         ] {
  10015             let mut changed = software.clone();
  10016             changed[0]["software"][key] = json!(value);
  10017             assert!(parse_owasp_vulnerable_software(&changed).is_err());
  10018         }
  10019         let vulnerabilities = json!([{
  10020             "name":"CVE-2099-0001", "description":"description", "notes":"notes", "references":references,
  10021             "source":"NVD", "severity":"HIGH", "unscored":"true", "cwes":["CWE-20"], "vulnerableSoftware":software,
  10022             "cvssv2": {"accessComplexity":"LOW", "accessVector":"NETWORK", "authenticationr":"NONE", "availabilityImpact":"PARTIAL", "confidentialityImpact":"PARTIAL", "integrityImpact":"PARTIAL", "score":7.5, "severity":"HIGH"},
  10023             "cvssv3": {"attackComplexity":"LOW", "attackVector":"NETWORK", "availabilityImpact":"HIGH", "baseScore":9.8, "baseSeverity":"CRITICAL", "confidentialityImpact":"HIGH", "integrityImpact":"HIGH", "privilegesRequired":"NONE", "scope":"UNCHANGED", "userInteraction":"NONE"},
  10024             "cvssv4": {"baseScore":9.3, "environmentalScore":9.0, "threatScore":8.0, "version":"4.0"}
  10025         }]);
  10026         rejects_wrong_node_types(&vulnerabilities, |value| {
  10027             parse_owasp_vulnerabilities(Some(value)).is_ok()
  10028         });
  10029         rejects_unknown_object_fields(
  10030             &vulnerabilities,
  10031             &["/0", "/0/cvssv2", "/0/cvssv3", "/0/cvssv4"],
  10032             |value| parse_owasp_vulnerabilities(Some(value)).is_ok(),
  10033         );
  10034         let mut unsorted_cwes = vulnerabilities.clone();
  10035         unsorted_cwes[0]["cwes"] = json!(["CWE-99", "CWE-20"]);
  10036         assert!(parse_owasp_vulnerabilities(Some(&unsorted_cwes)).is_err());
  10037         assert!(parse_owasp_vulnerabilities(None).unwrap().is_empty());
  10038         for (key, value) in [
  10039             ("name", "RUSTSEC-2099-0001"),
  10040             ("source", "OTHER"),
  10041             ("unscored", "false"),
  10042         ] {
  10043             let mut changed = vulnerabilities.clone();
  10044             changed[0][key] = json!(value);
  10045             assert!(parse_owasp_vulnerabilities(Some(&changed)).is_err());
  10046         }
  10047         let mut duplicated = vulnerabilities.clone();
  10048         duplicated
  10049             .as_array_mut()
  10050             .unwrap()
  10051             .push(vulnerabilities[0].clone());
  10052         assert!(parse_owasp_vulnerabilities(Some(&duplicated)).is_err());
  10053     }
  10054 
  10055     #[test]
  10056     fn owasp_related_artifacts_bind_file_digests_and_virtual_lineage() {
  10057         let physical = json!([{"fileName":"example.jar", "filePath":"/fixture/example.jar", "isVirtual":false,
  10058             "md5":"1".repeat(32), "sha1":"2".repeat(40), "sha256":"3".repeat(64),
  10059             "packageIds":[{"id":"pkg:maven/example/library@1.0"}]}]);
  10060         let expected = BTreeMap::new();
  10061         rejects_wrong_node_types(&physical, |value| {
  10062             parse_owasp_related(Some(value), &expected).is_ok()
  10063         });
  10064         rejects_unknown_object_fields(&physical, &["/0"], |value| {
  10065             parse_owasp_related(Some(value), &expected).is_ok()
  10066         });
  10067         let virtual_row =
  10068             json!([{"fileName":"project", "filePath":"project:root", "isVirtual":true}]);
  10069         rejects_wrong_node_types(&virtual_row, |value| {
  10070             parse_owasp_related(Some(value), &expected).is_ok()
  10071         });
  10072         for key in ["md5", "sha1", "sha256"] {
  10073             let mut changed = physical.clone();
  10074             changed[0][key] = json!("invalid");
  10075             assert!(parse_owasp_related(Some(&changed), &expected).is_err());
  10076             let mut changed = virtual_row.clone();
  10077             changed[0][key] = json!("invalid");
  10078             assert!(parse_owasp_related(Some(&changed), &expected).is_err());
  10079         }
  10080         let mut repeated = physical.clone();
  10081         repeated.as_array_mut().unwrap().push(physical[0].clone());
  10082         assert!(parse_owasp_related(Some(&repeated), &expected).is_err());
  10083         assert!(
  10084             parse_owasp_related(None, &expected)
  10085                 .unwrap()
  10086                 .aliases
  10087                 .is_empty()
  10088         );
  10089         for value in [
  10090             "",
  10091             "pkg:cargo/example@1.0",
  10092             "pkg:maven/example",
  10093             "pkg:maven/example@1.0",
  10094             "pkg:maven//example@1.0",
  10095             "pkg:maven/example/library@",
  10096             "pkg:maven/example/library@1.0?x",
  10097             "pkg:maven/example/library%20@1.0",
  10098         ] {
  10099             assert!(parse_exact_package_url(value).is_err(), "{value}");
  10100         }
  10101         assert_eq!(
  10102             parse_exact_package_url("pkg:maven/example/library@1.0").unwrap(),
  10103             (
  10104                 "maven".into(),
  10105                 "example".into(),
  10106                 "library".into(),
  10107                 "1.0".into()
  10108             )
  10109         );
  10110     }
  10111 
  10112     #[test]
  10113     fn nvd_pagination_requires_complete_contiguous_pages_and_time_windows() {
  10114         let fixture = SyntheticFixture::new().unwrap();
  10115         let producer: TrustedProducerRequest =
  10116             parse_canonical_authority(&fixture.request.producer_request).unwrap();
  10117         let baseline: NvdNetworkTrace =
  10118             parse_canonical_authority(&fixture.request.nvd_network_trace).unwrap();
  10119         let receipts = &fixture.process_receipts;
  10120         validate_nvd_trace(&producer, &baseline, receipts).unwrap();
  10121         let original = serde_json::to_value(&baseline).unwrap();
  10122         for (pointer, replacement) in [
  10123             ("/request/0/sequence", json!(0)),
  10124             ("/request/0/method", json!("POST")),
  10125             ("/request/0/scheme", json!("http")),
  10126             ("/request/0/authority", json!("example.test")),
  10127             ("/request/0/path", json!("/other")),
  10128             ("/request/0/started_at_epoch", json!(0)),
  10129             ("/request/0/completed_at_epoch", json!(0)),
  10130             ("/request/0/completed_at_epoch", json!(u64::MAX)),
  10131             ("/request/0/response_status", json!(500)),
  10132             ("/request/0/response_byte_length", json!(0)),
  10133             (
  10134                 "/request/0/response_byte_length",
  10135                 json!(MAX_NVD_AGGREGATE_BYTES + 1),
  10136             ),
  10137             (
  10138                 "/request/0/response_byte_length",
  10139                 json!(MAX_NVD_RESPONSE_BYTES + 1),
  10140             ),
  10141             ("/request/0/response_sha256", json!("invalid")),
  10142             ("/request/0/response_start_index", json!(1)),
  10143             ("/request/0/response_results_per_page", json!(0)),
  10144             ("/request/0/response_total_results", json!(0)),
  10145         ] {
  10146             let mut changed = original.clone();
  10147             *changed.pointer_mut(pointer).unwrap() = replacement;
  10148             let trace: NvdNetworkTrace = serde_json::from_value(changed).unwrap();
  10149             assert!(
  10150                 validate_nvd_trace(&producer, &trace, receipts).is_err(),
  10151                 "{pointer}"
  10152             );
  10153         }
  10154         for key in [
  10155             "lastModStartDate",
  10156             "lastModEndDate",
  10157             "startIndex",
  10158             "resultsPerPage",
  10159         ] {
  10160             let mut changed = baseline.clone();
  10161             changed.request[0]
  10162                 .query
  10163                 .iter_mut()
  10164                 .find(|row| row.name == key)
  10165                 .unwrap()
  10166                 .value = "invalid".into();
  10167             assert!(
  10168                 validate_nvd_trace(&producer, &changed, receipts).is_err(),
  10169                 "{key}"
  10170             );
  10171         }
  10172         let set_query = |row: &mut NvdRequestTrace, name: &str, value: String| {
  10173             row.query
  10174                 .iter_mut()
  10175                 .find(|query| query.name == name)
  10176                 .unwrap()
  10177                 .value = value;
  10178         };
  10179         let mut pages = baseline.clone();
  10180         assert_eq!(pages.request.len(), 1);
  10181         let page_size = pages.request[0].response_results_per_page;
  10182         pages.request[0].response_total_results = page_size + 1;
  10183         let mut second = pages.request[0].clone();
  10184         second.sequence = 2;
  10185         second.started_at_epoch = second.completed_at_epoch;
  10186         second.response_start_index = page_size;
  10187         set_query(&mut second, "startIndex", page_size.to_string());
  10188         pages.request.push(second);
  10189         validate_nvd_trace(&producer, &pages, receipts).unwrap();
  10190         let mut total_drift = pages.clone();
  10191         total_drift.request[1].response_total_results += 1;
  10192         assert!(validate_nvd_trace(&producer, &total_drift, receipts).is_err());
  10193         let mut backwards_time = pages.clone();
  10194         backwards_time.request[1].started_at_epoch = backwards_time.request[0].started_at_epoch;
  10195         assert!(validate_nvd_trace(&producer, &backwards_time, receipts).is_err());
  10196         let mut initial_offset = baseline.clone();
  10197         initial_offset.request[0].response_start_index = 1;
  10198         set_query(&mut initial_offset.request[0], "startIndex", "1".into());
  10199         assert!(validate_nvd_trace(&producer, &initial_offset, receipts).is_err());
  10200         let mut extra_query = baseline.clone();
  10201         let repeated_query = extra_query.request[0].query[0].clone();
  10202         extra_query.request[0].query.push(repeated_query);
  10203         assert!(validate_nvd_trace(&producer, &extra_query, receipts).is_err());
  10204         let mut reordered = baseline.clone();
  10205         reordered.request[0].query.swap(0, 1);
  10206         assert!(validate_nvd_trace(&producer, &reordered, receipts).is_err());
  10207         let mut truncated = pages.clone();
  10208         truncated.request.pop();
  10209         assert!(validate_nvd_trace(&producer, &truncated, receipts).is_err());
  10210         for gap in [0, page_size + 1] {
  10211             let mut changed = pages.clone();
  10212             changed.request[1].response_start_index = gap;
  10213             set_query(&mut changed.request[1], "startIndex", gap.to_string());
  10214             assert!(validate_nvd_trace(&producer, &changed, receipts).is_err());
  10215         }
  10216         let mut windows = baseline.clone();
  10217         let end = windows.request[0]
  10218             .query
  10219             .iter()
  10220             .find(|row| row.name == "lastModEndDate")
  10221             .unwrap()
  10222             .value
  10223             .clone();
  10224         let next_start = parse_report_epoch(&end).unwrap() + 1;
  10225         let mut second = windows.request[0].clone();
  10226         second.sequence = 2;
  10227         second.started_at_epoch = second.completed_at_epoch;
  10228         set_query(
  10229             &mut second,
  10230             "lastModStartDate",
  10231             format_report_epoch(next_start).unwrap(),
  10232         );
  10233         set_query(
  10234             &mut second,
  10235             "lastModEndDate",
  10236             format_report_epoch(next_start + 1).unwrap(),
  10237         );
  10238         windows.request.push(second);
  10239         validate_nvd_trace(&producer, &windows, receipts).unwrap();
  10240         let mut offset_window = windows.clone();
  10241         offset_window.request[1].response_start_index = 1;
  10242         set_query(&mut offset_window.request[1], "startIndex", "1".into());
  10243         assert!(validate_nvd_trace(&producer, &offset_window, receipts).is_err());
  10244         let mut reversed_window = windows.clone();
  10245         set_query(
  10246             &mut reversed_window.request[1],
  10247             "lastModStartDate",
  10248             format_report_epoch(next_start + 2).unwrap(),
  10249         );
  10250         assert!(validate_nvd_trace(&producer, &reversed_window, receipts).is_err());
  10251         set_query(
  10252             &mut windows.request[1],
  10253             "lastModStartDate",
  10254             format_report_epoch(next_start + 1).unwrap(),
  10255         );
  10256         assert!(validate_nvd_trace(&producer, &windows, receipts).is_err());
  10257     }
  10258 }