mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-30 20:39:39 +02:00
[pitboss/grind] deferred session-0003 (20260517T044708Z-e058)
This commit is contained in:
parent
6698eb96eb
commit
2544e5d9da
7 changed files with 257 additions and 10 deletions
|
|
@ -208,9 +208,12 @@ pub fn profile_path(name: &str) -> Option<PathBuf> {
|
||||||
}
|
}
|
||||||
let dir = profile_dir()?;
|
let dir = profile_dir()?;
|
||||||
let path = dir.join(format!("{key}.sb"));
|
let path = dir.join(format!("{key}.sb"));
|
||||||
if !path.exists() {
|
// Always overwrite on first miss in this process so an upgraded nyx
|
||||||
std::fs::write(&path, source).ok()?;
|
// binary picks up new profile content even when a previous version
|
||||||
}
|
// left a stale `.sb` file under `std::env::temp_dir()`. The in-process
|
||||||
|
// `PROFILE_PATHS` cache then short-circuits subsequent lookups so the
|
||||||
|
// write happens at most once per profile per process lifetime.
|
||||||
|
std::fs::write(&path, source).ok()?;
|
||||||
let mut cache = profile_paths().lock().ok()?;
|
let mut cache = profile_paths().lock().ok()?;
|
||||||
cache.insert(*key, path.clone());
|
cache.insert(*key, path.clone());
|
||||||
Some(path)
|
Some(path)
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,13 @@
|
||||||
(version 1)
|
(version 1)
|
||||||
(allow default)
|
(allow default)
|
||||||
|
|
||||||
|
;; The `/Users` denylist uses regex matches on specific secret-bearing
|
||||||
|
;; subpaths instead of a blanket `(subpath "/Users")` deny. The blanket
|
||||||
|
;; form blocks every interpreter cold-start (python3 / node / java) at
|
||||||
|
;; `_path_importer_cache` because Hombrew / Anaconda / pyenv / nvm all
|
||||||
|
;; install under `/Users/<user>/...`. Narrowing to a specific secret
|
||||||
|
;; set keeps the harness loadable while still blocking credential
|
||||||
|
;; exfiltration via a tainted-argv command.
|
||||||
(deny file-read*
|
(deny file-read*
|
||||||
(literal "/etc/passwd")
|
(literal "/etc/passwd")
|
||||||
(literal "/etc/master.passwd")
|
(literal "/etc/master.passwd")
|
||||||
|
|
@ -18,7 +25,21 @@
|
||||||
(literal "/private/etc/master.passwd")
|
(literal "/private/etc/master.passwd")
|
||||||
(literal "/private/etc/shadow")
|
(literal "/private/etc/shadow")
|
||||||
(literal "/private/etc/sudoers")
|
(literal "/private/etc/sudoers")
|
||||||
(subpath "/Users")
|
(regex #"^/Users/[^/]+/\.ssh(/|$)")
|
||||||
|
(regex #"^/Users/[^/]+/\.aws(/|$)")
|
||||||
|
(regex #"^/Users/[^/]+/\.gnupg(/|$)")
|
||||||
|
(regex #"^/Users/[^/]+/\.netrc$")
|
||||||
|
(regex #"^/Users/[^/]+/\.docker(/|$)")
|
||||||
|
(regex #"^/Users/[^/]+/\.kube(/|$)")
|
||||||
|
(regex #"^/Users/[^/]+/\.config/gh(/|$)")
|
||||||
|
(regex #"^/Users/[^/]+/\.zsh_history$")
|
||||||
|
(regex #"^/Users/[^/]+/\.bash_history$")
|
||||||
|
(regex #"^/Users/[^/]+/Library/Keychains(/|$)")
|
||||||
|
(regex #"^/Users/[^/]+/Library/Cookies(/|$)")
|
||||||
|
(regex #"^/Users/[^/]+/Library/Mail(/|$)")
|
||||||
|
(regex #"^/Users/[^/]+/Library/Application Support/com\.apple\.TCC(/|$)")
|
||||||
|
(regex #"^/Users/[^/]+/Library/Application Support/Slack(/|$)")
|
||||||
|
(regex #"^/Users/[^/]+/Library/Application Support/Code/User(/|$)")
|
||||||
(subpath "/var/db")
|
(subpath "/var/db")
|
||||||
(subpath "/private/var/db")
|
(subpath "/private/var/db")
|
||||||
(subpath "/Library/Keychains"))
|
(subpath "/Library/Keychains"))
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,9 @@
|
||||||
(version 1)
|
(version 1)
|
||||||
(allow default)
|
(allow default)
|
||||||
|
|
||||||
|
;; The `/Users` denylist uses regex matches on specific secret-bearing
|
||||||
|
;; subpaths instead of a blanket `(subpath "/Users")` deny. See the
|
||||||
|
;; matching comment in `cmdi.sb` for the cold-start rationale.
|
||||||
(deny file-read*
|
(deny file-read*
|
||||||
(literal "/etc/passwd")
|
(literal "/etc/passwd")
|
||||||
(literal "/etc/master.passwd")
|
(literal "/etc/master.passwd")
|
||||||
|
|
@ -18,5 +21,16 @@
|
||||||
(literal "/private/etc/master.passwd")
|
(literal "/private/etc/master.passwd")
|
||||||
(literal "/private/etc/shadow")
|
(literal "/private/etc/shadow")
|
||||||
(literal "/private/etc/sudoers")
|
(literal "/private/etc/sudoers")
|
||||||
(subpath "/Users")
|
(regex #"^/Users/[^/]+/\.ssh(/|$)")
|
||||||
|
(regex #"^/Users/[^/]+/\.aws(/|$)")
|
||||||
|
(regex #"^/Users/[^/]+/\.gnupg(/|$)")
|
||||||
|
(regex #"^/Users/[^/]+/\.netrc$")
|
||||||
|
(regex #"^/Users/[^/]+/\.docker(/|$)")
|
||||||
|
(regex #"^/Users/[^/]+/\.kube(/|$)")
|
||||||
|
(regex #"^/Users/[^/]+/\.config/gh(/|$)")
|
||||||
|
(regex #"^/Users/[^/]+/Library/Keychains(/|$)")
|
||||||
|
(regex #"^/Users/[^/]+/Library/Cookies(/|$)")
|
||||||
|
(regex #"^/Users/[^/]+/Library/Mail(/|$)")
|
||||||
|
(regex #"^/Users/[^/]+/Library/Application Support/com\.apple\.TCC(/|$)")
|
||||||
|
(regex #"^/Users/[^/]+/Library/Application Support/Slack(/|$)")
|
||||||
(subpath "/Library/Keychains"))
|
(subpath "/Library/Keychains"))
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,13 @@
|
||||||
(version 1)
|
(version 1)
|
||||||
(allow default)
|
(allow default)
|
||||||
|
|
||||||
|
;; The `/Users` denylist uses regex matches on specific secret-bearing
|
||||||
|
;; subpaths instead of a blanket `(subpath "/Users")` deny. See the
|
||||||
|
;; matching comment in `cmdi.sb` for the cold-start rationale. The
|
||||||
|
;; FILE_IO profile is the strictest of the cap profiles so the regex
|
||||||
|
;; set is wider than the CMDI / SSRF profiles: every credential file
|
||||||
|
;; under `~` plus per-app secret stores (Slack tokens, VS Code user
|
||||||
|
;; settings, Mail database) are denied.
|
||||||
(deny file-read*
|
(deny file-read*
|
||||||
(literal "/etc/passwd")
|
(literal "/etc/passwd")
|
||||||
(literal "/etc/master.passwd")
|
(literal "/etc/master.passwd")
|
||||||
|
|
@ -30,7 +37,21 @@
|
||||||
(literal "/private/etc/master.passwd")
|
(literal "/private/etc/master.passwd")
|
||||||
(literal "/private/etc/shadow")
|
(literal "/private/etc/shadow")
|
||||||
(literal "/private/etc/sudoers")
|
(literal "/private/etc/sudoers")
|
||||||
(subpath "/Users")
|
(regex #"^/Users/[^/]+/\.ssh(/|$)")
|
||||||
|
(regex #"^/Users/[^/]+/\.aws(/|$)")
|
||||||
|
(regex #"^/Users/[^/]+/\.gnupg(/|$)")
|
||||||
|
(regex #"^/Users/[^/]+/\.netrc$")
|
||||||
|
(regex #"^/Users/[^/]+/\.docker(/|$)")
|
||||||
|
(regex #"^/Users/[^/]+/\.kube(/|$)")
|
||||||
|
(regex #"^/Users/[^/]+/\.config/gh(/|$)")
|
||||||
|
(regex #"^/Users/[^/]+/\.zsh_history$")
|
||||||
|
(regex #"^/Users/[^/]+/\.bash_history$")
|
||||||
|
(regex #"^/Users/[^/]+/Library/Keychains(/|$)")
|
||||||
|
(regex #"^/Users/[^/]+/Library/Cookies(/|$)")
|
||||||
|
(regex #"^/Users/[^/]+/Library/Mail(/|$)")
|
||||||
|
(regex #"^/Users/[^/]+/Library/Application Support/com\.apple\.TCC(/|$)")
|
||||||
|
(regex #"^/Users/[^/]+/Library/Application Support/Slack(/|$)")
|
||||||
|
(regex #"^/Users/[^/]+/Library/Application Support/Code/User(/|$)")
|
||||||
(subpath "/var/db")
|
(subpath "/var/db")
|
||||||
(subpath "/private/var/db")
|
(subpath "/private/var/db")
|
||||||
(subpath "/var/log")
|
(subpath "/var/log")
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,9 @@
|
||||||
(version 1)
|
(version 1)
|
||||||
(allow default)
|
(allow default)
|
||||||
|
|
||||||
|
;; The `/Users` denylist uses regex matches on specific secret-bearing
|
||||||
|
;; subpaths instead of a blanket `(subpath "/Users")` deny. See the
|
||||||
|
;; matching comment in `cmdi.sb` for the cold-start rationale.
|
||||||
(deny file-read*
|
(deny file-read*
|
||||||
(literal "/etc/passwd")
|
(literal "/etc/passwd")
|
||||||
(literal "/etc/master.passwd")
|
(literal "/etc/master.passwd")
|
||||||
|
|
@ -18,5 +21,16 @@
|
||||||
(literal "/private/etc/master.passwd")
|
(literal "/private/etc/master.passwd")
|
||||||
(literal "/private/etc/shadow")
|
(literal "/private/etc/shadow")
|
||||||
(literal "/private/etc/sudoers")
|
(literal "/private/etc/sudoers")
|
||||||
(subpath "/Users")
|
(regex #"^/Users/[^/]+/\.ssh(/|$)")
|
||||||
|
(regex #"^/Users/[^/]+/\.aws(/|$)")
|
||||||
|
(regex #"^/Users/[^/]+/\.gnupg(/|$)")
|
||||||
|
(regex #"^/Users/[^/]+/\.netrc$")
|
||||||
|
(regex #"^/Users/[^/]+/\.docker(/|$)")
|
||||||
|
(regex #"^/Users/[^/]+/\.kube(/|$)")
|
||||||
|
(regex #"^/Users/[^/]+/\.config/gh(/|$)")
|
||||||
|
(regex #"^/Users/[^/]+/Library/Keychains(/|$)")
|
||||||
|
(regex #"^/Users/[^/]+/Library/Cookies(/|$)")
|
||||||
|
(regex #"^/Users/[^/]+/Library/Mail(/|$)")
|
||||||
|
(regex #"^/Users/[^/]+/Library/Application Support/com\.apple\.TCC(/|$)")
|
||||||
|
(regex #"^/Users/[^/]+/Library/Application Support/Slack(/|$)")
|
||||||
(subpath "/Library/Keychains"))
|
(subpath "/Library/Keychains"))
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,11 @@
|
||||||
;; Standard filesystem-escape denylist — shared shape with the other
|
;; Standard filesystem-escape denylist — shared shape with the other
|
||||||
;; per-cap profiles. `file://`-scheme entity reads of these paths
|
;; per-cap profiles. `file://`-scheme entity reads of these paths
|
||||||
;; will fault out before the parser hands the contents back.
|
;; will fault out before the parser hands the contents back.
|
||||||
|
;; The `/Users` denylist uses regex matches on specific secret-bearing
|
||||||
|
;; subpaths instead of a blanket `(subpath "/Users")` deny. See the
|
||||||
|
;; matching comment in `cmdi.sb` for the cold-start rationale. XXE
|
||||||
|
;; payloads that resolve `file:///Users/<user>/.ssh/id_rsa` still hit
|
||||||
|
;; EPERM at parser fetch time.
|
||||||
(deny file-read*
|
(deny file-read*
|
||||||
(literal "/etc/passwd")
|
(literal "/etc/passwd")
|
||||||
(literal "/etc/master.passwd")
|
(literal "/etc/master.passwd")
|
||||||
|
|
@ -39,5 +44,16 @@
|
||||||
(literal "/private/etc/master.passwd")
|
(literal "/private/etc/master.passwd")
|
||||||
(literal "/private/etc/shadow")
|
(literal "/private/etc/shadow")
|
||||||
(literal "/private/etc/sudoers")
|
(literal "/private/etc/sudoers")
|
||||||
(subpath "/Users")
|
(regex #"^/Users/[^/]+/\.ssh(/|$)")
|
||||||
|
(regex #"^/Users/[^/]+/\.aws(/|$)")
|
||||||
|
(regex #"^/Users/[^/]+/\.gnupg(/|$)")
|
||||||
|
(regex #"^/Users/[^/]+/\.netrc$")
|
||||||
|
(regex #"^/Users/[^/]+/\.docker(/|$)")
|
||||||
|
(regex #"^/Users/[^/]+/\.kube(/|$)")
|
||||||
|
(regex #"^/Users/[^/]+/\.config/gh(/|$)")
|
||||||
|
(regex #"^/Users/[^/]+/Library/Keychains(/|$)")
|
||||||
|
(regex #"^/Users/[^/]+/Library/Cookies(/|$)")
|
||||||
|
(regex #"^/Users/[^/]+/Library/Mail(/|$)")
|
||||||
|
(regex #"^/Users/[^/]+/Library/Application Support/com\.apple\.TCC(/|$)")
|
||||||
|
(regex #"^/Users/[^/]+/Library/Application Support/Slack(/|$)")
|
||||||
(subpath "/Library/Keychains"))
|
(subpath "/Library/Keychains"))
|
||||||
|
|
|
||||||
|
|
@ -115,8 +115,9 @@ except Exception as exc:
|
||||||
/// the probe exits 0 with the `network-attempted` marker.
|
/// the probe exits 0 with the `network-attempted` marker.
|
||||||
///
|
///
|
||||||
/// The probe source is read in at compile time and written into
|
/// The probe source is read in at compile time and written into
|
||||||
/// the harness workdir at run time so the sandbox-exec
|
/// the harness workdir at run time so the sandbox-exec narrow
|
||||||
/// `(subpath "/Users")` deny does not block the script load.
|
/// `/Users/<user>/Library/...` denies cannot accidentally shadow a
|
||||||
|
/// home-relative script-load path.
|
||||||
const XXE_PROBE_SOURCE: &str =
|
const XXE_PROBE_SOURCE: &str =
|
||||||
include_str!("dynamic_fixtures/hardening/xxe_probe.py");
|
include_str!("dynamic_fixtures/hardening/xxe_probe.py");
|
||||||
|
|
||||||
|
|
@ -528,6 +529,163 @@ except Exception as exc:
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Phase 18 acceptance (d): Strict-profile run of the cmdi positive
|
||||||
|
/// fixture confirms AND stamps `VerifyResult::hardening_outcome`.
|
||||||
|
/// Mirrors `verify_finding_under_standard_leaves_hardening_outcome_unset`
|
||||||
|
/// with `harden_profile = "strict"` so the macOS process backend
|
||||||
|
/// engages `sandbox-exec -f cmdi.sb -D WORKDIR=...` end-to-end.
|
||||||
|
/// The cmdi.sb profile's narrowed `/Users` deny (regex-matched
|
||||||
|
/// secret subpaths only, not a blanket `(subpath "/Users")` deny)
|
||||||
|
/// keeps `_path_importer_cache` reachable so the python harness
|
||||||
|
/// cold-starts; the `subprocess.run("echo NYX_PWN_CMDI", shell=True)`
|
||||||
|
/// invocation in the auto-emitted harness is the sink probe and
|
||||||
|
/// fires under the cmdi profile (process-exec is allowed; filesystem
|
||||||
|
/// reads of host secrets are denied via the inherited denylist).
|
||||||
|
#[test]
|
||||||
|
fn verify_finding_under_strict_stamps_hardening_outcome() {
|
||||||
|
use std::path::PathBuf;
|
||||||
|
unsafe { std::env::remove_var(SANDBOX_EXEC_BIN_ENV) };
|
||||||
|
if !sandbox_exec_available() {
|
||||||
|
eprintln!("SKIP: /usr/bin/sandbox-exec missing — cannot exercise wrap");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let python3_available = std::process::Command::new("/usr/bin/python3")
|
||||||
|
.arg("--version")
|
||||||
|
.output()
|
||||||
|
.map(|o| o.status.success())
|
||||||
|
.unwrap_or(false);
|
||||||
|
if !python3_available {
|
||||||
|
eprintln!("SKIP: /usr/bin/python3 missing — cannot run python harness");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
use nyx_scanner::commands::scan::Diag;
|
||||||
|
use nyx_scanner::dynamic::verify::{verify_finding, VerifyOptions};
|
||||||
|
use nyx_scanner::evidence::{
|
||||||
|
Confidence, Evidence, FlowStep, FlowStepKind, VerifyStatus,
|
||||||
|
};
|
||||||
|
use nyx_scanner::labels::Cap;
|
||||||
|
use nyx_scanner::patterns::{FindingCategory, Severity};
|
||||||
|
use nyx_scanner::utils::config::Config;
|
||||||
|
|
||||||
|
let fixture_src = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||||
|
.join("tests/dynamic_fixtures/python/cmdi_positive.py");
|
||||||
|
|
||||||
|
let tmp = tempfile::TempDir::new().expect("create tempdir");
|
||||||
|
let dst = tmp.path().join("cmdi_positive.py");
|
||||||
|
std::fs::copy(&fixture_src, &dst).expect("stage fixture into tempdir");
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
std::env::set_var(
|
||||||
|
"NYX_REPRO_BASE",
|
||||||
|
tmp.path().join("repro").to_str().unwrap(),
|
||||||
|
);
|
||||||
|
std::env::set_var(
|
||||||
|
"NYX_TELEMETRY_PATH",
|
||||||
|
tmp.path().join("events.jsonl").to_str().unwrap(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let path_str = dst.to_string_lossy().into_owned();
|
||||||
|
let evidence = Evidence {
|
||||||
|
flow_steps: vec![
|
||||||
|
FlowStep {
|
||||||
|
step: 1,
|
||||||
|
kind: FlowStepKind::Source,
|
||||||
|
file: path_str.clone(),
|
||||||
|
line: 1,
|
||||||
|
col: 0,
|
||||||
|
snippet: None,
|
||||||
|
variable: Some("host".into()),
|
||||||
|
callee: None,
|
||||||
|
function: Some("run_ping".into()),
|
||||||
|
is_cross_file: false,
|
||||||
|
},
|
||||||
|
FlowStep {
|
||||||
|
step: 2,
|
||||||
|
kind: FlowStepKind::Sink,
|
||||||
|
file: path_str.clone(),
|
||||||
|
line: 13,
|
||||||
|
col: 4,
|
||||||
|
snippet: None,
|
||||||
|
variable: None,
|
||||||
|
callee: None,
|
||||||
|
function: None,
|
||||||
|
is_cross_file: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
sink_caps: Cap::CODE_EXEC.bits(),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let diag = Diag {
|
||||||
|
path: path_str,
|
||||||
|
line: 13,
|
||||||
|
col: 0,
|
||||||
|
severity: Severity::High,
|
||||||
|
id: "taint-unsanitised-flow".into(),
|
||||||
|
category: FindingCategory::Security,
|
||||||
|
path_validated: false,
|
||||||
|
guard_kind: None,
|
||||||
|
message: None,
|
||||||
|
labels: vec![],
|
||||||
|
confidence: Some(Confidence::High),
|
||||||
|
evidence: Some(evidence),
|
||||||
|
rank_score: None,
|
||||||
|
rank_reason: None,
|
||||||
|
suppressed: false,
|
||||||
|
suppression: None,
|
||||||
|
rollup: None,
|
||||||
|
finding_id: String::new(),
|
||||||
|
alternative_finding_ids: vec![],
|
||||||
|
stable_hash: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut config = Config::default();
|
||||||
|
config.scanner.harden_profile = "strict".to_owned();
|
||||||
|
// Force the process backend: the macOS sandbox-exec wrap is gated
|
||||||
|
// on `SandboxBackend::Process`, and `SandboxBackend::Auto` would
|
||||||
|
// route the python harness to docker when docker is reachable
|
||||||
|
// (the common CI shape). Docker ignores `process_hardening`, so
|
||||||
|
// running under `Auto` would leave `hardening_outcome` unset
|
||||||
|
// regardless of `--harden=strict`, masking the wiring this test
|
||||||
|
// is asserting.
|
||||||
|
config.scanner.verify_backend = "process".to_owned();
|
||||||
|
let opts = VerifyOptions::from_config(&config);
|
||||||
|
let result = verify_finding(&diag, &opts);
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
std::env::remove_var("NYX_REPRO_BASE");
|
||||||
|
std::env::remove_var("NYX_TELEMETRY_PATH");
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
result.status,
|
||||||
|
VerifyStatus::Confirmed,
|
||||||
|
"cmdi_positive.py under --harden=strict should confirm: detail={:?}",
|
||||||
|
result.detail,
|
||||||
|
);
|
||||||
|
let summary = result
|
||||||
|
.hardening_outcome
|
||||||
|
.as_ref()
|
||||||
|
.expect("Strict run must stamp hardening_outcome");
|
||||||
|
assert_eq!(
|
||||||
|
summary.backend, "macos-process",
|
||||||
|
"macOS host should produce a macos-process backend stamp",
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
summary.level, "sandboxed",
|
||||||
|
"Strict-engaged sandbox-exec wrap should record level=sandboxed",
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
summary.profile, "cmdi",
|
||||||
|
"CODE_EXEC-cap finding should land the cmdi profile",
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
summary.primitives.is_empty(),
|
||||||
|
"macOS backend records no per-primitive entries",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// Round-trip the portable summary through JSON to lock in the
|
/// Round-trip the portable summary through JSON to lock in the
|
||||||
/// repro-bundle wire shape: `VerifyResult::hardening_outcome` lands
|
/// repro-bundle wire shape: `VerifyResult::hardening_outcome` lands
|
||||||
/// on `expected/verdict.json` so the eval-corpus tabulator and any
|
/// on `expected/verdict.json` so the eval-corpus tabulator and any
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue