mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-09 19:45:13 +02:00
[pitboss] phase 26: Track G.3 — End-to-end chain re-verification
This commit is contained in:
parent
4228be2db6
commit
8a801953e2
21 changed files with 991 additions and 15 deletions
200
tests/chain_reverify.rs
Normal file
200
tests/chain_reverify.rs
Normal file
|
|
@ -0,0 +1,200 @@
|
|||
//! Phase 26 — Track G.3 integration tests.
|
||||
//!
|
||||
//! Exercises the composite re-verification surface end-to-end with a
|
||||
//! stubbed reverifier so the test runs without a live sandbox backend.
|
||||
//! Two scenarios:
|
||||
//!
|
||||
//! 1. **Composite Confirms**: the stub returns `VerifyStatus::Confirmed`;
|
||||
//! the chain's severity is preserved and `reverify_reason` stays
|
||||
//! empty.
|
||||
//! 2. **Composite Inconclusive-downgrades**: the stub returns
|
||||
//! `VerifyStatus::Inconclusive`; the chain drops one severity bucket
|
||||
//! and records a typed reason on `reverify_reason`.
|
||||
//!
|
||||
//! Also covers the `reverify_top_n` cost-control gate and verifies the
|
||||
//! per-language `compose_chain_step` API surface bottoms out on
|
||||
//! [`ChainStepHarness::PREV_OUTPUT_ENV`] for every registered emitter.
|
||||
|
||||
#![cfg(feature = "dynamic")]
|
||||
|
||||
use nyx_scanner::chain::edges::FindingRef;
|
||||
use nyx_scanner::chain::finding::{ChainFinding, ChainSeverity, ChainSink};
|
||||
use nyx_scanner::chain::impact::ImpactCategory;
|
||||
use nyx_scanner::chain::reverify::{
|
||||
CompositeReverifier, reverify_chain_with, reverify_top_chains_with,
|
||||
};
|
||||
use nyx_scanner::dynamic::lang::{ChainStepHarness, compose_chain_step};
|
||||
use nyx_scanner::dynamic::verify::VerifyOptions;
|
||||
use nyx_scanner::evidence::{InconclusiveReason, VerifyResult, VerifyStatus};
|
||||
use nyx_scanner::surface::{SourceLocation, SurfaceMap};
|
||||
use nyx_scanner::symbol::Lang;
|
||||
|
||||
fn loc(file: &str, line: u32) -> SourceLocation {
|
||||
SourceLocation::new(file, line, 1)
|
||||
}
|
||||
|
||||
fn make_chain(
|
||||
hash: u64,
|
||||
severity: ChainSeverity,
|
||||
impact: ImpactCategory,
|
||||
score: f64,
|
||||
) -> ChainFinding {
|
||||
ChainFinding {
|
||||
stable_hash: hash,
|
||||
members: vec![FindingRef {
|
||||
finding_id: format!("f-{hash}"),
|
||||
stable_hash: hash,
|
||||
location: loc("app.py", 10),
|
||||
rule_id: "taint-shell-exec".into(),
|
||||
cap_bits: 0,
|
||||
}],
|
||||
sink: ChainSink {
|
||||
file: "app.py".into(),
|
||||
line: 30,
|
||||
col: 1,
|
||||
function_name: "shell.exec".into(),
|
||||
cap_bits: 0,
|
||||
},
|
||||
implied_impact: impact,
|
||||
severity,
|
||||
score,
|
||||
dynamic_verdict: None,
|
||||
reverify_reason: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn verdict(status: VerifyStatus, reason: Option<InconclusiveReason>) -> VerifyResult {
|
||||
VerifyResult {
|
||||
finding_id: "f-0".into(),
|
||||
status,
|
||||
triggered_payload: None,
|
||||
reason: None,
|
||||
inconclusive_reason: reason,
|
||||
detail: None,
|
||||
attempts: vec![],
|
||||
toolchain_match: None,
|
||||
differential: None,
|
||||
}
|
||||
}
|
||||
|
||||
struct StubReverifier(VerifyResult);
|
||||
impl CompositeReverifier for StubReverifier {
|
||||
fn reverify(
|
||||
&self,
|
||||
_chain: &ChainFinding,
|
||||
_surface: &SurfaceMap,
|
||||
_opts: &VerifyOptions,
|
||||
) -> VerifyResult {
|
||||
self.0.clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn composite_confirms_keeps_severity_and_attaches_verdict() {
|
||||
let mut chain = make_chain(0xAA, ChainSeverity::Critical, ImpactCategory::Rce, 100.0);
|
||||
let surface = SurfaceMap::new();
|
||||
let opts = VerifyOptions::default();
|
||||
let stub = StubReverifier(verdict(VerifyStatus::Confirmed, None));
|
||||
|
||||
let result = reverify_chain_with(&mut chain, &surface, &opts, &stub);
|
||||
assert!(!result.was_downgraded(), "Confirmed must not downgrade");
|
||||
assert_eq!(result.severity_before, ChainSeverity::Critical);
|
||||
assert_eq!(result.severity_after, ChainSeverity::Critical);
|
||||
assert_eq!(chain.severity, ChainSeverity::Critical);
|
||||
let attached = chain.dynamic_verdict.as_ref().expect("verdict attached");
|
||||
assert_eq!(attached.status, VerifyStatus::Confirmed);
|
||||
assert!(chain.reverify_reason.is_none(), "no reason on Confirmed");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn composite_inconclusive_downgrades_one_bucket_and_records_reason() {
|
||||
let mut chain = make_chain(0xBB, ChainSeverity::Critical, ImpactCategory::Rce, 100.0);
|
||||
let surface = SurfaceMap::new();
|
||||
let opts = VerifyOptions::default();
|
||||
let stub = StubReverifier(verdict(
|
||||
VerifyStatus::Inconclusive,
|
||||
Some(InconclusiveReason::BuildFailed),
|
||||
));
|
||||
|
||||
let result = reverify_chain_with(&mut chain, &surface, &opts, &stub);
|
||||
assert!(result.was_downgraded(), "Inconclusive must downgrade");
|
||||
assert_eq!(result.severity_before, ChainSeverity::Critical);
|
||||
assert_eq!(result.severity_after, ChainSeverity::High);
|
||||
assert_eq!(chain.severity, ChainSeverity::High);
|
||||
let reason = chain
|
||||
.reverify_reason
|
||||
.as_deref()
|
||||
.expect("reverify_reason recorded");
|
||||
assert!(
|
||||
reason.contains("BuildFailed"),
|
||||
"reason carries typed inconclusive reason; got {reason:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn top_n_limits_composite_reverification() {
|
||||
let mut chains = vec![
|
||||
make_chain(1, ChainSeverity::Critical, ImpactCategory::Rce, 200.0),
|
||||
make_chain(2, ChainSeverity::High, ImpactCategory::SessionHijack, 150.0),
|
||||
make_chain(
|
||||
3,
|
||||
ChainSeverity::Medium,
|
||||
ImpactCategory::InfoDisclosure,
|
||||
100.0,
|
||||
),
|
||||
make_chain(4, ChainSeverity::Low, ImpactCategory::InfoDisclosure, 50.0),
|
||||
];
|
||||
let surface = SurfaceMap::new();
|
||||
let opts = VerifyOptions::default();
|
||||
let stub = StubReverifier(verdict(VerifyStatus::Confirmed, None));
|
||||
|
||||
let results = reverify_top_chains_with(&mut chains, &surface, &opts, 2, &stub);
|
||||
assert_eq!(results.len(), 2);
|
||||
assert!(chains[0].dynamic_verdict.is_some());
|
||||
assert!(chains[1].dynamic_verdict.is_some());
|
||||
assert!(
|
||||
chains[2].dynamic_verdict.is_none(),
|
||||
"chain past top_n stays untouched"
|
||||
);
|
||||
assert!(
|
||||
chains[3].dynamic_verdict.is_none(),
|
||||
"chain past top_n stays untouched"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compose_chain_step_threads_prev_output_for_every_emitter() {
|
||||
// Phase 26 deliverable: each emitter exposes
|
||||
// `compose_chain_step(prev_output)`. Walk the registered languages
|
||||
// and check the prev-output env var lands in `extra_env`.
|
||||
let prev = b"chain-step-witness".as_slice();
|
||||
for lang in [
|
||||
Lang::Python,
|
||||
Lang::Rust,
|
||||
Lang::JavaScript,
|
||||
Lang::TypeScript,
|
||||
Lang::Go,
|
||||
Lang::Java,
|
||||
Lang::Php,
|
||||
Lang::Ruby,
|
||||
Lang::C,
|
||||
Lang::Cpp,
|
||||
] {
|
||||
let step = compose_chain_step(lang, Some(prev));
|
||||
assert!(
|
||||
step.extra_env
|
||||
.iter()
|
||||
.any(|(k, v)| k == ChainStepHarness::PREV_OUTPUT_ENV && v == "chain-step-witness"),
|
||||
"{lang:?} emitter must thread NYX_PREV_OUTPUT via extra_env; got {:?}",
|
||||
step.extra_env
|
||||
);
|
||||
assert!(!step.source.is_empty(), "{lang:?} step source must be non-empty");
|
||||
assert!(!step.command.is_empty(), "{lang:?} step command must be non-empty");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compose_chain_step_with_no_prev_output_has_empty_extra_env() {
|
||||
let step = compose_chain_step(Lang::Python, None);
|
||||
assert!(step.extra_env.is_empty());
|
||||
}
|
||||
|
|
@ -17,6 +17,7 @@
|
|||
//! | `src/commands/mod.rs` | `verify-feedback` subcommand (§21.2) |
|
||||
//! | `src/server/` (any file) | server start_scan verify wiring |
|
||||
//! | `src/rank.rs` | M7 rank-delta telemetry hook (§21 / M7) |
|
||||
//! | `src/chain/reverify.rs` | Phase 26 — composite chain re-verification |
|
||||
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
|
@ -30,6 +31,9 @@ const ALLOWED: &[&str] = &[
|
|||
"commands/mod.rs",
|
||||
"server/",
|
||||
"rank.rs",
|
||||
// Phase 26 — Track G.3: composite chain re-verification is the
|
||||
// public bridge between the chain composer and the dynamic verifier.
|
||||
"chain/reverify.rs",
|
||||
// The dynamic module itself is obviously allowed.
|
||||
"dynamic/",
|
||||
];
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue