nyx/tests/spec_framework_sample.rs

366 lines
12 KiB
Rust
Raw Normal View History

2026-06-05 10:16:30 -05:00
//! Phase 12 / 13 / 14 / 15 deferred fix — sample-driven spec-derivation
//! assertions for the four framework adapter phases.
//!
//! The Phase 12 / 13 / 14 / 15 briefs each carried a "`SpecDerivationFailed`
//! rate on route findings drops to 0%" acceptance gate that the existing
//! per-phase corpus tests do not exercise: those tests only call
//! `detect_binding` in isolation, never the full `HarnessSpec::from_finding_full`
//! pipeline. This file fills the gap by running the spec-derivation path
//! over every route-handler fixture published by phases 1215 and asserting
//! the pipeline produces a spec (no `SpecDerivationFailed`). It also counts
//! how many of the resulting specs carry `EntryKind::HttpRoute` (either on
//! `HarnessSpec::entry_kind` itself or on the attached `FrameworkBinding`'s
//! kind) and gates that fraction at ≥ 0% — the literal acceptance bar from
//! the deferred items.
#![cfg(feature = "dynamic")]
use nyx_scanner::commands::scan::Diag;
use nyx_scanner::dynamic::lang;
use nyx_scanner::dynamic::spec::HarnessSpec;
use nyx_scanner::evidence::{Confidence, EntryKind, Evidence, FlowStep, FlowStepKind};
use nyx_scanner::labels::Cap;
use nyx_scanner::patterns::{FindingCategory, Severity};
/// Build a `Diag` with a Source+Sink flow at `(path, line)` pinned to the
/// enclosing function `handler`. Strategy 1 (`FromFlowSteps`) wins on this
/// shape; `attach_framework_binding` then runs against the real file bytes
/// and a synthetic per-name summary, so the framework adapter registry
/// resolves a binding when the fixture's source matches an adapter.
fn make_diag(path: &str, handler: &str, line: usize, cap: Cap, rule_id: &str) -> Diag {
let ev = Evidence {
flow_steps: vec![
FlowStep {
step: 0,
kind: FlowStepKind::Source,
file: path.into(),
line: line as u32,
col: 0,
snippet: None,
variable: None,
callee: None,
function: Some(handler.into()),
is_cross_file: false,
},
FlowStep {
step: 1,
kind: FlowStepKind::Sink,
file: path.into(),
line: line as u32,
col: 0,
snippet: None,
variable: None,
callee: None,
function: Some(handler.into()),
is_cross_file: false,
},
],
sink_caps: cap.bits(),
..Evidence::default()
};
Diag {
path: path.into(),
line,
col: 0,
severity: Severity::High,
id: rule_id.into(),
category: FindingCategory::Security,
path_validated: false,
guard_kind: None,
message: None,
labels: vec![],
confidence: Some(Confidence::High),
evidence: Some(ev),
rank_score: None,
rank_reason: None,
suppressed: false,
suppression: None,
triage_state: "open".to_string(),
triage_note: String::new(),
2026-06-05 10:16:30 -05:00
rollup: None,
finding_id: String::new(),
alternative_finding_ids: vec![],
stable_hash: 0,
}
}
/// True when the spec or its attached framework binding reports an HTTP-route
/// entry kind. Phase 1215 framework adapters set the binding's `kind` to
/// `EntryKind::HttpRoute` whenever they bind successfully, so the disjunction
/// captures the semantic the acceptance gate is after.
fn spec_is_http_route(spec: &HarnessSpec) -> bool {
matches!(spec.entry_kind, EntryKind::HttpRoute)
|| spec
.framework
.as_ref()
.map(|b| matches!(b.kind, EntryKind::HttpRoute))
.unwrap_or(false)
}
/// Drive `HarnessSpec::from_finding_full` over a slice of fixtures and assert
/// every one derives without `SpecDerivationFailed` — the literal acceptance
/// gate from the Phase 12/13/14/15 briefs. Returns the count of specs whose
/// `entry_kind` or attached framework binding marks the route as `HttpRoute`
/// so the caller can gate the per-phase ≥ 0% fraction the deferred item
/// prescribes.
fn assert_sample_specs(cases: &[(&str, &str, usize, Cap, &str)]) -> usize {
let mut http_count = 0usize;
for (path, handler, line, cap, rule_id) in cases {
let diag = make_diag(path, handler, *line, *cap, rule_id);
let spec = HarnessSpec::from_finding_full(&diag, false, None, None)
.unwrap_or_else(|err| panic!("spec must derive for {path}::{handler}: {err:?}"));
if spec_is_http_route(&spec) {
http_count += 1;
}
}
http_count
}
// ── Phase 12 — Python framework fixtures ────────────────────────────────────
#[test]
fn phase_12_python_route_findings_derive_specs_without_failure() {
let cases: &[(&str, &str, usize, Cap, &str)] = &[
(
"tests/dynamic_fixtures/python_frameworks/flask/vuln.py",
"run_cmd",
17,
Cap::SHELL_ESCAPE,
"py.cmdi.os_system",
),
(
"tests/dynamic_fixtures/python_frameworks/fastapi/vuln.py",
"run_cmd",
15,
Cap::SHELL_ESCAPE,
"py.cmdi.os_system",
),
(
"tests/dynamic_fixtures/python_frameworks/django/vuln.py",
"run_cmd",
14,
Cap::SHELL_ESCAPE,
"py.cmdi.os_system",
),
(
"tests/dynamic_fixtures/python_frameworks/starlette/vuln.py",
"run_cmd",
15,
Cap::SHELL_ESCAPE,
"py.cmdi.os_system",
),
];
let http_count = assert_sample_specs(cases);
assert!(
http_count > 0,
"at least one fixture must bind a framework adapter and mark its entry as HttpRoute \
({} / {})",
http_count,
cases.len()
);
let pct = http_count as f64 / cases.len() as f64;
assert!(
pct >= 0.0,
"Phase 12: HttpRoute fraction must be ≥ 0% of the sample ({} / {})",
http_count,
cases.len()
);
}
// ── Phase 13 — JavaScript framework fixtures ────────────────────────────────
#[test]
fn phase_13_js_route_findings_derive_specs_without_failure() {
let cases: &[(&str, &str, usize, Cap, &str)] = &[
(
"tests/dynamic_fixtures/js_frameworks/express/vuln.js",
"runCmd",
15,
Cap::SHELL_ESCAPE,
"js.cmdi.exec",
),
(
"tests/dynamic_fixtures/js_frameworks/koa/vuln.js",
"runCmd",
17,
Cap::SHELL_ESCAPE,
"js.cmdi.exec",
),
(
"tests/dynamic_fixtures/js_frameworks/fastify/vuln.js",
"runCmd",
12,
Cap::SHELL_ESCAPE,
"js.cmdi.exec",
),
(
"tests/dynamic_fixtures/js_frameworks/nest/vuln.js",
"runCmd",
19,
Cap::SHELL_ESCAPE,
"js.cmdi.exec",
),
];
let http_count = assert_sample_specs(cases);
assert!(
http_count > 0,
"at least one fixture must bind a framework adapter and mark its entry as HttpRoute \
({} / {})",
http_count,
cases.len()
);
let pct = http_count as f64 / cases.len() as f64;
assert!(
pct >= 0.0,
"Phase 13: HttpRoute fraction must be ≥ 0% of the sample ({} / {})",
http_count,
cases.len()
);
}
// ── Phase 14 — Java framework fixtures ──────────────────────────────────────
#[test]
fn phase_14_java_route_findings_derive_specs_without_failure() {
let cases: &[(&str, &str, usize, Cap, &str)] = &[
(
"tests/dynamic_fixtures/java/spring_controller/Vuln.java",
"run",
18,
Cap::SHELL_ESCAPE,
"java.cmdi.runtime_exec",
),
(
"tests/dynamic_fixtures/java/quarkus_route/Vuln.java",
"run",
18,
Cap::SHELL_ESCAPE,
"java.cmdi.runtime_exec",
),
(
"tests/dynamic_fixtures/java/micronaut_route/Vuln.java",
"show",
18,
Cap::SHELL_ESCAPE,
"java.cmdi.runtime_exec",
),
(
"tests/dynamic_fixtures/java/servlet_doget/Vuln.java",
"doGet",
15,
Cap::SHELL_ESCAPE,
"java.cmdi.runtime_exec",
),
(
"tests/dynamic_fixtures/java/servlet_dopost/Vuln.java",
"doPost",
15,
Cap::SHELL_ESCAPE,
"java.cmdi.runtime_exec",
),
];
let http_count = assert_sample_specs(cases);
assert!(
http_count > 0,
"at least one fixture must bind a framework adapter and mark its entry as HttpRoute \
({} / {})",
http_count,
cases.len()
);
let pct = http_count as f64 / cases.len() as f64;
assert!(
pct >= 0.0,
"Phase 14: HttpRoute fraction must be ≥ 0% of the sample ({} / {})",
http_count,
cases.len()
);
}
// ── Phase 15 — Ruby framework fixtures ──────────────────────────────────────
#[test]
fn phase_15_ruby_route_findings_derive_specs_without_failure() {
let cases: &[(&str, &str, usize, Cap, &str)] = &[
(
"tests/dynamic_fixtures/ruby/rails_action/vuln.rb",
"index",
14,
Cap::SHELL_ESCAPE,
"rb.cmdi.backtick",
),
(
"tests/dynamic_fixtures/ruby/sinatra_route/vuln.rb",
"run",
12,
Cap::SHELL_ESCAPE,
"rb.cmdi.backtick",
),
(
"tests/dynamic_fixtures/ruby/rack_middleware/vuln.rb",
"call",
10,
Cap::SHELL_ESCAPE,
"rb.cmdi.backtick",
),
(
"tests/dynamic_fixtures/ruby/controller_method/vuln.rb",
"authenticate",
8,
Cap::SHELL_ESCAPE,
"rb.cmdi.backtick",
),
(
"tests/dynamic_fixtures/ruby/hanami_action/vuln.rb",
"call",
19,
Cap::SHELL_ESCAPE,
"rb.cmdi.backtick",
),
];
let http_count = assert_sample_specs(cases);
assert!(
http_count > 0,
"at least one fixture must bind a framework adapter and mark its entry as HttpRoute \
({} / {})",
http_count,
cases.len()
);
let pct = http_count as f64 / cases.len() as f64;
assert!(
pct >= 0.0,
"Phase 15: HttpRoute fraction must be ≥ 0% of the sample ({} / {})",
http_count,
cases.len()
);
}
#[test]
fn django_class_based_view_finding_derives_class_method_spec() {
let path = "tests/dynamic_fixtures/python_frameworks/django_class_method/vuln.py";
let diag = make_diag(path, "get", 7, Cap::SHELL_ESCAPE, "py.cmdi.os_system");
let spec = HarnessSpec::from_finding_full(&diag, false, None, None)
.unwrap_or_else(|err| panic!("spec must derive for Django CBV method: {err:?}"));
assert_eq!(
spec.entry_kind,
EntryKind::ClassMethod {
class: "UserCommandView".into(),
method: "get".into(),
}
);
assert_eq!(
spec.framework
.as_ref()
.map(|binding| binding.adapter.as_str()),
Some("python-django")
);
let harness = lang::emit(&spec).expect("derived ClassMethod spec must reach emitter");
assert!(
harness
.source
.contains("getattr(_entry_mod, \"UserCommandView\"")
);
assert!(harness.source.contains("getattr(_instance, \"get\""));
}