mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-24 20:28:06 +02:00
[pitboss] phase 02: M2 — Python end-to-end excellence with all hardening baked in
This commit is contained in:
parent
894f587b60
commit
0bf39047b9
50 changed files with 4167 additions and 170 deletions
|
|
@ -4,26 +4,31 @@
|
|||
//! It is the only function the rest of the crate needs to know about.
|
||||
|
||||
use crate::commands::scan::Diag;
|
||||
use crate::dynamic::corpus::payloads_for;
|
||||
use crate::dynamic::report::{AttemptSummary, VerifyResult, VerifyStatus};
|
||||
use crate::dynamic::runner::{run_spec, RunError};
|
||||
use crate::dynamic::sandbox::SandboxOptions;
|
||||
use crate::dynamic::spec::HarnessSpec;
|
||||
use crate::evidence::UnsupportedReason;
|
||||
use crate::dynamic::telemetry::{self, TelemetryEvent};
|
||||
use crate::dynamic::toolchain;
|
||||
use crate::evidence::{InconclusiveReason, UnsupportedReason};
|
||||
use crate::utils::config::Config;
|
||||
use std::path::Path;
|
||||
use std::time::Instant;
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct VerifyOptions {
|
||||
pub sandbox: SandboxOptions,
|
||||
/// Project root for repro artifact symlinks (optional).
|
||||
pub project_root: Option<std::path::PathBuf>,
|
||||
}
|
||||
|
||||
impl VerifyOptions {
|
||||
/// Build `VerifyOptions` from scanner config.
|
||||
///
|
||||
/// Currently forwards sandbox timeout from `config.scanner`; future
|
||||
/// milestones will add image/resource limits here.
|
||||
pub fn from_config(_config: &Config) -> Self {
|
||||
Self {
|
||||
sandbox: SandboxOptions::default(),
|
||||
project_root: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -33,8 +38,6 @@ impl VerifyOptions {
|
|||
/// Never fails: every error path collapses into a [`VerifyStatus`] so the
|
||||
/// caller can treat dynamic verification as best-effort enrichment.
|
||||
pub fn verify_finding(diag: &Diag, opts: &VerifyOptions) -> VerifyResult {
|
||||
// Use the stable hash to identify the finding so the VerifyResult's
|
||||
// finding_id matches HarnessSpec::finding_id (both use the same hex form).
|
||||
let finding_id = format!("{:016x}", diag.stable_hash);
|
||||
|
||||
let spec = match HarnessSpec::from_finding(diag) {
|
||||
|
|
@ -45,18 +48,56 @@ pub fn verify_finding(diag: &Diag, opts: &VerifyOptions) -> VerifyResult {
|
|||
status: VerifyStatus::Unsupported,
|
||||
triggered_payload: None,
|
||||
reason: Some(reason),
|
||||
inconclusive_reason: None,
|
||||
detail: None,
|
||||
attempts: vec![],
|
||||
toolchain_match: None,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// Spec derivable, but no backend implementation exists yet.
|
||||
// Phase M1 always lands here; real execution starts in Phase M2.
|
||||
let _ = &opts.sandbox;
|
||||
match run_spec(&spec, &opts.sandbox) {
|
||||
// Resolve toolchain information.
|
||||
let toolchain_res = toolchain::resolve_python(Path::new("."));
|
||||
let toolchain_match = if toolchain_res.toolchain_drift { "drift" } else { "exact" };
|
||||
|
||||
let start = Instant::now();
|
||||
let result = run_spec(&spec, &opts.sandbox);
|
||||
let elapsed = start.elapsed();
|
||||
|
||||
let verdict = build_verdict(
|
||||
&finding_id,
|
||||
&spec,
|
||||
result,
|
||||
toolchain_match,
|
||||
opts,
|
||||
elapsed,
|
||||
);
|
||||
|
||||
// Emit telemetry (best-effort; never affects verdict).
|
||||
let event = TelemetryEvent::new(
|
||||
&spec,
|
||||
verdict.status,
|
||||
verdict.inconclusive_reason,
|
||||
toolchain_match,
|
||||
elapsed,
|
||||
1, // build_attempts tracked in RunOutcome but not exposed here for simplicity
|
||||
);
|
||||
telemetry::emit(&event);
|
||||
|
||||
verdict
|
||||
}
|
||||
|
||||
fn build_verdict(
|
||||
finding_id: &str,
|
||||
spec: &HarnessSpec,
|
||||
result: Result<crate::dynamic::runner::RunOutcome, RunError>,
|
||||
toolchain_match: &str,
|
||||
opts: &VerifyOptions,
|
||||
_elapsed: std::time::Duration,
|
||||
) -> VerifyResult {
|
||||
match result {
|
||||
Ok(run) => {
|
||||
let attempts = run
|
||||
let attempts: Vec<AttemptSummary> = run
|
||||
.attempts
|
||||
.iter()
|
||||
.map(|a| AttemptSummary {
|
||||
|
|
@ -64,51 +105,138 @@ pub fn verify_finding(diag: &Diag, opts: &VerifyOptions) -> VerifyResult {
|
|||
exit_code: a.outcome.exit_code,
|
||||
timed_out: a.outcome.timed_out,
|
||||
triggered: a.triggered,
|
||||
sink_hit: a.outcome.sink_hit,
|
||||
})
|
||||
.collect();
|
||||
|
||||
match run.triggered_by {
|
||||
Some(i) => VerifyResult {
|
||||
finding_id,
|
||||
if let Some(i) = run.triggered_by {
|
||||
let triggered_payload = run.attempts[i].payload_label.to_string();
|
||||
let payloads = payloads_for(spec.expected_cap);
|
||||
let vuln_payloads: Vec<_> = payloads.iter().filter(|p| !p.is_benign).collect();
|
||||
let payload_bytes = vuln_payloads
|
||||
.get(i)
|
||||
.map(|p| p.bytes)
|
||||
.unwrap_or(b"");
|
||||
|
||||
// Emit repro artifact.
|
||||
let repro_result = crate::dynamic::repro::write(
|
||||
spec,
|
||||
&opts.sandbox,
|
||||
&run.attempts[i].outcome,
|
||||
&VerifyResult {
|
||||
finding_id: finding_id.to_owned(),
|
||||
status: VerifyStatus::Confirmed,
|
||||
triggered_payload: Some(triggered_payload.clone()),
|
||||
reason: None,
|
||||
inconclusive_reason: None,
|
||||
detail: None,
|
||||
attempts: attempts.clone(),
|
||||
toolchain_match: Some(toolchain_match.to_owned()),
|
||||
},
|
||||
&run.harness_source,
|
||||
&run.entry_source,
|
||||
payload_bytes,
|
||||
run.attempts[i].payload_label,
|
||||
opts.project_root.as_deref(),
|
||||
);
|
||||
|
||||
// If repro write fails, downgrade to NonReproducible.
|
||||
if repro_result.is_err() {
|
||||
return VerifyResult {
|
||||
finding_id: finding_id.to_owned(),
|
||||
status: VerifyStatus::Inconclusive,
|
||||
triggered_payload: None,
|
||||
reason: None,
|
||||
inconclusive_reason: Some(InconclusiveReason::NonReproducible),
|
||||
detail: Some(format!("repro write failed: {}", repro_result.unwrap_err())),
|
||||
attempts,
|
||||
toolchain_match: Some(toolchain_match.to_owned()),
|
||||
};
|
||||
}
|
||||
|
||||
VerifyResult {
|
||||
finding_id: finding_id.to_owned(),
|
||||
status: VerifyStatus::Confirmed,
|
||||
triggered_payload: Some(run.attempts[i].payload_label.to_string()),
|
||||
triggered_payload: Some(triggered_payload),
|
||||
reason: None,
|
||||
inconclusive_reason: None,
|
||||
detail: None,
|
||||
attempts,
|
||||
},
|
||||
None => VerifyResult {
|
||||
finding_id,
|
||||
toolchain_match: Some(toolchain_match.to_owned()),
|
||||
}
|
||||
} else if run.oracle_collision {
|
||||
// Oracle fired but probe didn't — likely collision.
|
||||
VerifyResult {
|
||||
finding_id: finding_id.to_owned(),
|
||||
status: VerifyStatus::Inconclusive,
|
||||
triggered_payload: None,
|
||||
reason: None,
|
||||
inconclusive_reason: Some(InconclusiveReason::OracleCollisionSuspected),
|
||||
detail: Some("oracle fired but sink-reachability probe did not".to_owned()),
|
||||
attempts,
|
||||
toolchain_match: Some(toolchain_match.to_owned()),
|
||||
}
|
||||
} else {
|
||||
VerifyResult {
|
||||
finding_id: finding_id.to_owned(),
|
||||
status: VerifyStatus::NotConfirmed,
|
||||
triggered_payload: None,
|
||||
reason: None,
|
||||
inconclusive_reason: None,
|
||||
detail: None,
|
||||
attempts,
|
||||
},
|
||||
toolchain_match: Some(toolchain_match.to_owned()),
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(RunError::NoPayloadsForCap) => VerifyResult {
|
||||
finding_id,
|
||||
finding_id: finding_id.to_owned(),
|
||||
status: VerifyStatus::Unsupported,
|
||||
triggered_payload: None,
|
||||
reason: Some(UnsupportedReason::NoPayloadsForCap),
|
||||
inconclusive_reason: None,
|
||||
detail: None,
|
||||
attempts: vec![],
|
||||
toolchain_match: None,
|
||||
},
|
||||
Err(RunError::Harness(_)) => VerifyResult {
|
||||
finding_id,
|
||||
status: VerifyStatus::Unsupported,
|
||||
triggered_payload: None,
|
||||
reason: Some(UnsupportedReason::BackendUnavailable),
|
||||
detail: None,
|
||||
attempts: vec![],
|
||||
},
|
||||
Err(RunError::Sandbox(e)) => VerifyResult {
|
||||
finding_id,
|
||||
Err(RunError::Harness(e)) => {
|
||||
// Typed `Unsupported(reason)` carries its semantics in `reason`; the
|
||||
// free-form `detail` is reserved for `Inconclusive`/unexpected paths
|
||||
// (cf. §10 decision 14 and the verify_result_json_shape contract).
|
||||
let (reason, detail) = match &e {
|
||||
crate::dynamic::harness::HarnessError::Unsupported(r) => (Some(r.clone()), None),
|
||||
_ => (Some(UnsupportedReason::BackendUnavailable), Some(format!("{e}"))),
|
||||
};
|
||||
VerifyResult {
|
||||
finding_id: finding_id.to_owned(),
|
||||
status: VerifyStatus::Unsupported,
|
||||
triggered_payload: None,
|
||||
reason,
|
||||
inconclusive_reason: None,
|
||||
detail,
|
||||
attempts: vec![],
|
||||
toolchain_match: None,
|
||||
}
|
||||
}
|
||||
Err(RunError::BuildFailed { stderr, attempts: build_att }) => VerifyResult {
|
||||
finding_id: finding_id.to_owned(),
|
||||
status: VerifyStatus::Inconclusive,
|
||||
triggered_payload: None,
|
||||
reason: None,
|
||||
inconclusive_reason: Some(InconclusiveReason::BuildFailed),
|
||||
detail: Some(format!("build failed after {build_att} attempts: {stderr}")),
|
||||
attempts: vec![],
|
||||
toolchain_match: None,
|
||||
},
|
||||
Err(RunError::Sandbox(e)) => VerifyResult {
|
||||
finding_id: finding_id.to_owned(),
|
||||
status: VerifyStatus::Inconclusive,
|
||||
triggered_payload: None,
|
||||
reason: None,
|
||||
inconclusive_reason: Some(InconclusiveReason::SandboxError),
|
||||
detail: Some(format!("sandbox failed: {e:?}")),
|
||||
attempts: vec![],
|
||||
toolchain_match: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue