nyx/tests/spec_framework_sample.rs

365 lines
12 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! 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(),
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\""));
}