From 6189c4a4c5d03fc091208a7ad04eb90cb374ea3e Mon Sep 17 00:00:00 2001 From: pitboss Date: Sat, 16 May 2026 13:05:27 -0500 Subject: [PATCH] [pitboss/grind] deferred session-0023 (20260516T052512Z-20f8) --- src/baseline.rs | 1 + src/chain/feasibility.rs | 1 + src/chain/reverify.rs | 2 + src/dynamic/repro.rs | 1 + src/dynamic/verify.rs | 114 ++++++++++++++++++++++++++- src/evidence.rs | 44 +++++++++++ src/rank.rs | 5 ++ tests/chain_reverify.rs | 1 + tests/common/fixture_harness.rs | 3 + tests/console_snapshot.rs | 4 + tests/fix_validation_e2e.rs | 2 + tests/go_fixtures.rs | 1 + tests/java_fixtures.rs | 1 + tests/js_fixtures.rs | 1 + tests/json_snapshot.rs | 3 + tests/php_fixtures.rs | 1 + tests/repro_determinism.rs | 1 + tests/repro_hermetic.rs | 1 + tests/sandbox_hardening_macos.rs | 105 ++++++++++++++++++++++++ tests/sarif_dynamic_verdict_tests.rs | 6 ++ 20 files changed, 297 insertions(+), 1 deletion(-) diff --git a/src/baseline.rs b/src/baseline.rs index ac9a8ea1..14afb829 100644 --- a/src/baseline.rs +++ b/src/baseline.rs @@ -448,6 +448,7 @@ mod tests { differential: None, replay_stable: None, wrong: None, + hardening_outcome: None, }), ..Default::default() }); diff --git a/src/chain/feasibility.rs b/src/chain/feasibility.rs index fe021db6..63da9be1 100644 --- a/src/chain/feasibility.rs +++ b/src/chain/feasibility.rs @@ -110,6 +110,7 @@ mod tests { differential: None, replay_stable: None, wrong: None, + hardening_outcome: None, } } diff --git a/src/chain/reverify.rs b/src/chain/reverify.rs index ae0d7849..c18905dc 100644 --- a/src/chain/reverify.rs +++ b/src/chain/reverify.rs @@ -131,6 +131,7 @@ impl CompositeReverifier for DefaultCompositeReverifier { differential: None, replay_stable: None, wrong: None, + hardening_outcome: None, } } } @@ -256,6 +257,7 @@ mod tests { differential: None, replay_stable: None, wrong: None, + hardening_outcome: None, } } diff --git a/src/dynamic/repro.rs b/src/dynamic/repro.rs index 620780c4..84a13d20 100644 --- a/src/dynamic/repro.rs +++ b/src/dynamic/repro.rs @@ -687,6 +687,7 @@ mod tests { differential: None, replay_stable: None, wrong: None, + hardening_outcome: None, } } diff --git a/src/dynamic/verify.rs b/src/dynamic/verify.rs index 6db66208..d8565bc1 100644 --- a/src/dynamic/verify.rs +++ b/src/dynamic/verify.rs @@ -14,7 +14,9 @@ use crate::dynamic::spec::{HarnessSpec, SPEC_FORMAT_VERSION}; use crate::dynamic::stubs::StubHarness; use crate::dynamic::telemetry::{self, SamplingPolicy, TelemetryEvent}; use crate::dynamic::toolchain; -use crate::evidence::{InconclusiveReason, SpecDerivationStrategy, UnsupportedReason}; +use crate::evidence::{HardeningSummary, InconclusiveReason, SpecDerivationStrategy, UnsupportedReason}; +#[cfg(target_os = "linux")] +use crate::evidence::HardeningPrimitive; use crate::summary::GlobalSummaries; use crate::utils::config::Config; use std::path::Path; @@ -305,6 +307,7 @@ fn entry_kind_unsupported_verdict( differential: None, replay_stable: None, wrong: None, + hardening_outcome: None, } } @@ -349,6 +352,7 @@ fn spec_derivation_failed_verdict( differential: None, replay_stable: None, wrong: None, + hardening_outcome: None, }; } @@ -367,6 +371,7 @@ fn spec_derivation_failed_verdict( differential: None, replay_stable: None, wrong: None, + hardening_outcome: None, } } @@ -474,6 +479,7 @@ pub fn verify_finding(diag: &Diag, opts: &VerifyOptions) -> VerifyResult { differential: None, replay_stable: None, wrong: None, + hardening_outcome: None, }; } @@ -558,6 +564,7 @@ pub fn verify_finding(diag: &Diag, opts: &VerifyOptions) -> VerifyResult { differential: None, replay_stable: None, wrong: None, + hardening_outcome: None, }; } @@ -588,6 +595,7 @@ pub fn verify_finding(diag: &Diag, opts: &VerifyOptions) -> VerifyResult { differential: None, replay_stable: None, wrong: None, + hardening_outcome: None, }; } } @@ -732,6 +740,91 @@ pub fn verify_finding(diag: &Diag, opts: &VerifyOptions) -> VerifyResult { } +/// Project the platform-cfg'd [`crate::dynamic::sandbox::HardeningRecord`] +/// into the portable [`HardeningSummary`] that lands on +/// [`VerifyResult::hardening_outcome`]. Returns `None` when the run did +/// not record a hardening outcome (docker backend, non-Linux/non-macOS +/// host, or `Standard` profile on a host whose backend skipped the wrap). +/// +/// Exposed for tests so a `sandbox::run`-driven probe can assert that the +/// projection lands the same record `build_verdict` would stamp on a +/// `Confirmed` `VerifyResult` from the same triggering attempt. +pub fn summarize_hardening( + outcome: &crate::dynamic::sandbox::SandboxOutcome, +) -> Option { + use crate::dynamic::sandbox::HardeningRecord; + let record = outcome.hardening_outcome.as_ref()?; + match record { + #[cfg(target_os = "linux")] + HardeningRecord::Linux(o) => { + use crate::dynamic::sandbox::process_linux::{ + HardeningLevel, PrimitiveStatus, ProcessHardeningProfileTag, + }; + fn status_str(s: PrimitiveStatus) -> (String, Option) { + match s { + PrimitiveStatus::Skipped => ("skipped".to_owned(), None), + PrimitiveStatus::Applied => ("applied".to_owned(), None), + PrimitiveStatus::Failed(errno) => ("failed".to_owned(), Some(errno)), + } + } + let primitives = [ + ("no_new_privs", o.no_new_privs), + ("rlimit_cpu", o.rlimit_cpu), + ("rlimit_nofile", o.rlimit_nofile), + ("rlimit_as", o.rlimit_as), + ("unshare", o.unshare), + ("chroot", o.chroot), + ("seccomp", o.seccomp), + ] + .into_iter() + .map(|(name, st)| { + let (status, errno) = status_str(st); + HardeningPrimitive { + name: name.to_owned(), + status, + errno, + } + }) + .collect(); + let level = match o.level() { + HardeningLevel::Baseline => "baseline", + HardeningLevel::Full => "full", + HardeningLevel::Partial => "partial", + HardeningLevel::None => "none", + }; + // The Linux backend uses the same `.sb`-style profile name + // surface (Standard / Strict) as macOS via the profile tag. + let profile = match o.profile { + ProcessHardeningProfileTag::Standard => String::new(), + ProcessHardeningProfileTag::Strict => "strict".to_owned(), + }; + Some(HardeningSummary { + backend: "linux-process".to_owned(), + level: level.to_owned(), + profile, + primitives, + }) + } + #[cfg(target_os = "macos")] + HardeningRecord::Macos(o) => { + use crate::dynamic::sandbox::process_macos::HardeningLevel; + let level = match o.level { + HardeningLevel::Trusted => "trusted", + HardeningLevel::Sandboxed => "sandboxed", + HardeningLevel::Failed => "failed", + }; + Some(HardeningSummary { + backend: "macos-process".to_owned(), + level: level.to_owned(), + profile: o.profile.clone(), + primitives: Vec::new(), + }) + } + #[cfg(not(any(target_os = "linux", target_os = "macos")))] + _ => None, + } +} + fn build_verdict( finding_id: &str, spec: &HarnessSpec, @@ -762,6 +855,7 @@ fn build_verdict( .get(i) .map(|p| p.bytes) .unwrap_or(b""); + let hardening_outcome = summarize_hardening(&run.attempts[i].outcome); // Emit repro artifact. let repro_result = crate::dynamic::repro::write( @@ -780,6 +874,7 @@ fn build_verdict( differential: run.differential.clone(), replay_stable: None, wrong: None, + hardening_outcome: hardening_outcome.clone(), }, &run.harness_source, &run.entry_source, @@ -802,6 +897,7 @@ fn build_verdict( differential: run.differential, replay_stable: None, wrong: None, + hardening_outcome, }; } @@ -817,6 +913,7 @@ fn build_verdict( differential: run.differential, replay_stable: None, wrong: None, + hardening_outcome, } } else if run.unrelated_crash { // Phase 08 §C.4: the harness crashed but the death @@ -838,6 +935,7 @@ fn build_verdict( differential: None, replay_stable: None, wrong: None, + hardening_outcome: None, } } else if run.no_benign_control { // Phase 07 §4.1: vuln oracle + sink-hit fired but the @@ -858,6 +956,7 @@ fn build_verdict( differential: None, replay_stable: None, wrong: None, + hardening_outcome: None, } } else if let Some(d) = run.differential.as_ref() { // Differential ran but didn't produce `Confirmed`. Map @@ -881,6 +980,7 @@ fn build_verdict( differential: run.differential, replay_stable: None, wrong: None, + hardening_outcome: None, } } crate::evidence::DifferentialVerdict::ReversedDifferential => { @@ -900,6 +1000,7 @@ fn build_verdict( differential: run.differential, replay_stable: None, wrong: None, + hardening_outcome: None, } } crate::evidence::DifferentialVerdict::Confirmed @@ -915,6 +1016,7 @@ fn build_verdict( differential: run.differential, replay_stable: None, wrong: None, + hardening_outcome: None, }, } } else if run.oracle_collision { @@ -933,6 +1035,7 @@ fn build_verdict( differential: None, replay_stable: None, wrong: None, + hardening_outcome: None, } } else { VerifyResult { @@ -947,6 +1050,7 @@ fn build_verdict( differential: None, replay_stable: None, wrong: None, + hardening_outcome: None, } } } @@ -962,6 +1066,7 @@ fn build_verdict( differential: None, replay_stable: None, wrong: None, + hardening_outcome: None, }, Err(RunError::Harness(e)) => { // Defence-in-depth residual for `EntryKindUnsupported` from the @@ -1007,6 +1112,7 @@ fn build_verdict( differential: None, replay_stable: None, wrong: None, + hardening_outcome: None, } } Err(RunError::BuildFailed { stderr, attempts: build_att }) => VerifyResult { @@ -1021,6 +1127,7 @@ fn build_verdict( differential: None, replay_stable: None, wrong: None, + hardening_outcome: None, }, Err(RunError::Sandbox(e)) => VerifyResult { finding_id: finding_id.to_owned(), @@ -1034,6 +1141,7 @@ fn build_verdict( differential: None, replay_stable: None, wrong: None, + hardening_outcome: None, }, } } @@ -1142,6 +1250,7 @@ mod tests { differential: None, replay_stable: None, wrong: None, + hardening_outcome: None, }; // Insert. @@ -1193,6 +1302,7 @@ mod tests { differential: None, replay_stable: None, wrong: None, + hardening_outcome: None, }; insert_verdict_cache(&db_path, "spec_aaa", "hash_xyz", "", "python-3.11", &result); @@ -1230,6 +1340,7 @@ mod tests { differential: None, replay_stable: None, wrong: None, + hardening_outcome: None, }; insert_verdict_cache(db_path, "spec", "hash", "", "python-3", &result); assert!(!db_path.exists(), "insert must not create a new DB"); @@ -1286,6 +1397,7 @@ mod tests { differential: None, replay_stable: None, wrong: None, + hardening_outcome: None, }; // Insert directly with the old corpus_version bypassing the helper. diff --git a/src/evidence.rs b/src/evidence.rs index c62ddf7a..1e079869 100644 --- a/src/evidence.rs +++ b/src/evidence.rs @@ -506,6 +506,42 @@ pub struct DifferentialProbeRecord { pub payload_id: String, } +/// Per-primitive entry inside [`HardeningSummary::primitives`]. +/// +/// Mirrors the Linux process backend's `PrimitiveStatus`-per-primitive +/// table without depending on the `dynamic` feature. `status` is one of +/// `"applied"`, `"failed"`, or `"skipped"`; `errno` is populated when +/// `status == "failed"`. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct HardeningPrimitive { + pub name: String, + pub status: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub errno: Option, +} + +/// Portable, JSON-serialisable projection of the per-run hardening +/// outcome the process backend stamps on `SandboxOutcome`. +/// +/// Stored on [`VerifyResult::hardening_outcome`] so callers (eval-corpus +/// tabulator, repro round-trips, end-to-end acceptance tests) can assert +/// on the matched profile and per-primitive status without depending on +/// the platform-cfg'd `HardeningRecord` enum. `backend` is one of +/// `"linux-process"` or `"macos-process"`; `level` is the coarse outcome +/// (`"trusted"` / `"sandboxed"` / `"failed"` on macOS; +/// `"baseline"` / `"full"` / `"partial"` / `"none"` on Linux); `profile` +/// is the matched `.sb` name on macOS and empty on Linux; `primitives` +/// is empty on macOS and one entry per primitive on Linux. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct HardeningSummary { + pub backend: String, + pub level: String, + #[serde(default, skip_serializing_if = "String::is_empty")] + pub profile: String, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub primitives: Vec, +} + /// Full record of a Phase 07 differential confirmation run. /// /// Captures the rule's verdict plus the raw probe traces from both the @@ -584,6 +620,14 @@ pub struct VerifyResult { /// `wrong_confirmed` column in `tests/eval_corpus/tabulate.py`. #[serde(default, skip_serializing_if = "Option::is_none")] pub wrong: Option, + /// Phase 17/18 per-run hardening outcome, projected from the + /// triggering attempt's [`crate::dynamic::sandbox::SandboxOutcome`]. + /// Populated only when a payload actually ran under the process + /// backend on Linux or macOS and the run captured a primitive + /// outcome; `None` for docker-backend runs, host platforms with no + /// hardening primitives, or verdicts that never executed a payload. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub hardening_outcome: Option, } // ───────────────────────────────────────────────────────────────────────────── diff --git a/src/rank.rs b/src/rank.rs index 3e0c97e3..b3e3a920 100644 --- a/src/rank.rs +++ b/src/rank.rs @@ -1159,6 +1159,7 @@ mod tests { differential: None, replay_stable: None, wrong: None, + hardening_outcome: None, } } @@ -1181,6 +1182,7 @@ mod tests { differential: None, replay_stable: None, wrong: None, + hardening_outcome: None, } } @@ -1197,6 +1199,7 @@ mod tests { differential: None, replay_stable: None, wrong: None, + hardening_outcome: None, } } @@ -1213,6 +1216,7 @@ mod tests { differential: None, replay_stable: None, wrong: None, + hardening_outcome: None, } } @@ -1229,6 +1233,7 @@ mod tests { differential: None, replay_stable: None, wrong: None, + hardening_outcome: None, } } diff --git a/tests/chain_reverify.rs b/tests/chain_reverify.rs index da09d1e6..e45dae35 100644 --- a/tests/chain_reverify.rs +++ b/tests/chain_reverify.rs @@ -76,6 +76,7 @@ fn verdict(status: VerifyStatus, reason: Option) -> VerifyRe differential: None, replay_stable: None, wrong: None, + hardening_outcome: None, } } diff --git a/tests/common/fixture_harness.rs b/tests/common/fixture_harness.rs index 7eaddeb4..bdcf9d98 100644 --- a/tests/common/fixture_harness.rs +++ b/tests/common/fixture_harness.rs @@ -584,6 +584,7 @@ pub fn run_shape_fixture_lang( differential: None, replay_stable: None, wrong: None, + hardening_outcome: None, } } Err(RunError::NoPayloadsForCap) => VerifyResult { @@ -598,6 +599,7 @@ pub fn run_shape_fixture_lang( differential: None, replay_stable: None, wrong: None, + hardening_outcome: None, }, Err(e) => VerifyResult { finding_id: spec.finding_id.clone(), @@ -611,6 +613,7 @@ pub fn run_shape_fixture_lang( differential: None, replay_stable: None, wrong: None, + hardening_outcome: None, }, } } diff --git a/tests/console_snapshot.rs b/tests/console_snapshot.rs index 69dbdd55..41339e39 100644 --- a/tests/console_snapshot.rs +++ b/tests/console_snapshot.rs @@ -74,6 +74,7 @@ fn diag_with_verdict(status: VerifyStatus) -> Diag { differential: None, replay_stable: None, wrong: None, + hardening_outcome: None, }, VerifyStatus::NotConfirmed => VerifyResult { finding_id: "abc123".into(), @@ -93,6 +94,7 @@ fn diag_with_verdict(status: VerifyStatus) -> Diag { differential: None, replay_stable: None, wrong: None, + hardening_outcome: None, }, VerifyStatus::Unsupported => VerifyResult { finding_id: "abc123".into(), @@ -106,6 +108,7 @@ fn diag_with_verdict(status: VerifyStatus) -> Diag { differential: None, replay_stable: None, wrong: None, + hardening_outcome: None, }, VerifyStatus::Inconclusive => VerifyResult { finding_id: "abc123".into(), @@ -119,6 +122,7 @@ fn diag_with_verdict(status: VerifyStatus) -> Diag { differential: None, replay_stable: None, wrong: None, + hardening_outcome: None, }, }; diff --git a/tests/fix_validation_e2e.rs b/tests/fix_validation_e2e.rs index 6d20f186..35b5854d 100644 --- a/tests/fix_validation_e2e.rs +++ b/tests/fix_validation_e2e.rs @@ -55,6 +55,7 @@ fn set_verdict( differential: None, replay_stable: None, wrong: None, + hardening_outcome: None, }); } } @@ -170,6 +171,7 @@ fn new_confirmed_fails_no_new_confirmed_gate() { differential: None, replay_stable: None, wrong: None, + hardening_outcome: None, }); } } diff --git a/tests/go_fixtures.rs b/tests/go_fixtures.rs index f0f931d6..6d5697ef 100644 --- a/tests/go_fixtures.rs +++ b/tests/go_fixtures.rs @@ -61,6 +61,7 @@ mod go_fixture_tests { differential: None, replay_stable: None, wrong: None, + hardening_outcome: None, }; } diff --git a/tests/java_fixtures.rs b/tests/java_fixtures.rs index a60ac41f..bcdd8c9c 100644 --- a/tests/java_fixtures.rs +++ b/tests/java_fixtures.rs @@ -69,6 +69,7 @@ mod java_fixture_tests { differential: None, replay_stable: None, wrong: None, + hardening_outcome: None, }; } diff --git a/tests/js_fixtures.rs b/tests/js_fixtures.rs index db9120a8..490ec3e5 100644 --- a/tests/js_fixtures.rs +++ b/tests/js_fixtures.rs @@ -62,6 +62,7 @@ mod js_fixture_tests { differential: None, replay_stable: None, wrong: None, + hardening_outcome: None, }; } diff --git a/tests/json_snapshot.rs b/tests/json_snapshot.rs index e2e182d0..bd0fa9de 100644 --- a/tests/json_snapshot.rs +++ b/tests/json_snapshot.rs @@ -60,6 +60,7 @@ fn json_dynamic_verdict_confirmed_serialises_correctly() { differential: None, replay_stable: None, wrong: None, + hardening_outcome: None, }), ..Default::default() }); @@ -100,6 +101,7 @@ fn json_dynamic_verdict_not_confirmed_serialises_correctly() { differential: None, replay_stable: None, wrong: None, + hardening_outcome: None, }), ..Default::default() }); @@ -165,6 +167,7 @@ fn json_unsupported_verdict_has_reason() { differential: None, replay_stable: None, wrong: None, + hardening_outcome: None, }), ..Default::default() }); diff --git a/tests/php_fixtures.rs b/tests/php_fixtures.rs index c27fb450..5e2ef65c 100644 --- a/tests/php_fixtures.rs +++ b/tests/php_fixtures.rs @@ -61,6 +61,7 @@ mod php_fixture_tests { differential: None, replay_stable: None, wrong: None, + hardening_outcome: None, }; } diff --git a/tests/repro_determinism.rs b/tests/repro_determinism.rs index 3a197ed8..299337e9 100644 --- a/tests/repro_determinism.rs +++ b/tests/repro_determinism.rs @@ -70,6 +70,7 @@ mod repro_determinism_tests { differential: None, replay_stable: None, wrong: None, + hardening_outcome: None, } } diff --git a/tests/repro_hermetic.rs b/tests/repro_hermetic.rs index d1dbab35..3f5057b1 100644 --- a/tests/repro_hermetic.rs +++ b/tests/repro_hermetic.rs @@ -89,6 +89,7 @@ mod repro_hermetic_tests { differential: None, replay_stable: None, wrong: None, + hardening_outcome: None, } } diff --git a/tests/sandbox_hardening_macos.rs b/tests/sandbox_hardening_macos.rs index 7cb64971..7a343fdc 100644 --- a/tests/sandbox_hardening_macos.rs +++ b/tests/sandbox_hardening_macos.rs @@ -350,6 +350,111 @@ except Exception as exc: "refuse_filesystem_confirm should be false when sandbox-exec is reachable" ); } + + /// Phase 18 verifier-side projection: when a real strict run lands a + /// macOS `HardeningRecord`, `summarize_hardening` collapses it into + /// the portable [`crate::evidence::HardeningSummary`] that + /// `build_verdict` stamps on a `Confirmed` `VerifyResult`. Drives + /// the same `sandbox::run` path the existing + /// `path_traversal_payload_blocked_under_strict` test uses, then + /// asserts on the projection that would land on + /// `VerifyResult::hardening_outcome` if this run had triggered the + /// finding's oracle. + #[test] + fn summarize_hardening_lands_path_traversal_on_strict_file_io_run() { + unsafe { std::env::remove_var(SANDBOX_EXEC_BIN_ENV) }; + if !sandbox_exec_available() { + eprintln!("SKIP: /usr/bin/sandbox-exec missing — cannot exercise wrap"); + return; + } + const FILE_IO: u32 = 1 << 5; + let tmp = workdir(); + let harness = build_harness(tmp.path()); + let opts = strict_opts(FILE_IO); + let result = sandbox::run(&harness, b"", &opts).expect("sandbox::run"); + let summary = nyx_scanner::dynamic::verify::summarize_hardening(&result) + .expect("hardening summary should populate after a strict macOS run"); + assert_eq!(summary.backend, "macos-process"); + assert_eq!(summary.level, "sandboxed"); + assert_eq!( + summary.profile, "path_traversal", + "FILE_IO-cap strict run should select the path_traversal profile" + ); + assert!( + summary.primitives.is_empty(), + "macOS backend records no per-primitive entries" + ); + } + + /// Standard-profile runs leave `SandboxOutcome::hardening_outcome` + /// unset, so `summarize_hardening` returns `None` and + /// `VerifyResult::hardening_outcome` stays `None`. Companion to + /// `standard_profile_does_not_wrap_with_sandbox_exec`. + #[test] + fn summarize_hardening_returns_none_for_standard_profile_run() { + unsafe { std::env::remove_var(SANDBOX_EXEC_BIN_ENV) }; + let tmp = workdir(); + let harness = build_harness(tmp.path()); + let opts = standard_opts(); + let result = sandbox::run(&harness, b"", &opts).expect("sandbox::run"); + assert!( + nyx_scanner::dynamic::verify::summarize_hardening(&result).is_none(), + "standard profile should leave hardening_outcome unset" + ); + } + + /// Round-trip the portable summary through JSON to lock in the + /// repro-bundle wire shape: `VerifyResult::hardening_outcome` lands + /// on `expected/verdict.json` so the eval-corpus tabulator and any + /// downstream replay reads the same fields back. + #[test] + fn hardening_summary_round_trips_through_json() { + use nyx_scanner::evidence::{HardeningSummary, HardeningPrimitive}; + let summary = HardeningSummary { + backend: "macos-process".into(), + level: "sandboxed".into(), + profile: "path_traversal".into(), + primitives: vec![], + }; + let json = serde_json::to_string(&summary).expect("serialize"); + let parsed: HardeningSummary = serde_json::from_str(&json).expect("deserialize"); + assert_eq!(parsed, summary); + + // Defaults: missing `profile` and `primitives` must decode as + // empty so older `verdict.json` payloads keep round-tripping. + let minimal: HardeningSummary = + serde_json::from_str(r#"{"backend":"linux-process","level":"full"}"#) + .expect("minimal decode"); + assert_eq!(minimal.profile, ""); + assert!(minimal.primitives.is_empty()); + + // Linux-shape: per-primitive entries decode + re-encode with + // their `errno` field intact when populated. + let with_primitives = HardeningSummary { + backend: "linux-process".into(), + level: "partial".into(), + profile: "strict".into(), + primitives: vec![ + HardeningPrimitive { + name: "no_new_privs".into(), + status: "applied".into(), + errno: None, + }, + HardeningPrimitive { + name: "seccomp".into(), + status: "failed".into(), + errno: Some(1), + }, + ], + }; + let json = serde_json::to_string(&with_primitives).expect("serialize primitives"); + assert!( + json.contains("\"errno\":1"), + "errno field should survive JSON round-trip; got: {json}" + ); + let parsed: HardeningSummary = serde_json::from_str(&json).expect("decode primitives"); + assert_eq!(parsed, with_primitives); + } } // Non-macOS placeholder so `cargo nextest run --test sandbox_hardening_macos` diff --git a/tests/sarif_dynamic_verdict_tests.rs b/tests/sarif_dynamic_verdict_tests.rs index ccc98293..18db29fd 100644 --- a/tests/sarif_dynamic_verdict_tests.rs +++ b/tests/sarif_dynamic_verdict_tests.rs @@ -76,6 +76,7 @@ fn sarif_confirmed_verdict_sets_partial_fingerprint() { differential: None, replay_stable: None, wrong: None, + hardening_outcome: None, }; let result = sarif_result(diag_with_verdict(verdict)); @@ -111,6 +112,7 @@ fn sarif_not_confirmed_verdict_sets_partial_fingerprint() { differential: None, replay_stable: None, wrong: None, + hardening_outcome: None, }; let result = sarif_result(diag_with_verdict(verdict)); @@ -140,6 +142,7 @@ fn sarif_unsupported_verdict_sets_partial_fingerprint() { differential: None, replay_stable: None, wrong: None, + hardening_outcome: None, }; let result = sarif_result(diag_with_verdict(verdict)); @@ -174,6 +177,7 @@ fn sarif_inconclusive_verdict_sets_partial_fingerprint() { differential: None, replay_stable: None, wrong: None, + hardening_outcome: None, }; let result = sarif_result(diag_with_verdict(verdict)); @@ -224,6 +228,7 @@ fn sarif_confirmed_verdict_nyx_dynamic_verdict_contains_triggered_payload() { differential: None, replay_stable: None, wrong: None, + hardening_outcome: None, }; let result = sarif_result(diag_with_verdict(verdict)); @@ -257,6 +262,7 @@ fn sarif_all_four_statuses_produce_partial_fingerprint() { differential: None, replay_stable: None, wrong: None, + hardening_outcome: None, }; let result = sarif_result(diag_with_verdict(verdict));