[pitboss/grind] deferred session-0002 (20260517T044708Z-e058)

This commit is contained in:
pitboss 2026-05-17 00:46:22 -05:00
parent 3d51a3d8ae
commit 6698eb96eb
5 changed files with 237 additions and 2 deletions

View file

@ -513,6 +513,24 @@ pub enum Commands {
#[arg(long, help_heading = "Dynamic", value_name = "BACKEND")]
backend: Option<String>,
/// Process-backend hardening profile applied to every verified finding.
///
/// `standard` (default): baseline only. Linux runs no-new-privs +
/// memory rlimit; macOS skips the sandbox-exec wrap.
/// `strict`: full lockdown. Linux layers namespaces, chroot to
/// workdir, and a default-deny seccomp filter; macOS wraps the
/// harness with `sandbox-exec -f <cap>.sb`. Opt-in because
/// interpreted Linux harnesses may SIGSYS until the per-language
/// seccomp allowlists are expanded.
#[cfg_attr(not(feature = "dynamic"), arg(hide = true))]
#[arg(
long,
help_heading = "Dynamic",
value_name = "PROFILE",
value_parser = ["standard", "strict"],
)]
harden: Option<String>,
// ── Baseline / patch-validation (§M6.5) ────────────────────────
/// Read a previous scan's JSON output (or a stripped .nyx/baseline.json)
/// and diff it against the current scan on stable_hash.

View file

@ -104,6 +104,7 @@ pub fn handle_command(
verify_all_confidence,
unsafe_sandbox,
backend,
harden,
baseline,
baseline_write,
gate,
@ -346,9 +347,13 @@ pub fn handle_command(
config.scanner.verify_all_confidence = true;
}
config.scanner.verify_backend = resolved_backend.to_owned();
// --harden=<standard|strict> overrides the config default.
if let Some(ref profile) = harden {
config.scanner.harden_profile = profile.to_owned();
}
}
// Without the dynamic feature, --verify / --no-verify / --unsafe-sandbox /
// --backend are silently accepted (no-op).
// --backend / --harden are silently accepted (no-op).
#[cfg(not(feature = "dynamic"))]
{
let _ = verify;
@ -356,6 +361,7 @@ pub fn handle_command(
let _ = verify_all_confidence;
let _ = unsafe_sandbox;
let _ = backend;
let _ = harden;
}
// ── --explain-engine: print resolved config and exit ────────

View file

@ -101,7 +101,7 @@ impl VerifyOptions {
/// (`src/dynamic/runner.rs` `oob_nonce_slot` branch) while non-OOB
/// payloads continue to run against their existing oracle.
pub fn from_config(config: &Config) -> Self {
use crate::dynamic::sandbox::{NetworkPolicy, SandboxBackend};
use crate::dynamic::sandbox::{NetworkPolicy, ProcessHardeningProfile, SandboxBackend};
let backend = match config.scanner.verify_backend.as_str() {
"docker" => SandboxBackend::Docker,
"process" => SandboxBackend::Process,
@ -116,6 +116,17 @@ impl VerifyOptions {
Some(listener) => NetworkPolicy::OobOutbound { listener },
None => NetworkPolicy::None,
};
// Phase 17/18 (Track E.1/E.2): `--harden=strict` (or
// `harden_profile = "strict"` in nyx.toml) opts the verifier into
// the full process-backend lockdown. Linux engages namespace
// unshare + chroot + default-deny seccomp on top of the baseline;
// macOS wraps the harness with `sandbox-exec -f <cap>.sb` keyed
// off the per-finding expected cap (set later in `verify_finding`
// because the cap is only known once spec derivation runs).
let process_hardening = match config.scanner.harden_profile.as_str() {
"strict" => ProcessHardeningProfile::Strict,
_ => ProcessHardeningProfile::Standard,
};
// Phase 18 (Track E.2): the macOS process backend depends on
// `/usr/bin/sandbox-exec` to confine filesystem reach. When the
// binary is absent, surface that up-front so filesystem oracles
@ -135,6 +146,7 @@ impl VerifyOptions {
sandbox: SandboxOptions {
backend,
network_policy,
process_hardening,
..SandboxOptions::default()
},
project_root: None,
@ -661,6 +673,18 @@ pub fn verify_finding(diag: &Diag, opts: &VerifyOptions) -> VerifyResult {
if !stub_harness.is_empty() {
sandbox_opts.stub_harness = Some(Arc::clone(&stub_harness));
}
// Phase 17/18: when the operator opted into Strict hardening, seed
// `seccomp_caps` from the spec's expected cap so the Linux process
// backend installs the cap-minimal syscall allowlist and the macOS
// backend picks the matching `.sb` profile (`FILE_IO →
// path_traversal`, `CODE_EXEC → cmdi`, …). Standard runs leave the
// field at 0 (base allowlist / no wrap) for back-compat.
if matches!(
sandbox_opts.process_hardening,
crate::dynamic::sandbox::ProcessHardeningProfile::Strict,
) {
sandbox_opts.seccomp_caps = spec.expected_cap.bits();
}
// Phase 30: hand the runner an `Arc` clone so it can append
// `build_*` / `sandbox_started` / `oracle_*` stages from inside
// `run_spec`. The verifier still owns the trace for verdict-stage
@ -1211,6 +1235,46 @@ mod tests {
unsafe { std::env::remove_var("NYX_VERIFY_REPLAY_STABLE") };
}
#[test]
fn from_config_defaults_process_hardening_to_standard() {
use crate::dynamic::sandbox::ProcessHardeningProfile;
let opts = VerifyOptions::from_config(&Config::default());
assert!(
matches!(opts.sandbox.process_hardening, ProcessHardeningProfile::Standard),
"back-compat: missing harden_profile must keep the Standard baseline so \
existing call sites (process backend without `--harden=strict`) keep \
their pre-Phase-17 hardening matrix"
);
}
#[test]
fn from_config_picks_up_strict_harden_profile() {
use crate::dynamic::sandbox::ProcessHardeningProfile;
let mut config = Config::default();
config.scanner.harden_profile = "strict".to_owned();
let opts = VerifyOptions::from_config(&config);
assert!(
matches!(opts.sandbox.process_hardening, ProcessHardeningProfile::Strict),
"harden_profile=strict must engage the full Phase-17/18 lockdown so \
`--harden=strict` actually wraps the harness with sandbox-exec on macOS \
and layers chroot + seccomp on Linux"
);
}
#[test]
fn from_config_unknown_harden_profile_falls_back_to_standard() {
use crate::dynamic::sandbox::ProcessHardeningProfile;
let mut config = Config::default();
config.scanner.harden_profile = "lockdown".to_owned();
let opts = VerifyOptions::from_config(&config);
assert!(
matches!(opts.sandbox.process_hardening, ProcessHardeningProfile::Standard),
"unknown harden_profile values must degrade to Standard so a typo in \
nyx.toml does not silently leave the operator without the baseline \
hardening they were already paying for"
);
}
#[test]
fn verdict_cache_round_trip() {
let dir = tempfile::TempDir::new().unwrap();

View file

@ -281,6 +281,24 @@ pub struct ScannerConfig {
/// `"process"`: in-process runner (same as `--unsafe-sandbox`).
#[serde(default = "default_verify_backend")]
pub verify_backend: String,
/// Process-backend hardening profile applied during dynamic verification.
///
/// `"standard"` (default): the historical baseline. On Linux this
/// engages `prctl(PR_SET_NO_NEW_PRIVS)` plus `setrlimit(RLIMIT_AS)`;
/// on macOS the harness runs without a `sandbox-exec` wrap.
/// `"strict"`: opts into the full Phase 17/18 lockdown. On Linux the
/// process backend layers the namespace unshare, chroot to workdir,
/// and default-deny seccomp filter on top of the baseline. On macOS
/// the harness is wrapped with `sandbox-exec -f <profile>.sb` keyed
/// off the finding's expected cap (FILE_IO → `path_traversal.sb`,
/// CODE_EXEC → `cmdi.sb`, SSRF → `ssrf.sb`, …).
///
/// Opt-in. Interpreted Linux harnesses (python3, node, java) may
/// SIGSYS under strict seccomp until the per-language allowlists are
/// expanded; static native harnesses run unaffected.
#[serde(default = "default_harden_profile")]
pub harden_profile: String,
}
fn default_verify() -> bool {
true
@ -288,6 +306,9 @@ fn default_verify() -> bool {
fn default_verify_backend() -> String {
"auto".to_owned()
}
fn default_harden_profile() -> String {
"standard".to_owned()
}
impl Default for ScannerConfig {
fn default() -> Self {
Self {
@ -327,6 +348,7 @@ impl Default for ScannerConfig {
verify: true,
verify_all_confidence: false,
verify_backend: "auto".to_owned(),
harden_profile: "standard".to_owned(),
}
}
}