mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-27 20:29:39 +02:00
[pitboss/grind] deferred session-0004 (20260516T052512Z-20f8)
This commit is contained in:
parent
678f0f5d48
commit
3e08382a3f
4 changed files with 135 additions and 5 deletions
|
|
@ -570,8 +570,22 @@ pub fn handle(
|
||||||
opts.callgraph = Some(load_verify_callgraph(s));
|
opts.callgraph = Some(load_verify_callgraph(s));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Phase 29 follow-up: resolve the telemetry events log path once
|
||||||
|
// per scan so the per-finding `wrong:` stamp is a cheap fs read,
|
||||||
|
// not a directories-crate lookup each iteration. `None` (no
|
||||||
|
// log path resolvable on this host) leaves every `wrong` as
|
||||||
|
// `None` — the eval-corpus tabulator treats that as "no signal."
|
||||||
|
let telemetry_log = crate::dynamic::telemetry::log_path();
|
||||||
for diag in &mut diags {
|
for diag in &mut diags {
|
||||||
let result = crate::dynamic::verify::verify_finding(diag, &opts);
|
let mut result = crate::dynamic::verify::verify_finding(diag, &opts);
|
||||||
|
if result.status == crate::dynamic::report::VerifyStatus::Confirmed {
|
||||||
|
if let Some(ref log_path) = telemetry_log {
|
||||||
|
result.wrong = crate::dynamic::telemetry::feedback_wrong_for_finding(
|
||||||
|
log_path,
|
||||||
|
&result.finding_id,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
if let Some(ref mut ev) = diag.evidence {
|
if let Some(ref mut ev) = diag.evidence {
|
||||||
ev.dynamic_verdict = Some(result);
|
ev.dynamic_verdict = Some(result);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,15 +30,25 @@
|
||||||
//! # Phase 28 extension (Track H.5 — PII scrubber)
|
//! # Phase 28 extension (Track H.5 — PII scrubber)
|
||||||
//!
|
//!
|
||||||
//! [`Scrubber`] hashes probe-witness values whose textual shape matches a
|
//! [`Scrubber`] hashes probe-witness values whose textual shape matches a
|
||||||
//! project secret pattern. The pattern set is the same one
|
//! project secret pattern. The pattern set is the one
|
||||||
//! [`crate::utils::redact`] already uses for `--show-suppressed` console
|
//! [`crate::utils::redact`] already applies to dynamic sandbox output —
|
||||||
//! output and repro `outcome.json` redaction: AWS access key IDs, GitHub /
|
//! repro bundle `outcome.json` redaction and telemetry payload scrubbing
|
||||||
|
//! before they hit disk. Covered shapes: AWS access key IDs, GitHub /
|
||||||
//! Slack / OpenAI tokens, PEM blocks, `password=` / `api_key=` / `secret=`
|
//! Slack / OpenAI tokens, PEM blocks, `password=` / `api_key=` / `secret=`
|
||||||
//! query strings, and `Bearer` headers. Re-using the redactor's pattern
|
//! query strings, and `Bearer` headers. Re-using the redactor's pattern
|
||||||
//! list keeps the rule "what counts as PII" defined in exactly one place
|
//! list keeps the rule "what counts as PII" defined in exactly one place
|
||||||
//! across the project — adding a new pattern in `redact.rs` also tightens
|
//! across the project — adding a new pattern in `redact.rs` also tightens
|
||||||
//! probe-witness scrubbing without a second registry to maintain.
|
//! probe-witness scrubbing without a second registry to maintain.
|
||||||
//!
|
//!
|
||||||
|
//! Note on the `--show-suppressed` CLI flag: that flag is a boolean
|
||||||
|
//! toggle for inline-comment suppression of static findings
|
||||||
|
//! ([`crate::commands::scan`] `show_suppressed`); it does not consume
|
||||||
|
//! the secret-pattern set defined here. A future user-configurable
|
||||||
|
//! "what counts as a secret in this project" regex list (e.g. a
|
||||||
|
//! `[scrubber]` section in `default-nyx.conf`) would plug into
|
||||||
|
//! [`Scrubber::project_default`] alongside the static
|
||||||
|
//! [`crate::utils::redact`] patterns, not the suppression flag.
|
||||||
|
//!
|
||||||
//! The witness scrubber differs from the redactor in one respect: instead
|
//! The witness scrubber differs from the redactor in one respect: instead
|
||||||
//! of erasing the secret behind a `<REDACTED>` placeholder it replaces it
|
//! of erasing the secret behind a `<REDACTED>` placeholder it replaces it
|
||||||
//! with `<scrubbed-hash:<prefix>>` where the prefix is the first 16 hex
|
//! with `<scrubbed-hash:<prefix>>` where the prefix is the first 16 hex
|
||||||
|
|
|
||||||
|
|
@ -269,6 +269,23 @@ fn repro_root(spec_hash: &str) -> Result<PathBuf, ReproError> {
|
||||||
Ok(root)
|
Ok(root)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Resolve the bundle path for `spec_hash` without creating any directories.
|
||||||
|
///
|
||||||
|
/// Returns the same path [`write`] uses (`~/.cache/nyx/dynamic/repro/{spec_hash}/`)
|
||||||
|
/// so callers can locate an existing bundle for replay. Respects the
|
||||||
|
/// `NYX_REPRO_BASE` test override.
|
||||||
|
///
|
||||||
|
/// Returns `None` when the host has no resolvable cache dir.
|
||||||
|
pub fn bundle_root_for(spec_hash: &str) -> Option<PathBuf> {
|
||||||
|
let base = if let Ok(p) = std::env::var("NYX_REPRO_BASE") {
|
||||||
|
PathBuf::from(p)
|
||||||
|
} else {
|
||||||
|
let dirs = ProjectDirs::from("", "", "nyx")?;
|
||||||
|
dirs.cache_dir().join("dynamic").join("repro")
|
||||||
|
};
|
||||||
|
Some(base.join(spec_hash))
|
||||||
|
}
|
||||||
|
|
||||||
fn write_json(path: &Path, value: &impl serde::Serialize) -> Result<(), ReproError> {
|
fn write_json(path: &Path, value: &impl serde::Serialize) -> Result<(), ReproError> {
|
||||||
let json = serde_json::to_string_pretty(value)?;
|
let json = serde_json::to_string_pretty(value)?;
|
||||||
fs::write(path, json.as_bytes())?;
|
fs::write(path, json.as_bytes())?;
|
||||||
|
|
@ -835,6 +852,36 @@ mod tests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn bundle_root_for_honours_test_override() {
|
||||||
|
let dir = TempDir::new().unwrap();
|
||||||
|
unsafe { std::env::set_var("NYX_REPRO_BASE", dir.path().to_str().unwrap()) };
|
||||||
|
let root = bundle_root_for("cafe0001").unwrap();
|
||||||
|
assert_eq!(root, dir.path().join("cafe0001"));
|
||||||
|
unsafe { std::env::remove_var("NYX_REPRO_BASE") };
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn bundle_root_for_matches_write_output_under_override() {
|
||||||
|
// The path returned by `bundle_root_for` must equal the bundle path
|
||||||
|
// that `write` produces — replay callers locate the bundle without
|
||||||
|
// re-creating directories, so a drift between the two helpers would
|
||||||
|
// silently skip the replay for every Confirmed finding.
|
||||||
|
let dir = TempDir::new().unwrap();
|
||||||
|
unsafe { std::env::set_var("NYX_REPRO_BASE", dir.path().to_str().unwrap()) };
|
||||||
|
let spec = make_spec();
|
||||||
|
let opts = SandboxOptions::default();
|
||||||
|
let outcome = make_outcome();
|
||||||
|
let verdict = make_verdict();
|
||||||
|
let artifact = write(
|
||||||
|
&spec, &opts, &outcome, &verdict,
|
||||||
|
"# harness", "# entry", b"payload", "label", None,
|
||||||
|
).unwrap();
|
||||||
|
let resolved = bundle_root_for(&spec.spec_hash).unwrap();
|
||||||
|
assert_eq!(resolved, artifact.root);
|
||||||
|
unsafe { std::env::remove_var("NYX_REPRO_BASE") };
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn outcome_json_redacts_secrets() {
|
fn outcome_json_redacts_secrets() {
|
||||||
let dir = TempDir::new().unwrap();
|
let dir = TempDir::new().unwrap();
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,18 @@ pub struct VerifyOptions {
|
||||||
/// end-of-verify. Wired to the future `--verbose` CLI flag; off by
|
/// end-of-verify. Wired to the future `--verbose` CLI flag; off by
|
||||||
/// default so non-interactive scans stay quiet.
|
/// default so non-interactive scans stay quiet.
|
||||||
pub trace_verbose: bool,
|
pub trace_verbose: bool,
|
||||||
|
/// Phase 29 follow-up: when `true`, the verifier re-runs
|
||||||
|
/// `reproduce.sh` against the freshly written repro bundle whenever a
|
||||||
|
/// finding is `Confirmed` and stamps the typed
|
||||||
|
/// [`crate::evidence::VerifyResult::replay_stable`] field via
|
||||||
|
/// [`crate::dynamic::repro::replay_stability`]. Opt-in because
|
||||||
|
/// invoking `reproduce.sh` per Confirmed finding doubles wall-clock
|
||||||
|
/// cost — the eval-corpus driver flips it on; interactive `nyx scan`
|
||||||
|
/// keeps it off and leaves `replay_stable: None`.
|
||||||
|
///
|
||||||
|
/// Default `false`. [`Self::from_config`] honours the
|
||||||
|
/// `NYX_VERIFY_REPLAY_STABLE` environment variable (`1` / `true`).
|
||||||
|
pub replay_stable_check: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl VerifyOptions {
|
impl VerifyOptions {
|
||||||
|
|
@ -113,6 +125,10 @@ impl VerifyOptions {
|
||||||
#[cfg(not(target_os = "macos"))]
|
#[cfg(not(target_os = "macos"))]
|
||||||
let refuse_filesystem_confirm = false;
|
let refuse_filesystem_confirm = false;
|
||||||
|
|
||||||
|
let replay_stable_check = std::env::var("NYX_VERIFY_REPLAY_STABLE")
|
||||||
|
.map(|v| matches!(v.as_str(), "1" | "true" | "TRUE"))
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
sandbox: SandboxOptions {
|
sandbox: SandboxOptions {
|
||||||
backend,
|
backend,
|
||||||
|
|
@ -127,6 +143,7 @@ impl VerifyOptions {
|
||||||
refuse_filesystem_confirm,
|
refuse_filesystem_confirm,
|
||||||
telemetry_policy: SamplingPolicy::from_config(&config.telemetry),
|
telemetry_policy: SamplingPolicy::from_config(&config.telemetry),
|
||||||
trace_verbose: false,
|
trace_verbose: false,
|
||||||
|
replay_stable_check,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -653,7 +670,7 @@ pub fn verify_finding(diag: &Diag, opts: &VerifyOptions) -> VerifyResult {
|
||||||
_ => 1,
|
_ => 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
let verdict = build_verdict(
|
let mut verdict = build_verdict(
|
||||||
&finding_id,
|
&finding_id,
|
||||||
&spec,
|
&spec,
|
||||||
result,
|
result,
|
||||||
|
|
@ -662,6 +679,21 @@ pub fn verify_finding(diag: &Diag, opts: &VerifyOptions) -> VerifyResult {
|
||||||
elapsed,
|
elapsed,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Phase 29 follow-up: stamp `replay_stable` from a `reproduce.sh` rerun
|
||||||
|
// against the freshly written bundle. Opt-in (see
|
||||||
|
// `VerifyOptions::replay_stable_check`) because invoking the script
|
||||||
|
// per Confirmed finding doubles wall-clock cost — the eval-corpus
|
||||||
|
// driver flips it on so the tabulated `stable_replays` column becomes
|
||||||
|
// non-vacuous; interactive `nyx scan` keeps `replay_stable: None`.
|
||||||
|
if verdict.status == VerifyStatus::Confirmed
|
||||||
|
&& opts.replay_stable_check
|
||||||
|
&& let Some(bundle) = crate::dynamic::repro::bundle_root_for(&spec.spec_hash)
|
||||||
|
&& bundle.join("reproduce.sh").exists()
|
||||||
|
{
|
||||||
|
let replay = crate::dynamic::repro::replay_bundle(&bundle, &[]);
|
||||||
|
verdict.replay_stable = crate::dynamic::repro::replay_stability(&replay);
|
||||||
|
}
|
||||||
|
|
||||||
// Store result in verdict cache (best-effort; errors are silently ignored).
|
// Store result in verdict cache (best-effort; errors are silently ignored).
|
||||||
if let Some(ref db_path) = opts.db_path {
|
if let Some(ref db_path) = opts.db_path {
|
||||||
insert_verdict_cache(
|
insert_verdict_cache(
|
||||||
|
|
@ -1044,6 +1076,33 @@ mod tests {
|
||||||
assert_eq!(transitive_import_digest_placeholder(), "");
|
assert_eq!(transitive_import_digest_placeholder(), "");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn from_config_defaults_replay_stable_check_off() {
|
||||||
|
// Make sure the test is hermetic — `from_config` reads the env
|
||||||
|
// var, so a stale process-wide setting could mask the default.
|
||||||
|
unsafe { std::env::remove_var("NYX_VERIFY_REPLAY_STABLE") };
|
||||||
|
let opts = VerifyOptions::from_config(&Config::default());
|
||||||
|
assert!(
|
||||||
|
!opts.replay_stable_check,
|
||||||
|
"NYX_VERIFY_REPLAY_STABLE absent must leave the opt-in off so \
|
||||||
|
interactive `nyx scan` does not pay the per-finding reproduce.sh cost"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn from_config_picks_up_replay_stable_env_flag() {
|
||||||
|
unsafe { std::env::set_var("NYX_VERIFY_REPLAY_STABLE", "1") };
|
||||||
|
let opts = VerifyOptions::from_config(&Config::default());
|
||||||
|
assert!(opts.replay_stable_check);
|
||||||
|
unsafe { std::env::set_var("NYX_VERIFY_REPLAY_STABLE", "true") };
|
||||||
|
let opts = VerifyOptions::from_config(&Config::default());
|
||||||
|
assert!(opts.replay_stable_check);
|
||||||
|
unsafe { std::env::set_var("NYX_VERIFY_REPLAY_STABLE", "0") };
|
||||||
|
let opts = VerifyOptions::from_config(&Config::default());
|
||||||
|
assert!(!opts.replay_stable_check);
|
||||||
|
unsafe { std::env::remove_var("NYX_VERIFY_REPLAY_STABLE") };
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn verdict_cache_round_trip() {
|
fn verdict_cache_round_trip() {
|
||||||
let dir = tempfile::TempDir::new().unwrap();
|
let dir = tempfile::TempDir::new().unwrap();
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue