mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-09 19:45:13 +02:00
feat: Implement dynamic verification layer with harness generation and payload orchestration
This commit is contained in:
parent
fb698d2c27
commit
56e934656c
10 changed files with 582 additions and 0 deletions
89
src/dynamic/corpus.rs
Normal file
89
src/dynamic/corpus.rs
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
//! Per-capability payload corpus.
|
||||
//!
|
||||
//! Each [`Cap`] maps to a small set of canonical payloads plus a matching
|
||||
//! detection oracle. Payloads are static data — adding a new one is a code
|
||||
//! review, not a runtime config knob, so they cannot drift between versions.
|
||||
//!
|
||||
//! The corpus is intentionally minimal at the start. Goal is one payload
|
||||
//! per cap that triggers reliably on the obvious case; tuning happens once
|
||||
//! we have real targets.
|
||||
|
||||
use crate::labels::Cap;
|
||||
|
||||
/// A single payload + the oracle that confirms it fired.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Payload {
|
||||
/// Bytes injected into the [`crate::dynamic::spec::PayloadSlot`].
|
||||
pub bytes: &'static [u8],
|
||||
/// Human label for logs and reports (`"sqli-quote-or-1"` etc.).
|
||||
pub label: &'static str,
|
||||
/// How we decide the sink fired. See [`Oracle`].
|
||||
pub oracle: Oracle,
|
||||
}
|
||||
|
||||
/// Detection strategy. Multiple oracles run in parallel; first hit wins.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Oracle {
|
||||
/// Substring on stdout/stderr (e.g. `"PAYLOAD-MARKER"`, SQL error text).
|
||||
OutputContains(&'static str),
|
||||
/// Process exited with a crash signal (SIGSEGV, SIGABRT).
|
||||
Crash,
|
||||
/// Outbound network connection observed to a controlled sink host.
|
||||
OobCallback { host: &'static str },
|
||||
/// File written outside the sandbox sandbox root.
|
||||
FileEscape,
|
||||
/// Non-zero exit with specific status (e.g. shell command success).
|
||||
ExitStatus(i32),
|
||||
}
|
||||
|
||||
/// Pick the payload set for a given cap. Empty slice = unsupported cap.
|
||||
pub fn payloads_for(cap: Cap) -> &'static [Payload] {
|
||||
if cap.contains(Cap::SQL_QUERY) {
|
||||
return SQLI;
|
||||
}
|
||||
if cap.contains(Cap::CODE_EXEC) {
|
||||
return CMDI;
|
||||
}
|
||||
if cap.contains(Cap::FILE_IO) {
|
||||
return PATH_TRAV;
|
||||
}
|
||||
if cap.contains(Cap::SSRF) {
|
||||
return SSRF_PAYLOADS;
|
||||
}
|
||||
if cap.contains(Cap::HTML_ESCAPE) {
|
||||
return XSS;
|
||||
}
|
||||
&[]
|
||||
}
|
||||
|
||||
const SQLI: &[Payload] = &[Payload {
|
||||
bytes: b"' OR 1=1-- NYX",
|
||||
label: "sqli-or-1",
|
||||
oracle: Oracle::OutputContains("NYX"),
|
||||
}];
|
||||
|
||||
const CMDI: &[Payload] = &[Payload {
|
||||
bytes: b"; echo NYX_PWN",
|
||||
label: "cmdi-echo-marker",
|
||||
oracle: Oracle::OutputContains("NYX_PWN"),
|
||||
}];
|
||||
|
||||
const PATH_TRAV: &[Payload] = &[Payload {
|
||||
bytes: b"../../../../etc/passwd",
|
||||
label: "path-traversal-passwd",
|
||||
oracle: Oracle::OutputContains("root:"),
|
||||
}];
|
||||
|
||||
const SSRF_PAYLOADS: &[Payload] = &[Payload {
|
||||
bytes: b"http://nyx-oob.invalid/probe",
|
||||
label: "ssrf-oob",
|
||||
oracle: Oracle::OobCallback {
|
||||
host: "nyx-oob.invalid",
|
||||
},
|
||||
}];
|
||||
|
||||
const XSS: &[Payload] = &[Payload {
|
||||
bytes: b"<script>NYX_XSS</script>",
|
||||
label: "xss-script-marker",
|
||||
oracle: Oracle::OutputContains("<script>NYX_XSS</script>"),
|
||||
}];
|
||||
52
src/dynamic/harness.rs
Normal file
52
src/dynamic/harness.rs
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
//! Harness code generation.
|
||||
//!
|
||||
//! Given a [`HarnessSpec`], emit a small program that:
|
||||
//!
|
||||
//! 1. Imports/loads the target module from the project tree.
|
||||
//! 2. Reads the payload from a known channel (env var `NYX_PAYLOAD`).
|
||||
//! 3. Invokes the entry point with the payload routed to the right slot.
|
||||
//! 4. Lets the sink either fire or not — the oracle observes from outside.
|
||||
//!
|
||||
//! One generator per [`Lang`]. Each emits source plus a build command.
|
||||
//! Build artefacts are staged inside the sandbox working dir, never the
|
||||
//! user's tree.
|
||||
|
||||
use crate::dynamic::spec::HarnessSpec;
|
||||
use crate::symbol::Lang;
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// A built harness ready to hand off to the sandbox.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BuiltHarness {
|
||||
/// Working directory containing the harness source + any build output.
|
||||
pub workdir: PathBuf,
|
||||
/// Command to invoke (e.g. `["python3", "harness.py"]` or
|
||||
/// `["./target/release/harness"]`).
|
||||
pub command: Vec<String>,
|
||||
/// Environment variables to set when running. Payload bytes go in via
|
||||
/// `NYX_PAYLOAD` regardless of language.
|
||||
pub env: Vec<(String, String)>,
|
||||
}
|
||||
|
||||
/// Build a harness from a spec. Returns the artefact + run command.
|
||||
///
|
||||
/// Stub: per-language emitters will live in their own files
|
||||
/// (`harness/python.rs`, `harness/rust.rs`, etc.) and dispatch off
|
||||
/// `spec.lang`.
|
||||
pub fn build(_spec: &HarnessSpec) -> Result<BuiltHarness, HarnessError> {
|
||||
Err(HarnessError::Unimplemented)
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum HarnessError {
|
||||
Unimplemented,
|
||||
UnsupportedLang(Lang),
|
||||
BuildFailed(String),
|
||||
Io(std::io::Error),
|
||||
}
|
||||
|
||||
impl From<std::io::Error> for HarnessError {
|
||||
fn from(e: std::io::Error) -> Self {
|
||||
HarnessError::Io(e)
|
||||
}
|
||||
}
|
||||
36
src/dynamic/mod.rs
Normal file
36
src/dynamic/mod.rs
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
//! Dynamic verification layer (feature-gated: `dynamic`).
|
||||
//!
|
||||
//! Static analysis confirms a flow exists. Dynamic execution confirms it fires.
|
||||
//! This module turns a [`crate::commands::scan::Diag`] into a runnable harness,
|
||||
//! injects a payload from a per-cap corpus, executes inside a sandbox, and
|
||||
//! reports back whether the sink actually triggered.
|
||||
//!
|
||||
//! Pipeline:
|
||||
//!
|
||||
//! ```text
|
||||
//! Diag --> HarnessSpec --> Harness (generated source/binary)
|
||||
//! |
|
||||
//! v
|
||||
//! Sandbox::run(payload)
|
||||
//! |
|
||||
//! v
|
||||
//! VerifyResult
|
||||
//! ```
|
||||
//!
|
||||
//! All submodules are read-only consumers of the static engine's output.
|
||||
//! Nothing in this tree mutates SSA, taint, or label state.
|
||||
//!
|
||||
//! Off by default. Enable with `--features dynamic`. Heavy deps (container
|
||||
//! runtime client, fuzzer harness) live behind the same gate.
|
||||
|
||||
pub mod corpus;
|
||||
pub mod harness;
|
||||
pub mod report;
|
||||
pub mod runner;
|
||||
pub mod sandbox;
|
||||
pub mod spec;
|
||||
pub mod verify;
|
||||
|
||||
pub use report::{VerifyResult, VerifyStatus};
|
||||
pub use spec::HarnessSpec;
|
||||
pub use verify::{verify_finding, VerifyOptions};
|
||||
42
src/dynamic/report.rs
Normal file
42
src/dynamic/report.rs
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
//! Verdict types returned by the dynamic layer.
|
||||
//!
|
||||
//! Kept separate from the run pipeline so the CLI / JSON output side can
|
||||
//! depend on this without pulling in sandbox or harness deps.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum VerifyStatus {
|
||||
/// Sink fired with at least one payload. Static finding is exploitable
|
||||
/// against the live target.
|
||||
Confirmed,
|
||||
/// All payloads ran cleanly. Either the path is infeasible at runtime
|
||||
/// or the corpus is too narrow. Treat as "static-only" not "false".
|
||||
NotConfirmed,
|
||||
/// Could not build, run, or observe (toolchain missing, sandbox refused,
|
||||
/// timeout on every attempt, etc.).
|
||||
Inconclusive,
|
||||
/// We do not yet know how to drive this finding (missing language
|
||||
/// support, unsupported entry kind, no payloads for cap).
|
||||
Unsupported,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct VerifyResult {
|
||||
pub finding_id: String,
|
||||
pub status: VerifyStatus,
|
||||
/// Label of the payload that triggered, when [`VerifyStatus::Confirmed`].
|
||||
pub triggered_payload: Option<String>,
|
||||
/// Free-form note for inconclusive/unsupported cases.
|
||||
pub reason: Option<String>,
|
||||
/// Per-attempt log (payload label, exit code, timed_out flag).
|
||||
pub attempts: Vec<AttemptSummary>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AttemptSummary {
|
||||
pub payload_label: String,
|
||||
pub exit_code: Option<i32>,
|
||||
pub timed_out: bool,
|
||||
pub triggered: bool,
|
||||
}
|
||||
100
src/dynamic/runner.rs
Normal file
100
src/dynamic/runner.rs
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
//! Orchestration: spec -> harness -> sandbox -> oracle -> verdict.
|
||||
//!
|
||||
//! The runner is the only place that knows about all four submodules at
|
||||
//! once. Everything below it (corpus, harness, sandbox) is independent;
|
||||
//! everything above it ([`crate::dynamic::verify`]) just calls
|
||||
//! [`run_spec`] and turns the result into a [`crate::dynamic::report::VerifyResult`].
|
||||
|
||||
use crate::dynamic::corpus::{payloads_for, Oracle};
|
||||
use crate::dynamic::harness::{self, BuiltHarness, HarnessError};
|
||||
use crate::dynamic::sandbox::{self, SandboxError, SandboxOptions, SandboxOutcome};
|
||||
use crate::dynamic::spec::HarnessSpec;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct RunOutcome {
|
||||
pub spec: HarnessSpec,
|
||||
pub attempts: Vec<Attempt>,
|
||||
/// First attempt that fired the sink, if any.
|
||||
pub triggered_by: Option<usize>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Attempt {
|
||||
pub payload_label: &'static str,
|
||||
pub outcome: SandboxOutcome,
|
||||
pub triggered: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum RunError {
|
||||
NoPayloadsForCap,
|
||||
Harness(HarnessError),
|
||||
Sandbox(SandboxError),
|
||||
}
|
||||
|
||||
impl From<HarnessError> for RunError {
|
||||
fn from(e: HarnessError) -> Self {
|
||||
RunError::Harness(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SandboxError> for RunError {
|
||||
fn from(e: SandboxError) -> Self {
|
||||
RunError::Sandbox(e)
|
||||
}
|
||||
}
|
||||
|
||||
/// Build harness once, run every payload from the cap-matched corpus,
|
||||
/// stop at first trigger.
|
||||
pub fn run_spec(spec: &HarnessSpec, opts: &SandboxOptions) -> Result<RunOutcome, RunError> {
|
||||
let payloads = payloads_for(spec.expected_cap);
|
||||
if payloads.is_empty() {
|
||||
return Err(RunError::NoPayloadsForCap);
|
||||
}
|
||||
|
||||
let harness: BuiltHarness = harness::build(spec)?;
|
||||
|
||||
let mut attempts = Vec::with_capacity(payloads.len());
|
||||
let mut triggered_by = None;
|
||||
|
||||
for (i, payload) in payloads.iter().enumerate() {
|
||||
let outcome = sandbox::run(&harness, payload, opts)?;
|
||||
let triggered = oracle_fired(&payload.oracle, &outcome);
|
||||
attempts.push(Attempt {
|
||||
payload_label: payload.label,
|
||||
outcome,
|
||||
triggered,
|
||||
});
|
||||
if triggered {
|
||||
triggered_by = Some(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(RunOutcome {
|
||||
spec: spec.clone(),
|
||||
attempts,
|
||||
triggered_by,
|
||||
})
|
||||
}
|
||||
|
||||
fn oracle_fired(oracle: &Oracle, outcome: &SandboxOutcome) -> bool {
|
||||
match oracle {
|
||||
Oracle::OutputContains(needle) => {
|
||||
let nb = needle.as_bytes();
|
||||
contains_subslice(&outcome.stdout, nb) || contains_subslice(&outcome.stderr, nb)
|
||||
}
|
||||
Oracle::Crash => matches!(outcome.exit_code, None) && !outcome.timed_out,
|
||||
Oracle::OobCallback { .. } => outcome.oob_callback_seen,
|
||||
Oracle::FileEscape => false, // TODO(dynamic): wire fs watcher in sandbox layer.
|
||||
Oracle::ExitStatus(code) => outcome.exit_code == Some(*code),
|
||||
}
|
||||
}
|
||||
|
||||
fn contains_subslice(hay: &[u8], needle: &[u8]) -> bool {
|
||||
if needle.is_empty() || needle.len() > hay.len() {
|
||||
return needle.is_empty();
|
||||
}
|
||||
hay.windows(needle.len()).any(|w| w == needle)
|
||||
}
|
||||
|
||||
90
src/dynamic/sandbox.rs
Normal file
90
src/dynamic/sandbox.rs
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
//! Execution sandbox.
|
||||
//!
|
||||
//! The sandbox isolates a [`crate::dynamic::harness::BuiltHarness`] from
|
||||
//! the host: no outbound network except to the oracle's OOB host, no file
|
||||
//! writes outside the workdir, hard timeout, memory cap, no host PID
|
||||
//! visibility.
|
||||
//!
|
||||
//! Two backends planned, picked at runtime:
|
||||
//!
|
||||
//! - **`docker`**: portable, default on Linux/macOS. Image is a thin debian
|
||||
//! plus the language toolchain matching `spec.lang`.
|
||||
//! - **`process`**: fallback for hosts without docker. Uses OS primitives
|
||||
//! (`unshare` on Linux, `sandbox-exec` on macOS) and runs the harness
|
||||
//! directly. Less isolation; gated behind `--unsafe-sandbox`.
|
||||
//!
|
||||
//! All public state on the sandbox is owned by the caller — there is no
|
||||
//! global runtime, no daemon, no persistent containers between runs.
|
||||
|
||||
use crate::dynamic::corpus::Payload;
|
||||
use crate::dynamic::harness::BuiltHarness;
|
||||
use std::time::Duration;
|
||||
|
||||
/// Result of a single sandboxed run.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SandboxOutcome {
|
||||
/// Process exit code; `None` on timeout or signal kill.
|
||||
pub exit_code: Option<i32>,
|
||||
/// Captured stdout (truncated to a bound, default 64 KiB).
|
||||
pub stdout: Vec<u8>,
|
||||
/// Captured stderr (same bound).
|
||||
pub stderr: Vec<u8>,
|
||||
/// Whether the run hit `timeout`.
|
||||
pub timed_out: bool,
|
||||
/// Whether the OOB host received a probe.
|
||||
pub oob_callback_seen: bool,
|
||||
/// Wall-clock duration of the run.
|
||||
pub duration: Duration,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SandboxOptions {
|
||||
/// Hard timeout. Default: 5s.
|
||||
pub timeout: Duration,
|
||||
/// Memory cap in MiB. Default: 256.
|
||||
pub memory_mib: u64,
|
||||
/// Backend selection. `Auto` = docker if available, else process.
|
||||
pub backend: SandboxBackend,
|
||||
}
|
||||
|
||||
impl Default for SandboxOptions {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
timeout: Duration::from_secs(5),
|
||||
memory_mib: 256,
|
||||
backend: SandboxBackend::Auto,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum SandboxBackend {
|
||||
Auto,
|
||||
Docker,
|
||||
Process,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum SandboxError {
|
||||
BackendUnavailable(SandboxBackend),
|
||||
Spawn(std::io::Error),
|
||||
Io(std::io::Error),
|
||||
}
|
||||
|
||||
impl From<std::io::Error> for SandboxError {
|
||||
fn from(e: std::io::Error) -> Self {
|
||||
SandboxError::Io(e)
|
||||
}
|
||||
}
|
||||
|
||||
/// Run a built harness once with a chosen payload.
|
||||
///
|
||||
/// Stub: dispatches to one of the backend submodules
|
||||
/// (`sandbox/docker.rs`, `sandbox/process.rs`) once those land.
|
||||
pub fn run(
|
||||
_harness: &BuiltHarness,
|
||||
_payload: &Payload,
|
||||
_opts: &SandboxOptions,
|
||||
) -> Result<SandboxOutcome, SandboxError> {
|
||||
Err(SandboxError::BackendUnavailable(SandboxBackend::Auto))
|
||||
}
|
||||
81
src/dynamic/spec.rs
Normal file
81
src/dynamic/spec.rs
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
//! Harness specification: the bridge between a static finding and a runnable harness.
|
||||
//!
|
||||
//! A [`HarnessSpec`] is built from a [`crate::commands::scan::Diag`] without
|
||||
//! any further analysis. It records what the dynamic side needs to know:
|
||||
//! which entry point to drive, which parameter carries the payload, what
|
||||
//! sink (cap) we expect to hit, and which language toolchain to use.
|
||||
//!
|
||||
//! Construction is total but may return `None` when the finding lacks the
|
||||
//! evidence required to drive it dynamically (no source span, no callable
|
||||
//! entry, sink in dead code, etc.). Those findings stay static-only.
|
||||
|
||||
use crate::commands::scan::Diag;
|
||||
use crate::labels::Cap;
|
||||
use crate::symbol::Lang;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// What kind of entry point the harness should call.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum EntryKind {
|
||||
/// Free function. Build a `main` that calls it directly.
|
||||
Function,
|
||||
/// HTTP route. Stand up the framework, send a request.
|
||||
HttpRoute,
|
||||
/// CLI subcommand. Spawn the binary with crafted argv.
|
||||
CliSubcommand,
|
||||
/// Library API surface. Build an in-process consumer.
|
||||
LibraryApi,
|
||||
}
|
||||
|
||||
/// Where the payload goes when the harness fires.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum PayloadSlot {
|
||||
/// Nth positional parameter of the entry function.
|
||||
Param(usize),
|
||||
/// Named HTTP query parameter.
|
||||
QueryParam(String),
|
||||
/// HTTP request body (raw bytes).
|
||||
HttpBody,
|
||||
/// Environment variable.
|
||||
EnvVar(String),
|
||||
/// CLI argv slot (0-based, excluding argv[0]).
|
||||
Argv(usize),
|
||||
/// stdin.
|
||||
Stdin,
|
||||
}
|
||||
|
||||
/// Self-contained recipe for building and running a single harness.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct HarnessSpec {
|
||||
/// Stable id of the source finding (`Diag::id` plus location hash).
|
||||
pub finding_id: String,
|
||||
/// Project-relative path to the file holding the entry point.
|
||||
pub entry_file: String,
|
||||
/// Function/route/subcommand name to drive.
|
||||
pub entry_name: String,
|
||||
/// How to invoke it.
|
||||
pub entry_kind: EntryKind,
|
||||
/// Source language (drives toolchain selection).
|
||||
pub lang: Lang,
|
||||
/// Where the payload is injected.
|
||||
pub payload_slot: PayloadSlot,
|
||||
/// Sink capability we expect to fire (drives oracle + corpus pick).
|
||||
pub expected_cap: Cap,
|
||||
/// Optional symex-derived constraint hints (prefix/suffix locks, etc.).
|
||||
/// Populated later from `Evidence::engine_notes` when available.
|
||||
#[serde(default)]
|
||||
pub constraint_hints: Vec<String>,
|
||||
}
|
||||
|
||||
impl HarnessSpec {
|
||||
/// Build a spec from a finding. Returns `None` when the finding cannot
|
||||
/// be driven dynamically (missing entry, ambient sink, etc.).
|
||||
///
|
||||
/// Stub: real impl will read `Diag::evidence.flow_steps` to pick the
|
||||
/// outermost entry function and walk the source span back to a parameter.
|
||||
pub fn from_finding(_diag: &Diag) -> Option<Self> {
|
||||
// TODO(dynamic): map flow_steps[0] -> entry function, evidence.source_span -> PayloadSlot,
|
||||
// evidence.sink_caps -> expected_cap.
|
||||
None
|
||||
}
|
||||
}
|
||||
86
src/dynamic/verify.rs
Normal file
86
src/dynamic/verify.rs
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
//! Top-level entry point for the dynamic layer.
|
||||
//!
|
||||
//! The CLI subcommand and any library consumer call [`verify_finding`].
|
||||
//! It is the only function the rest of the crate needs to know about.
|
||||
|
||||
use crate::commands::scan::Diag;
|
||||
use crate::dynamic::report::{AttemptSummary, VerifyResult, VerifyStatus};
|
||||
use crate::dynamic::runner::{run_spec, RunError};
|
||||
use crate::dynamic::sandbox::SandboxOptions;
|
||||
use crate::dynamic::spec::HarnessSpec;
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct VerifyOptions {
|
||||
pub sandbox: SandboxOptions,
|
||||
}
|
||||
|
||||
/// Try to dynamically confirm a static finding.
|
||||
///
|
||||
/// 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 {
|
||||
let finding_id = diag.id.clone();
|
||||
|
||||
let Some(spec) = HarnessSpec::from_finding(diag) else {
|
||||
return VerifyResult {
|
||||
finding_id,
|
||||
status: VerifyStatus::Unsupported,
|
||||
triggered_payload: None,
|
||||
reason: Some("no harness spec derivable from finding".into()),
|
||||
attempts: vec![],
|
||||
};
|
||||
};
|
||||
|
||||
match run_spec(&spec, &opts.sandbox) {
|
||||
Ok(run) => {
|
||||
let attempts = run
|
||||
.attempts
|
||||
.iter()
|
||||
.map(|a| AttemptSummary {
|
||||
payload_label: a.payload_label.to_string(),
|
||||
exit_code: a.outcome.exit_code,
|
||||
timed_out: a.outcome.timed_out,
|
||||
triggered: a.triggered,
|
||||
})
|
||||
.collect();
|
||||
|
||||
match run.triggered_by {
|
||||
Some(i) => VerifyResult {
|
||||
finding_id,
|
||||
status: VerifyStatus::Confirmed,
|
||||
triggered_payload: Some(run.attempts[i].payload_label.to_string()),
|
||||
reason: None,
|
||||
attempts,
|
||||
},
|
||||
None => VerifyResult {
|
||||
finding_id,
|
||||
status: VerifyStatus::NotConfirmed,
|
||||
triggered_payload: None,
|
||||
reason: None,
|
||||
attempts,
|
||||
},
|
||||
}
|
||||
}
|
||||
Err(RunError::NoPayloadsForCap) => VerifyResult {
|
||||
finding_id,
|
||||
status: VerifyStatus::Unsupported,
|
||||
triggered_payload: None,
|
||||
reason: Some("no payload corpus for sink cap".into()),
|
||||
attempts: vec![],
|
||||
},
|
||||
Err(RunError::Harness(e)) => VerifyResult {
|
||||
finding_id,
|
||||
status: VerifyStatus::Inconclusive,
|
||||
triggered_payload: None,
|
||||
reason: Some(format!("harness build failed: {e:?}")),
|
||||
attempts: vec![],
|
||||
},
|
||||
Err(RunError::Sandbox(e)) => VerifyResult {
|
||||
finding_id,
|
||||
status: VerifyStatus::Inconclusive,
|
||||
triggered_payload: None,
|
||||
reason: Some(format!("sandbox failed: {e:?}")),
|
||||
attempts: vec![],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -99,6 +99,8 @@ pub mod commands;
|
|||
pub mod constraint;
|
||||
pub mod convergence_telemetry;
|
||||
pub mod database;
|
||||
#[cfg(feature = "dynamic")]
|
||||
pub mod dynamic;
|
||||
pub mod engine_notes;
|
||||
pub mod errors;
|
||||
pub mod evidence;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue