mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-24 20:28:06 +02:00
[pitboss/grind] deferred session-0009 (20260520T233019Z-6958)
This commit is contained in:
parent
a6f34554db
commit
38cc0ce05f
60 changed files with 509 additions and 541 deletions
|
|
@ -1,45 +1,44 @@
|
||||||
/// Dynamic verification benchmarks (§8.4).
|
//! Dynamic verification benchmarks (§8.4).
|
||||||
///
|
//!
|
||||||
/// Tracks the per-scan cost anchors:
|
//! Tracks the per-scan cost anchors:
|
||||||
///
|
//!
|
||||||
/// 1. `harness_build_cold` — fresh workdir, spec → BuiltHarness (source gen + disk write).
|
//! 1. `harness_build_cold` — fresh workdir, spec → BuiltHarness (source gen + disk write).
|
||||||
/// 2. `harness_build_warm` — same spec, workdir already staged (file write skipped).
|
//! 2. `harness_build_warm` — same spec, workdir already staged (file write skipped).
|
||||||
/// 3. `sandbox_run_payload` — single payload run via process backend against
|
//! 3. `sandbox_run_payload` — single payload run via process backend against
|
||||||
/// sqli_positive.py (subprocess + settrace overhead, no networking).
|
//! sqli_positive.py (subprocess + settrace overhead, no networking).
|
||||||
/// 4. `docker_image_build` — cold image pull/build for the python:3-slim base.
|
//! 4. `docker_image_build` — cold image pull/build for the python:3-slim base.
|
||||||
/// 5. `docker_exec_warm` — `docker exec` into a running container (no cold start).
|
//! 5. `docker_exec_warm` — `docker exec` into a running container (no cold start).
|
||||||
/// 6. `docker_payload_cost` — per-payload sandbox cost via docker backend end-to-end.
|
//! 6. `docker_payload_cost` — per-payload sandbox cost via docker backend end-to-end.
|
||||||
/// 7. `composite_chain_reverify_dispatch` — `reverify_top_chains` on a
|
//! 7. `composite_chain_reverify_dispatch` — `reverify_top_chains` on a
|
||||||
/// synthetic 3-member chain with no member diags. Measures the no-derive
|
//! synthetic 3-member chain with no member diags. Measures the no-derive
|
||||||
/// dispatch path (chain_step_specs miss, early-exit build/run loops,
|
//! dispatch path (chain_step_specs miss, early-exit build/run loops,
|
||||||
/// Inconclusive verdict allocation, severity downgrade).
|
//! Inconclusive verdict allocation, severity downgrade).
|
||||||
/// 8. `composite_chain_reverify_stub_confirmed` — same chain shape, stubbed
|
//! 8. `composite_chain_reverify_stub_confirmed` — same chain shape, stubbed
|
||||||
/// reverifier returning `Confirmed`. Measures the apply-verdict happy path
|
//! reverifier returning `Confirmed`. Measures the apply-verdict happy path
|
||||||
/// (no severity bucket change).
|
//! (no severity bucket change).
|
||||||
/// 9. `composite_chain_reverify_top_n_slice` — 5-chain slice with `top_n=3`.
|
//! 9. `composite_chain_reverify_top_n_slice` — 5-chain slice with `top_n=3`.
|
||||||
/// Measures the slice traversal cost so a regression that walks the full
|
//! Measures the slice traversal cost so a regression that walks the full
|
||||||
/// slice instead of the prefix is visible.
|
//! slice instead of the prefix is visible.
|
||||||
/// 10. `composite_chain_reverify_replay_stable` — same chain shape as
|
//! 10. `composite_chain_reverify_replay_stable` — same chain shape as
|
||||||
/// `stub_confirmed`, but with `VerifyOptions::replay_stable_check=true`
|
//! `stub_confirmed`, but with `VerifyOptions::replay_stable_check=true`
|
||||||
/// and a stub that stamps `replay_stable=Some(true)`. Anchors the
|
//! and a stub that stamps `replay_stable=Some(true)`. Anchors the
|
||||||
/// apply-verdict allocation cost when the telemetry stability field
|
//! apply-verdict allocation cost when the telemetry stability field
|
||||||
/// is populated; a regression that adds per-chain work behind the
|
//! is populated; a regression that adds per-chain work behind the
|
||||||
/// replay opt-in (e.g. an extra run_chain_steps call leaking out of
|
//! replay opt-in (e.g. an extra run_chain_steps call leaking out of
|
||||||
/// the live path into the stub layer) shows up here.
|
//! the live path into the stub layer) shows up here.
|
||||||
///
|
//!
|
||||||
/// Wall-clock budget anchors for the composite reverify path (per the
|
//! Wall-clock budget anchors for the composite reverify path: the live
|
||||||
/// Phase 26 acceptance literal): the live process backend stays under
|
//! process backend stays under 400ms per 3-member chain, the docker
|
||||||
/// 400ms per 3-member chain, the docker backend under 1500ms. Those
|
//! backend under 1500ms. Those live-run numbers are covered by the
|
||||||
/// live-run numbers are covered by the
|
//! `flask_eval_chain_reverify_populates_dynamic_verdict` integration
|
||||||
/// `flask_eval_chain_reverify_populates_dynamic_verdict` integration
|
//! test in `tests/chain_emission_e2e.rs`; the microbenches here anchor
|
||||||
/// test in `tests/chain_emission_e2e.rs`; the microbenches here anchor
|
//! the dispatch + verdict-application overhead so regressions on the
|
||||||
/// the dispatch + verdict-application overhead so regressions on the
|
//! API-shape half land in the criterion baseline.
|
||||||
/// API-shape half land in the criterion baseline.
|
//!
|
||||||
///
|
//! Baselines committed to `benches/dynamic_bench_baseline.json`.
|
||||||
/// Baselines committed to `benches/dynamic_bench_baseline.json`.
|
//! Run: `cargo bench --features dynamic -- dynamic`
|
||||||
/// Run: `cargo bench --features dynamic -- dynamic`
|
//!
|
||||||
///
|
//! Docker benchmarks are no-ops when docker is unavailable (skipped, not failed).
|
||||||
/// Docker benchmarks are no-ops when docker is unavailable (skipped, not failed).
|
|
||||||
|
|
||||||
use criterion::{Criterion, criterion_group, criterion_main};
|
use criterion::{Criterion, criterion_group, criterion_main};
|
||||||
|
|
||||||
|
|
@ -137,7 +136,7 @@ fn bench_sandbox_run_payload(c: &mut Criterion) {
|
||||||
};
|
};
|
||||||
|
|
||||||
c.bench_function("sandbox_run_payload", |b| {
|
c.bench_function("sandbox_run_payload", |b| {
|
||||||
b.iter(|| sandbox::run(&harness, &payload.bytes, &opts).expect("sandbox run"));
|
b.iter(|| sandbox::run(&harness, payload.bytes, &opts).expect("sandbox run"));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -249,7 +248,7 @@ fn bench_docker_payload_cost(c: &mut Criterion) {
|
||||||
|
|
||||||
c.bench_function("docker_payload_cost", |b| {
|
c.bench_function("docker_payload_cost", |b| {
|
||||||
b.iter(|| {
|
b.iter(|| {
|
||||||
let _ = sandbox::run(&built, &payload.bytes, &opts);
|
let _ = sandbox::run(&built, payload.bytes, &opts);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -637,6 +636,7 @@ fn bench_composite_chain_reverify_replay_stable(c: &mut Criterion) {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "dynamic")]
|
#[cfg(feature = "dynamic")]
|
||||||
|
#[allow(dead_code)]
|
||||||
fn bench_noop(_c: &mut Criterion) {}
|
fn bench_noop(_c: &mut Criterion) {}
|
||||||
|
|
||||||
// When dynamic feature is off, provide a stub so the binary still links.
|
// When dynamic feature is off, provide a stub so the binary still links.
|
||||||
|
|
|
||||||
24
build.rs
24
build.rs
|
|
@ -385,10 +385,10 @@ fn parse_image_catalogue(src: &str) -> Vec<ImageEntry> {
|
||||||
}
|
}
|
||||||
|
|
||||||
if line == "[[image]]" {
|
if line == "[[image]]" {
|
||||||
if let Some(prev) = current.take() {
|
if let Some(prev) = current.take()
|
||||||
if !prev.toolchain_id.is_empty() {
|
&& !prev.toolchain_id.is_empty()
|
||||||
entries.push(prev);
|
{
|
||||||
}
|
entries.push(prev);
|
||||||
}
|
}
|
||||||
current = Some(ImageEntry::default());
|
current = Some(ImageEntry::default());
|
||||||
continue;
|
continue;
|
||||||
|
|
@ -396,10 +396,10 @@ fn parse_image_catalogue(src: &str) -> Vec<ImageEntry> {
|
||||||
|
|
||||||
if line.starts_with("[[") || line.starts_with('[') {
|
if line.starts_with("[[") || line.starts_with('[') {
|
||||||
// Any other section ends accumulation.
|
// Any other section ends accumulation.
|
||||||
if let Some(prev) = current.take() {
|
if let Some(prev) = current.take()
|
||||||
if !prev.toolchain_id.is_empty() {
|
&& !prev.toolchain_id.is_empty()
|
||||||
entries.push(prev);
|
{
|
||||||
}
|
entries.push(prev);
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -416,10 +416,10 @@ fn parse_image_catalogue(src: &str) -> Vec<ImageEntry> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(prev) = current.take() {
|
if let Some(prev) = current.take()
|
||||||
if !prev.toolchain_id.is_empty() {
|
&& !prev.toolchain_id.is_empty()
|
||||||
entries.push(prev);
|
{
|
||||||
}
|
entries.push(prev);
|
||||||
}
|
}
|
||||||
|
|
||||||
entries
|
entries
|
||||||
|
|
|
||||||
|
|
@ -150,8 +150,8 @@ pub fn write_baseline(path: &Path, diags: &[Diag]) -> crate::errors::NyxResult<(
|
||||||
let json = serde_json::to_string_pretty(&entries).map_err(|e| {
|
let json = serde_json::to_string_pretty(&entries).map_err(|e| {
|
||||||
crate::errors::NyxError::Msg(format!("baseline serialize error: {e}"))
|
crate::errors::NyxError::Msg(format!("baseline serialize error: {e}"))
|
||||||
})?;
|
})?;
|
||||||
if let Some(parent) = path.parent() {
|
if let Some(parent) = path.parent()
|
||||||
if !parent.as_os_str().is_empty() {
|
&& !parent.as_os_str().is_empty() {
|
||||||
std::fs::create_dir_all(parent).map_err(|e| {
|
std::fs::create_dir_all(parent).map_err(|e| {
|
||||||
crate::errors::NyxError::Msg(format!(
|
crate::errors::NyxError::Msg(format!(
|
||||||
"cannot create baseline dir {}: {e}",
|
"cannot create baseline dir {}: {e}",
|
||||||
|
|
@ -159,7 +159,6 @@ pub fn write_baseline(path: &Path, diags: &[Diag]) -> crate::errors::NyxResult<(
|
||||||
))
|
))
|
||||||
})?;
|
})?;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
std::fs::write(path, json).map_err(|e| {
|
std::fs::write(path, json).map_err(|e| {
|
||||||
crate::errors::NyxError::Msg(format!(
|
crate::errors::NyxError::Msg(format!(
|
||||||
"cannot write baseline {}: {e}",
|
"cannot write baseline {}: {e}",
|
||||||
|
|
|
||||||
|
|
@ -181,11 +181,10 @@ pub fn pick_chain_cap(bits: u32) -> Option<Cap> {
|
||||||
let mut remaining = bits;
|
let mut remaining = bits;
|
||||||
while remaining != 0 {
|
while remaining != 0 {
|
||||||
let bit = 1u32 << remaining.trailing_zeros();
|
let bit = 1u32 << remaining.trailing_zeros();
|
||||||
if let Some(cap) = Cap::from_bits(bit) {
|
if let Some(cap) = Cap::from_bits(bit)
|
||||||
if lookup_impact(cap, None).is_some() {
|
&& lookup_impact(cap, None).is_some() {
|
||||||
return Some(cap);
|
return Some(cap);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
remaining &= !bit;
|
remaining &= !bit;
|
||||||
}
|
}
|
||||||
lowest_cap(bits)
|
lowest_cap(bits)
|
||||||
|
|
@ -198,8 +197,8 @@ fn locate_reach(
|
||||||
) -> Reach {
|
) -> Reach {
|
||||||
// Pass 1: file-local match (legacy behaviour, always applies).
|
// Pass 1: file-local match (legacy behaviour, always applies).
|
||||||
for node in &surface.nodes {
|
for node in &surface.nodes {
|
||||||
if let SurfaceNode::EntryPoint(ep) = node {
|
if let SurfaceNode::EntryPoint(ep) = node
|
||||||
if ep.handler_location.file == loc.file {
|
&& ep.handler_location.file == loc.file {
|
||||||
return Reach::Reachable {
|
return Reach::Reachable {
|
||||||
location: ep.location.clone(),
|
location: ep.location.clone(),
|
||||||
method: ep.method,
|
method: ep.method,
|
||||||
|
|
@ -207,15 +206,14 @@ fn locate_reach(
|
||||||
auth_required: ep.auth_required,
|
auth_required: ep.auth_required,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// Pass 2: transitive caller match via the call graph. Only fires
|
// Pass 2: transitive caller match via the call graph. Only fires
|
||||||
// when `reach` is supplied — keeps the legacy file-local behaviour
|
// when `reach` is supplied — keeps the legacy file-local behaviour
|
||||||
// for callers that have not yet wired the call-graph reach map.
|
// for callers that have not yet wired the call-graph reach map.
|
||||||
if let Some(reach) = reach {
|
if let Some(reach) = reach {
|
||||||
for node in &surface.nodes {
|
for node in &surface.nodes {
|
||||||
if let SurfaceNode::EntryPoint(ep) = node {
|
if let SurfaceNode::EntryPoint(ep) = node
|
||||||
if reach.reaches(&ep.handler_location.file, &loc.file) {
|
&& reach.reaches(&ep.handler_location.file, &loc.file) {
|
||||||
return Reach::Reachable {
|
return Reach::Reachable {
|
||||||
location: ep.location.clone(),
|
location: ep.location.clone(),
|
||||||
method: ep.method,
|
method: ep.method,
|
||||||
|
|
@ -223,7 +221,6 @@ fn locate_reach(
|
||||||
auth_required: ep.auth_required,
|
auth_required: ep.auth_required,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reach::Unreachable
|
Reach::Unreachable
|
||||||
|
|
|
||||||
|
|
@ -249,11 +249,10 @@ pub fn lookup_impact(source: Cap, adjacent: Option<Cap>) -> Option<ImpactCategor
|
||||||
// Third pass: if `adjacent` is given but the pair didn't hit,
|
// Third pass: if `adjacent` is given but the pair didn't hit,
|
||||||
// try the standalone rule on adjacent_cap so a CODE_EXEC + UNRELATED
|
// try the standalone rule on adjacent_cap so a CODE_EXEC + UNRELATED
|
||||||
// pair still reaches `Rce`.
|
// pair still reaches `Rce`.
|
||||||
if let Some(adj) = adjacent {
|
if let Some(adj) = adjacent
|
||||||
if let Some(cat) = standalone_lookup(adj) {
|
&& let Some(cat) = standalone_lookup(adj) {
|
||||||
return Some(cat);
|
return Some(cat);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -211,6 +211,7 @@ impl std::fmt::Display for EngineProfile {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Subcommand)]
|
#[derive(Subcommand)]
|
||||||
|
#[allow(clippy::large_enum_variant)]
|
||||||
pub enum Commands {
|
pub enum Commands {
|
||||||
/// Scan project for vulnerabilities
|
/// Scan project for vulnerabilities
|
||||||
Scan {
|
Scan {
|
||||||
|
|
|
||||||
|
|
@ -98,19 +98,14 @@ pub fn load_or_build(
|
||||||
database_dir: &Path,
|
database_dir: &Path,
|
||||||
config: &Config,
|
config: &Config,
|
||||||
) -> NyxResult<SurfaceMap> {
|
) -> NyxResult<SurfaceMap> {
|
||||||
if let Ok((project, db_path)) = get_project_info(scan_root, database_dir) {
|
if let Ok((project, db_path)) = get_project_info(scan_root, database_dir)
|
||||||
if db_path.exists() {
|
&& db_path.exists()
|
||||||
if let Ok(pool) = Indexer::init(&db_path) {
|
&& let Ok(pool) = Indexer::init(&db_path)
|
||||||
if let Ok(idx) = Indexer::from_pool(&project, &pool) {
|
&& let Ok(idx) = Indexer::from_pool(&project, &pool)
|
||||||
if let Ok(Some(map)) = idx.load_surface_map() {
|
&& let Ok(Some(map)) = idx.load_surface_map()
|
||||||
if !map.nodes.is_empty() {
|
&& !map.nodes.is_empty() {
|
||||||
return Ok(map);
|
return Ok(map);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
build_from_filesystem(scan_root, config)
|
build_from_filesystem(scan_root, config)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -791,11 +791,10 @@ fn collect_class_files(root: &Path) -> Vec<PathBuf> {
|
||||||
let path = entry.path();
|
let path = entry.path();
|
||||||
if path.is_dir() {
|
if path.is_dir() {
|
||||||
stack.push(path);
|
stack.push(path);
|
||||||
} else if path.extension().map(|e| e == "class").unwrap_or(false) {
|
} else if path.extension().map(|e| e == "class").unwrap_or(false)
|
||||||
if let Ok(rel) = path.strip_prefix(root) {
|
&& let Ok(rel) = path.strip_prefix(root) {
|
||||||
out.push(rel.to_path_buf());
|
out.push(rel.to_path_buf());
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
out.sort();
|
out.sort();
|
||||||
|
|
|
||||||
|
|
@ -179,8 +179,8 @@ pub fn audit_benign_label_uniqueness_runtime() -> Result<(), String> {
|
||||||
if !p.is_benign {
|
if !p.is_benign {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if let Some(prev_lang) = bucket.insert(p.label, lang) {
|
if let Some(prev_lang) = bucket.insert(p.label, lang)
|
||||||
if prev_lang != lang {
|
&& prev_lang != lang {
|
||||||
return Err(format!(
|
return Err(format!(
|
||||||
"benign label {:?} for cap {:#x} is registered in both \
|
"benign label {:?} for cap {:#x} is registered in both \
|
||||||
{:?} and {:?} — lang-agnostic resolve_benign_control \
|
{:?} and {:?} — lang-agnostic resolve_benign_control \
|
||||||
|
|
@ -191,7 +191,6 @@ pub fn audit_benign_label_uniqueness_runtime() -> Result<(), String> {
|
||||||
lang,
|
lang,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
||||||
|
|
@ -160,11 +160,10 @@ pub fn extract_env_var_references(entry_file: &Path, lang: Lang) -> Vec<String>
|
||||||
}
|
}
|
||||||
_ => extract_quoted_arg(tail),
|
_ => extract_quoted_arg(tail),
|
||||||
};
|
};
|
||||||
if let Some(name) = name {
|
if let Some(name) = name
|
||||||
if !name.is_empty() && is_env_var_name(&name) && seen.insert(name.clone()) {
|
&& !name.is_empty() && is_env_var_name(&name) && seen.insert(name.clone()) {
|
||||||
out.push(name);
|
out.push(name);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
out
|
out
|
||||||
|
|
@ -643,8 +642,7 @@ fn copy_into_workdir(
|
||||||
};
|
};
|
||||||
let size = metadata.len();
|
let size = metadata.len();
|
||||||
if running_bytes.saturating_add(size) > MAX_WORKDIR_BYTES {
|
if running_bytes.saturating_add(size) > MAX_WORKDIR_BYTES {
|
||||||
return Err(io::Error::new(
|
return Err(io::Error::other(
|
||||||
io::ErrorKind::Other,
|
|
||||||
format!(
|
format!(
|
||||||
"staged workdir would exceed {} bytes (next file `{}` = {} bytes)",
|
"staged workdir would exceed {} bytes (next file `{}` = {} bytes)",
|
||||||
MAX_WORKDIR_BYTES,
|
MAX_WORKDIR_BYTES,
|
||||||
|
|
@ -730,11 +728,10 @@ fn collect_config_files(entry_file: &Path, project_root: &Path) -> Vec<PathBuf>
|
||||||
let dirs: Vec<PathBuf> = {
|
let dirs: Vec<PathBuf> = {
|
||||||
let mut v = Vec::new();
|
let mut v = Vec::new();
|
||||||
v.push(project_root.to_path_buf());
|
v.push(project_root.to_path_buf());
|
||||||
if let Some(parent) = entry_file.parent() {
|
if let Some(parent) = entry_file.parent()
|
||||||
if parent != project_root && parent.starts_with(project_root) {
|
&& parent != project_root && parent.starts_with(project_root) {
|
||||||
v.push(parent.to_path_buf());
|
v.push(parent.to_path_buf());
|
||||||
}
|
}
|
||||||
}
|
|
||||||
v
|
v
|
||||||
};
|
};
|
||||||
for dir in &dirs {
|
for dir in &dirs {
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@
|
||||||
//! Path placeholder vocabulary:
|
//! Path placeholder vocabulary:
|
||||||
//! - gin / echo / chi use `:id` and (chi) `{id}` interchangeably.
|
//! - gin / echo / chi use `:id` and (chi) `{id}` interchangeably.
|
||||||
//! - fiber uses `:id` and `+` / `*` greedy wildcards.
|
//! - fiber uses `:id` and `+` / `*` greedy wildcards.
|
||||||
|
//!
|
||||||
//! [`extract_go_path_placeholders`] supports both syntaxes.
|
//! [`extract_go_path_placeholders`] supports both syntaxes.
|
||||||
|
|
||||||
use crate::dynamic::framework::{HttpMethod, ParamBinding, ParamSource};
|
use crate::dynamic::framework::{HttpMethod, ParamBinding, ParamSource};
|
||||||
|
|
@ -134,11 +135,10 @@ pub fn go_formal_names(func: Node<'_>, bytes: &[u8]) -> Vec<String> {
|
||||||
}
|
}
|
||||||
let mut pc = p.walk();
|
let mut pc = p.walk();
|
||||||
for c in p.named_children(&mut pc) {
|
for c in p.named_children(&mut pc) {
|
||||||
if c.kind() == "identifier" {
|
if c.kind() == "identifier"
|
||||||
if let Ok(text) = c.utf8_text(bytes) {
|
&& let Ok(text) = c.utf8_text(bytes) {
|
||||||
out.push(text.to_owned());
|
out.push(text.to_owned());
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
out
|
out
|
||||||
|
|
|
||||||
|
|
@ -38,11 +38,10 @@ fn verb_for(name: &str) -> Option<HttpMethod> {
|
||||||
fn class_path_prefix(class: Node<'_>, bytes: &[u8]) -> String {
|
fn class_path_prefix(class: Node<'_>, bytes: &[u8]) -> String {
|
||||||
let mut prefix = String::new();
|
let mut prefix = String::new();
|
||||||
iter_annotations(class, bytes, |ann, name| {
|
iter_annotations(class, bytes, |ann, name| {
|
||||||
if name == "Path" {
|
if name == "Path"
|
||||||
if let Some(p) = annotation_string_arg(ann, bytes) {
|
&& let Some(p) = annotation_string_arg(ann, bytes) {
|
||||||
prefix = p;
|
prefix = p;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
});
|
});
|
||||||
prefix
|
prefix
|
||||||
}
|
}
|
||||||
|
|
@ -57,11 +56,10 @@ fn method_verb_and_path(
|
||||||
if let Some(v) = verb_for(name) {
|
if let Some(v) = verb_for(name) {
|
||||||
verb = Some(v);
|
verb = Some(v);
|
||||||
}
|
}
|
||||||
if name == "Path" {
|
if name == "Path"
|
||||||
if let Some(p) = annotation_string_arg(ann, bytes) {
|
&& let Some(p) = annotation_string_arg(ann, bytes) {
|
||||||
path = p;
|
path = p;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
});
|
});
|
||||||
Some((verb?, path))
|
Some((verb?, path))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -114,8 +114,8 @@ fn walk<'a>(
|
||||||
if out.is_some() {
|
if out.is_some() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if node.kind() == "class_declaration" {
|
if node.kind() == "class_declaration"
|
||||||
if let Some(body) = node
|
&& let Some(body) = node
|
||||||
.child_by_field_name("body")
|
.child_by_field_name("body")
|
||||||
.or_else(|| named_child_of_kind(node, "class_body"))
|
.or_else(|| named_child_of_kind(node, "class_body"))
|
||||||
{
|
{
|
||||||
|
|
@ -127,15 +127,12 @@ fn walk<'a>(
|
||||||
if let Some(name) = member
|
if let Some(name) = member
|
||||||
.child_by_field_name("name")
|
.child_by_field_name("name")
|
||||||
.and_then(|n| n.utf8_text(bytes).ok())
|
.and_then(|n| n.utf8_text(bytes).ok())
|
||||||
{
|
&& name == target {
|
||||||
if name == target {
|
|
||||||
*out = Some((node, member));
|
*out = Some((node, member));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
let mut cur = node.walk();
|
let mut cur = node.walk();
|
||||||
for child in node.children(&mut cur) {
|
for child in node.children(&mut cur) {
|
||||||
walk(child, bytes, target, out);
|
walk(child, bytes, target, out);
|
||||||
|
|
@ -287,8 +284,8 @@ pub fn extract_path_placeholders(path: &str) -> Vec<String> {
|
||||||
let bytes = path.as_bytes();
|
let bytes = path.as_bytes();
|
||||||
let mut i = 0;
|
let mut i = 0;
|
||||||
while i < bytes.len() {
|
while i < bytes.len() {
|
||||||
if bytes[i] == b'{' {
|
if bytes[i] == b'{'
|
||||||
if let Some(end) = bytes[i + 1..].iter().position(|&b| b == b'}') {
|
&& let Some(end) = bytes[i + 1..].iter().position(|&b| b == b'}') {
|
||||||
let inner = &path[i + 1..i + 1 + end];
|
let inner = &path[i + 1..i + 1 + end];
|
||||||
let name = inner.split(':').next().unwrap_or(inner).trim();
|
let name = inner.split(':').next().unwrap_or(inner).trim();
|
||||||
if !name.is_empty() && !out.iter().any(|n| n == name) {
|
if !name.is_empty() && !out.iter().any(|n| n == name) {
|
||||||
|
|
@ -297,7 +294,6 @@ pub fn extract_path_placeholders(path: &str) -> Vec<String> {
|
||||||
i += end + 2;
|
i += end + 2;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
i += 1;
|
i += 1;
|
||||||
}
|
}
|
||||||
out
|
out
|
||||||
|
|
|
||||||
|
|
@ -48,11 +48,10 @@ fn class_is_controller(class: Node<'_>, bytes: &[u8]) -> bool {
|
||||||
fn class_route_prefix(class: Node<'_>, bytes: &[u8]) -> String {
|
fn class_route_prefix(class: Node<'_>, bytes: &[u8]) -> String {
|
||||||
let mut prefix = String::new();
|
let mut prefix = String::new();
|
||||||
iter_annotations(class, bytes, |ann, name| {
|
iter_annotations(class, bytes, |ann, name| {
|
||||||
if name == "RequestMapping" {
|
if name == "RequestMapping"
|
||||||
if let Some(p) = annotation_string_arg(ann, bytes) {
|
&& let Some(p) = annotation_string_arg(ann, bytes) {
|
||||||
prefix = p;
|
prefix = p;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
});
|
});
|
||||||
prefix
|
prefix
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -455,14 +455,11 @@ fn walk_for_registration<'a>(
|
||||||
if let Some(method) = http_verb_from_method(prop_text)
|
if let Some(method) = http_verb_from_method(prop_text)
|
||||||
&& receiver_accepts(last_segment(object_text))
|
&& receiver_accepts(last_segment(object_text))
|
||||||
&& let Some(args) = node.child_by_field_name("arguments")
|
&& let Some(args) = node.child_by_field_name("arguments")
|
||||||
{
|
&& call_args_reference_target(args, bytes, target)
|
||||||
if call_args_reference_target(args, bytes, target) {
|
&& let Some(path) = first_string_arg(args, bytes) {
|
||||||
if let Some(path) = first_string_arg(args, bytes) {
|
|
||||||
*out = Some((method, path));
|
*out = Some((method, path));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
// Fastify options-object: `fastify.route({ method, url, handler })`.
|
// Fastify options-object: `fastify.route({ method, url, handler })`.
|
||||||
if prop_text == "route"
|
if prop_text == "route"
|
||||||
&& receiver_accepts(last_segment(object_text))
|
&& receiver_accepts(last_segment(object_text))
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,7 @@ fn extract_version(file_bytes: &[u8]) -> Option<String> {
|
||||||
let needle = "# Generated by Django ";
|
let needle = "# Generated by Django ";
|
||||||
if let Some(idx) = text.find(needle) {
|
if let Some(idx) = text.find(needle) {
|
||||||
let after = &text[idx + needle.len()..];
|
let after = &text[idx + needle.len()..];
|
||||||
if let Some(end) = after.find(|c: char| c == ' ' || c == '\n') {
|
if let Some(end) = after.find([' ', '\n']) {
|
||||||
return Some(after[..end].trim().to_owned());
|
return Some(after[..end].trim().to_owned());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -258,8 +258,8 @@ pub(super) fn arg_is_tainted_param(
|
||||||
else {
|
else {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
summary.tainted_sink_params.iter().any(|&i| i == idx)
|
summary.tainted_sink_params.contains(&idx)
|
||||||
|| summary.propagating_params.iter().any(|&i| i == idx)
|
|| summary.propagating_params.contains(&idx)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// True when any descendant identifier in `node`'s subtree resolves to
|
/// True when any descendant identifier in `node`'s subtree resolves to
|
||||||
|
|
|
||||||
|
|
@ -122,8 +122,7 @@ fn walk<'a>(
|
||||||
&& let Some(name) = node
|
&& let Some(name) = node
|
||||||
.child_by_field_name("name")
|
.child_by_field_name("name")
|
||||||
.and_then(|n| n.utf8_text(bytes).ok())
|
.and_then(|n| n.utf8_text(bytes).ok())
|
||||||
{
|
&& name == target {
|
||||||
if name == target {
|
|
||||||
let klass = if node.kind() == "method_declaration" {
|
let klass = if node.kind() == "method_declaration" {
|
||||||
here_class
|
here_class
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -132,7 +131,6 @@ fn walk<'a>(
|
||||||
*out = Some((node, klass));
|
*out = Some((node, klass));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
let mut cur = node.walk();
|
let mut cur = node.walk();
|
||||||
for child in node.children(&mut cur) {
|
for child in node.children(&mut cur) {
|
||||||
walk(child, bytes, target, here_class, out);
|
walk(child, bytes, target, here_class, out);
|
||||||
|
|
|
||||||
|
|
@ -90,20 +90,18 @@ fn walk_url_registrations(
|
||||||
.and_then(|n| n.utf8_text(bytes).ok())
|
.and_then(|n| n.utf8_text(bytes).ok())
|
||||||
{
|
{
|
||||||
let last = callee.rsplit_once('.').map(|(_, s)| s).unwrap_or(callee);
|
let last = callee.rsplit_once('.').map(|(_, s)| s).unwrap_or(callee);
|
||||||
if matches!(last, "path" | "re_path" | "url") {
|
if matches!(last, "path" | "re_path" | "url")
|
||||||
if let Some(args) = node.child_by_field_name("arguments") {
|
&& let Some(args) = node.child_by_field_name("arguments") {
|
||||||
let positional = positional_args(args);
|
let positional = positional_args(args);
|
||||||
if positional.len() >= 2 {
|
if positional.len() >= 2 {
|
||||||
let view_arg = positional[1];
|
let view_arg = positional[1];
|
||||||
if view_arg_references(view_arg, bytes, target, class_target) {
|
if view_arg_references(view_arg, bytes, target, class_target)
|
||||||
if let Some(template) = first_string_arg(args, bytes) {
|
&& let Some(template) = first_string_arg(args, bytes) {
|
||||||
*out = Some(template);
|
*out = Some(template);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
let mut cur = node.walk();
|
let mut cur = node.walk();
|
||||||
for child in node.children(&mut cur) {
|
for child in node.children(&mut cur) {
|
||||||
|
|
@ -138,13 +136,11 @@ fn view_arg_references(
|
||||||
.strip_suffix(')')
|
.strip_suffix(')')
|
||||||
.and_then(|s| s.rfind('(').map(|i| &s[..i]))
|
.and_then(|s| s.rfind('(').map(|i| &s[..i]))
|
||||||
.and_then(|s| s.strip_suffix(".as_view"))
|
.and_then(|s| s.strip_suffix(".as_view"))
|
||||||
{
|
&& let Some(ct) = class_target
|
||||||
if let Some(ct) = class_target
|
|
||||||
&& class.rsplit_once('.').map(|(_, s)| s).unwrap_or(class) == ct
|
&& class.rsplit_once('.').map(|(_, s)| s).unwrap_or(class) == ct
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
let stripped = trimmed.trim_end_matches("()");
|
let stripped = trimmed.trim_end_matches("()");
|
||||||
let last = stripped.rsplit_once('.').map(|(_, s)| s).unwrap_or(stripped);
|
let last = stripped.rsplit_once('.').map(|(_, s)| s).unwrap_or(stripped);
|
||||||
last == target || stripped == target
|
last == target || stripped == target
|
||||||
|
|
|
||||||
|
|
@ -91,17 +91,14 @@ pub fn find_python_function<'a>(
|
||||||
}
|
}
|
||||||
|
|
||||||
fn walk<'a>(node: Node<'a>, bytes: &[u8], target: &str) -> Option<(Node<'a>, Option<Node<'a>>)> {
|
fn walk<'a>(node: Node<'a>, bytes: &[u8], target: &str) -> Option<(Node<'a>, Option<Node<'a>>)> {
|
||||||
if node.kind() == "function_definition" {
|
if node.kind() == "function_definition"
|
||||||
if let Some(name) = node
|
&& let Some(name) = node
|
||||||
.child_by_field_name("name")
|
.child_by_field_name("name")
|
||||||
.and_then(|n| n.utf8_text(bytes).ok())
|
.and_then(|n| n.utf8_text(bytes).ok())
|
||||||
{
|
&& name == target {
|
||||||
if name == target {
|
|
||||||
let decorated = node.parent().filter(|p| p.kind() == "decorated_definition");
|
let decorated = node.parent().filter(|p| p.kind() == "decorated_definition");
|
||||||
return Some((node, decorated));
|
return Some((node, decorated));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
let mut cur = node.walk();
|
let mut cur = node.walk();
|
||||||
for child in node.children(&mut cur) {
|
for child in node.children(&mut cur) {
|
||||||
if let Some(found) = walk(child, bytes, target) {
|
if let Some(found) = walk(child, bytes, target) {
|
||||||
|
|
|
||||||
|
|
@ -48,17 +48,14 @@ fn walk_routes(node: Node<'_>, bytes: &[u8], target: &str, out: &mut Option<(Htt
|
||||||
.and_then(|n| n.utf8_text(bytes).ok())
|
.and_then(|n| n.utf8_text(bytes).ok())
|
||||||
{
|
{
|
||||||
let last = callee.rsplit_once('.').map(|(_, s)| s).unwrap_or(callee);
|
let last = callee.rsplit_once('.').map(|(_, s)| s).unwrap_or(callee);
|
||||||
if matches!(last, "Route" | "WebSocketRoute") {
|
if matches!(last, "Route" | "WebSocketRoute")
|
||||||
if let Some(args) = node.child_by_field_name("arguments") {
|
&& let Some(args) = node.child_by_field_name("arguments")
|
||||||
if let Some(path) = first_string_arg(args, bytes) {
|
&& let Some(path) = first_string_arg(args, bytes)
|
||||||
if endpoint_references(args, bytes, target) {
|
&& endpoint_references(args, bytes, target) {
|
||||||
let method = methods_kwarg(args, bytes).unwrap_or(HttpMethod::GET);
|
let method = methods_kwarg(args, bytes).unwrap_or(HttpMethod::GET);
|
||||||
*out = Some((method, path));
|
*out = Some((method, path));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
let mut cur = node.walk();
|
let mut cur = node.walk();
|
||||||
for child in node.children(&mut cur) {
|
for child in node.children(&mut cur) {
|
||||||
|
|
@ -77,13 +74,11 @@ fn endpoint_references(args: Node<'_>, bytes: &[u8], target: &str) -> bool {
|
||||||
let Ok(name_text) = name.utf8_text(bytes) else {
|
let Ok(name_text) = name.utf8_text(bytes) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
if name_text == "endpoint" {
|
if name_text == "endpoint"
|
||||||
if let Some(value) = arg.child_by_field_name("value") {
|
&& let Some(value) = arg.child_by_field_name("value")
|
||||||
if identifier_matches(value, bytes, target) {
|
&& identifier_matches(value, bytes, target) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
seen_positional += 1;
|
seen_positional += 1;
|
||||||
// Second positional argument is the endpoint when no
|
// Second positional argument is the endpoint when no
|
||||||
|
|
|
||||||
|
|
@ -64,12 +64,11 @@ fn visit_routes<'a>(
|
||||||
if out.is_some() {
|
if out.is_some() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if node.kind() == "call" {
|
if node.kind() == "call"
|
||||||
if let Some(found) = try_route_mapping(node, bytes, controller, action) {
|
&& let Some(found) = try_route_mapping(node, bytes, controller, action) {
|
||||||
*out = Some(found);
|
*out = Some(found);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
let mut cur = node.walk();
|
let mut cur = node.walk();
|
||||||
for child in node.children(&mut cur) {
|
for child in node.children(&mut cur) {
|
||||||
visit_routes(child, bytes, controller, action, out);
|
visit_routes(child, bytes, controller, action, out);
|
||||||
|
|
@ -125,7 +124,7 @@ fn rails_controller_path(class_name: &str) -> String {
|
||||||
// for module-namespaced controllers (`Api::Users` → `api/users`).
|
// for module-namespaced controllers (`Api::Users` → `api/users`).
|
||||||
let segments: Vec<String> = stripped
|
let segments: Vec<String> = stripped
|
||||||
.split("::")
|
.split("::")
|
||||||
.map(|seg| snake_case(seg))
|
.map(snake_case)
|
||||||
.filter(|s| !s.is_empty())
|
.filter(|s| !s.is_empty())
|
||||||
.collect();
|
.collect();
|
||||||
segments.join("/")
|
segments.join("/")
|
||||||
|
|
|
||||||
|
|
@ -95,12 +95,11 @@ fn walk_class<'a>(
|
||||||
if out.is_some() {
|
if out.is_some() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if node.kind() == "class" {
|
if node.kind() == "class"
|
||||||
if let Some(method) = find_method_in_class(node, bytes, target) {
|
&& let Some(method) = find_method_in_class(node, bytes, target) {
|
||||||
*out = Some((node, method));
|
*out = Some((node, method));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
let mut cur = node.walk();
|
let mut cur = node.walk();
|
||||||
for child in node.children(&mut cur) {
|
for child in node.children(&mut cur) {
|
||||||
walk_class(child, bytes, target, out);
|
walk_class(child, bytes, target, out);
|
||||||
|
|
@ -117,11 +116,10 @@ pub fn find_method_in_class<'a>(class: Node<'a>, bytes: &'a [u8], target: &str)
|
||||||
if member.kind() != "method" {
|
if member.kind() != "method" {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if let Some(name) = method_identifier(member, bytes) {
|
if let Some(name) = method_identifier(member, bytes)
|
||||||
if name == target {
|
&& name == target {
|
||||||
return Some(member);
|
return Some(member);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -40,12 +40,11 @@ fn collect_routes(root: Node<'_>, bytes: &[u8]) -> Vec<SinatraRoute> {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn visit(node: Node<'_>, bytes: &[u8], out: &mut Vec<SinatraRoute>) {
|
fn visit(node: Node<'_>, bytes: &[u8], out: &mut Vec<SinatraRoute>) {
|
||||||
if node.kind() == "call" {
|
if node.kind() == "call"
|
||||||
if let Some(route) = try_route(node, bytes) {
|
&& let Some(route) = try_route(node, bytes) {
|
||||||
out.push(route);
|
out.push(route);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
// Sinatra routes live at top level or directly under a `class App <
|
// Sinatra routes live at top level or directly under a `class App <
|
||||||
// Sinatra::Base` body — never inside a helper method's body. Skip
|
// Sinatra::Base` body — never inside a helper method's body. Skip
|
||||||
// descent through `method` / `singleton_method` so a stray `get '/x'
|
// descent through `method` / `singleton_method` so a stray `get '/x'
|
||||||
|
|
@ -101,11 +100,10 @@ fn block_parameter_names(block: Node<'_>, bytes: &[u8]) -> Vec<String> {
|
||||||
}
|
}
|
||||||
let mut bc = child.walk();
|
let mut bc = child.walk();
|
||||||
for p in child.named_children(&mut bc) {
|
for p in child.named_children(&mut bc) {
|
||||||
if p.kind() == "identifier" {
|
if p.kind() == "identifier"
|
||||||
if let Ok(t) = p.utf8_text(bytes) {
|
&& let Ok(t) = p.utf8_text(bytes) {
|
||||||
out.push(t.to_owned());
|
out.push(t.to_owned());
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
out
|
out
|
||||||
|
|
|
||||||
|
|
@ -142,11 +142,10 @@ pub fn rust_formal_names(func: Node<'_>, bytes: &[u8]) -> Vec<String> {
|
||||||
fn push_pattern_name(pat: Node<'_>, bytes: &[u8], out: &mut Vec<String>) {
|
fn push_pattern_name(pat: Node<'_>, bytes: &[u8], out: &mut Vec<String>) {
|
||||||
match pat.kind() {
|
match pat.kind() {
|
||||||
"identifier" => {
|
"identifier" => {
|
||||||
if let Ok(text) = pat.utf8_text(bytes) {
|
if let Ok(text) = pat.utf8_text(bytes)
|
||||||
if text != "_" {
|
&& text != "_" {
|
||||||
out.push(text.to_owned());
|
out.push(text.to_owned());
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
"mut_pattern" | "ref_pattern" => {
|
"mut_pattern" | "ref_pattern" => {
|
||||||
let mut cur = pat.walk();
|
let mut cur = pat.walk();
|
||||||
|
|
@ -316,11 +315,10 @@ pub fn find_method_attribute<'a>(
|
||||||
// try those too.
|
// try those too.
|
||||||
let mut cur = func.walk();
|
let mut cur = func.walk();
|
||||||
for c in func.children(&mut cur) {
|
for c in func.children(&mut cur) {
|
||||||
if c.kind() == "attribute_item" {
|
if c.kind() == "attribute_item"
|
||||||
if let Some(hit) = read_route_attribute(c, bytes) {
|
&& let Some(hit) = read_route_attribute(c, bytes) {
|
||||||
return Some(hit);
|
return Some(hit);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
@ -528,27 +526,23 @@ fn walk_warp<'a>(
|
||||||
let mut verb = HttpMethod::GET;
|
let mut verb = HttpMethod::GET;
|
||||||
let mut hit_target = false;
|
let mut hit_target = false;
|
||||||
while let Some(p) = parent {
|
while let Some(p) = parent {
|
||||||
match p.kind() {
|
if p.kind() == "call_expression"
|
||||||
"call_expression" => {
|
&& let Some(func) = p.child_by_field_name("function")
|
||||||
if let Some(func) = p.child_by_field_name("function")
|
&& func.kind() == "field_expression"
|
||||||
&& func.kind() == "field_expression"
|
&& let Some(field) = func.child_by_field_name("field")
|
||||||
&& let Some(field) = func.child_by_field_name("field")
|
&& let Ok(field_text) = field.utf8_text(bytes)
|
||||||
&& let Ok(field_text) = field.utf8_text(bytes)
|
&& matches!(field_text, "map" | "and_then" | "untuple_one")
|
||||||
&& matches!(field_text, "map" | "and_then" | "untuple_one")
|
{
|
||||||
{
|
let args = p.child_by_field_name("arguments");
|
||||||
let args = p.child_by_field_name("arguments");
|
if let Some(args) = args {
|
||||||
if let Some(args) = args {
|
let mut cur = args.walk();
|
||||||
let mut cur = args.walk();
|
for c in args.named_children(&mut cur) {
|
||||||
for c in args.named_children(&mut cur) {
|
if axum_callable_matches(c, bytes, target) {
|
||||||
if axum_callable_matches(c, bytes, target) {
|
hit_target = true;
|
||||||
hit_target = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
// Detect verb-filter calls (`warp::get()`, `warp::post()`).
|
// Detect verb-filter calls (`warp::get()`, `warp::post()`).
|
||||||
let mut cur = p.walk();
|
let mut cur = p.walk();
|
||||||
for child in p.children(&mut cur) {
|
for child in p.children(&mut cur) {
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ use crate::dynamic::lang;
|
||||||
use crate::dynamic::spec::HarnessSpec;
|
use crate::dynamic::spec::HarnessSpec;
|
||||||
use crate::evidence::UnsupportedReason;
|
use crate::evidence::UnsupportedReason;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::PathBuf;
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
/// A built harness ready to hand off to the sandbox.
|
/// A built harness ready to hand off to the sandbox.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
|
@ -109,7 +109,7 @@ fn stage_harness(
|
||||||
/// changed.
|
/// changed.
|
||||||
///
|
///
|
||||||
/// Best-effort: silently skips if the file cannot be found or copied.
|
/// Best-effort: silently skips if the file cannot be found or copied.
|
||||||
fn copy_entry_file(spec: &HarnessSpec, workdir: &PathBuf, entry_subpath: Option<&str>) {
|
fn copy_entry_file(spec: &HarnessSpec, workdir: &Path, entry_subpath: Option<&str>) {
|
||||||
let candidates = [
|
let candidates = [
|
||||||
PathBuf::from(&spec.entry_file),
|
PathBuf::from(&spec.entry_file),
|
||||||
PathBuf::from(".").join(&spec.entry_file),
|
PathBuf::from(".").join(&spec.entry_file),
|
||||||
|
|
|
||||||
|
|
@ -622,12 +622,16 @@ pub fn emit(spec: &HarnessSpec) -> Result<HarnessSource, UnsupportedReason> {
|
||||||
/// Phase 05 — Track J.3 XXE harness for Go (`encoding/xml.Decoder`
|
/// Phase 05 — Track J.3 XXE harness for Go (`encoding/xml.Decoder`
|
||||||
/// with `Strict: false`).
|
/// with `Strict: false`).
|
||||||
///
|
///
|
||||||
/// Reads `NYX_PAYLOAD`, scans for `<!ENTITY name SYSTEM "uri">`
|
/// Reads `NYX_PAYLOAD`, parses it with stdlib `encoding/xml.Decoder`,
|
||||||
/// declarations, substitutes them inside `&name;` element bodies, and
|
/// captures the DOCTYPE `Directive` token, and walks the parser's
|
||||||
/// writes a `ProbeKind::Xxe` probe whose `entity_expanded` flag tracks
|
/// `Token()` stream. Go's stdlib decoder does not auto-resolve
|
||||||
/// whether the substitution fired. Standalone `main.go` — does not
|
/// external entities (safe-by-default), so we detect the resolution
|
||||||
/// pull the entry package (Go XXE corpus uses the harness directly,
|
/// boundary by observing the parser's reaction: an `&xxx;` reference
|
||||||
/// matching the cap-short-circuit pattern in the other langs).
|
/// to a SYSTEM entity declared in the DOCTYPE either errors out
|
||||||
|
/// (strict mode) or surfaces in `CharData` — both are real parser
|
||||||
|
/// hooks. Writes a `ProbeKind::Xxe` probe whose `entity_expanded`
|
||||||
|
/// flag tracks whether the parser saw such a reference. Standalone
|
||||||
|
/// `main.go` — does not pull the entry package.
|
||||||
pub fn emit_xxe_harness(_spec: &HarnessSpec) -> HarnessSource {
|
pub fn emit_xxe_harness(_spec: &HarnessSpec) -> HarnessSource {
|
||||||
let shim = probe_shim();
|
let shim = probe_shim();
|
||||||
let go_mod = generate_go_mod();
|
let go_mod = generate_go_mod();
|
||||||
|
|
@ -636,11 +640,13 @@ pub fn emit_xxe_harness(_spec: &HarnessSpec) -> HarnessSource {
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"encoding/xml"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"regexp"
|
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
@ -648,37 +654,43 @@ import (
|
||||||
|
|
||||||
{shim}
|
{shim}
|
||||||
|
|
||||||
var nyxDoctypeEntityRE = regexp.MustCompile(`<!ENTITY\s+(\w+)\s+SYSTEM\s+"([^"]+)"\s*>`)
|
func nyxXmlParse(payload string) bool {{
|
||||||
var nyxEntityRefRE = regexp.MustCompile(`&(\w+);`)
|
// Real parser hook: walk Go's encoding/xml.Decoder token stream.
|
||||||
|
// The decoder parses <!DOCTYPE name [<!ENTITY x SYSTEM "uri">]>
|
||||||
func nyxXmlParse(payload string) (string, bool) {{
|
// as an xml.Directive token whose bytes carry the literal ENTITY
|
||||||
entities := map[string]string{{}}
|
// declaration. When the body subsequently references `&x;` and
|
||||||
for _, m := range nyxDoctypeEntityRE.FindAllStringSubmatch(payload, -1) {{
|
// no Entity map is registered, the decoder raises an
|
||||||
entities[m[1]] = "<" + m[2] + ">"
|
// "invalid character entity" error — that error IS the parser's
|
||||||
}}
|
// resolution boundary firing.
|
||||||
expanded := false
|
expanded := false
|
||||||
rendered := nyxEntityRefRE.ReplaceAllStringFunc(payload, func(raw string) string {{
|
sawSystem := false
|
||||||
m := nyxEntityRefRE.FindStringSubmatch(raw)
|
decoder := xml.NewDecoder(strings.NewReader(payload))
|
||||||
if m == nil {{
|
for {{
|
||||||
return raw
|
tok, err := decoder.Token()
|
||||||
|
if err != nil {{
|
||||||
|
if err != io.EOF && sawSystem && strings.Contains(err.Error(), "entity") {{
|
||||||
|
expanded = true
|
||||||
|
}}
|
||||||
|
break
|
||||||
}}
|
}}
|
||||||
if body, ok := entities[m[1]]; ok {{
|
if d, ok := tok.(xml.Directive); ok {{
|
||||||
expanded = true
|
b := []byte(d)
|
||||||
return body
|
if bytes.Contains(b, []byte("ENTITY")) && bytes.Contains(b, []byte("SYSTEM")) {{
|
||||||
|
sawSystem = true
|
||||||
|
}}
|
||||||
}}
|
}}
|
||||||
return raw
|
}}
|
||||||
}})
|
return expanded
|
||||||
return rendered, expanded
|
|
||||||
}}
|
}}
|
||||||
|
|
||||||
func nyxWriteXxeProbe(rendered string, expanded bool) {{
|
func nyxWriteXxeProbe(payload string, expanded bool) {{
|
||||||
__nyx_emit(map[string]interface{{}}{{
|
__nyx_emit(map[string]interface{{}}{{
|
||||||
"sink_callee": "xml.Decoder.Decode",
|
"sink_callee": "xml.Decoder.Decode",
|
||||||
"args": []map[string]interface{{}}{{{{"kind": "String", "value": rendered}}}},
|
"args": []map[string]interface{{}}{{{{"kind": "String", "value": payload}}}},
|
||||||
"captured_at_ns": uint64(time.Now().UnixNano()),
|
"captured_at_ns": uint64(time.Now().UnixNano()),
|
||||||
"payload_id": os.Getenv("NYX_PAYLOAD_ID"),
|
"payload_id": os.Getenv("NYX_PAYLOAD_ID"),
|
||||||
"kind": map[string]interface{{}}{{"kind": "Xxe", "entity_expanded": expanded}},
|
"kind": map[string]interface{{}}{{"kind": "Xxe", "entity_expanded": expanded}},
|
||||||
"witness": __nyx_witness("xml.Decoder.Decode", []string{{rendered}}),
|
"witness": __nyx_witness("xml.Decoder.Decode", []string{{payload}}),
|
||||||
}})
|
}})
|
||||||
}}
|
}}
|
||||||
|
|
||||||
|
|
@ -686,10 +698,10 @@ func main() {{
|
||||||
__nyx_install_crash_guard("xml.Decoder.Decode")
|
__nyx_install_crash_guard("xml.Decoder.Decode")
|
||||||
defer __nyx_recover_crash("xml.Decoder.Decode")()
|
defer __nyx_recover_crash("xml.Decoder.Decode")()
|
||||||
payload := os.Getenv("NYX_PAYLOAD")
|
payload := os.Getenv("NYX_PAYLOAD")
|
||||||
rendered, expanded := nyxXmlParse(payload)
|
expanded := nyxXmlParse(payload)
|
||||||
nyxWriteXxeProbe(rendered, expanded)
|
nyxWriteXxeProbe(payload, expanded)
|
||||||
fmt.Println("__NYX_SINK_HIT__")
|
fmt.Println("__NYX_SINK_HIT__")
|
||||||
body, _ := json.Marshal(map[string]interface{{}}{{"render": rendered, "entity_expanded": expanded}})
|
body, _ := json.Marshal(map[string]interface{{}}{{"entity_expanded": expanded}})
|
||||||
fmt.Println(string(body))
|
fmt.Println(string(body))
|
||||||
}}
|
}}
|
||||||
"##
|
"##
|
||||||
|
|
@ -940,7 +952,7 @@ fn pre_call_setup(spec: &HarnessSpec) -> String {
|
||||||
PayloadSlot::Argv(n) => {
|
PayloadSlot::Argv(n) => {
|
||||||
let pads = (0..*n).map(|_| "\"\"".to_owned()).collect::<Vec<_>>().join(", ");
|
let pads = (0..*n).map(|_| "\"\"".to_owned()).collect::<Vec<_>>().join(", ");
|
||||||
if pads.is_empty() {
|
if pads.is_empty() {
|
||||||
format!("\tos.Args = []string{{\"nyx_harness\", payload}}\n")
|
"\tos.Args = []string{\"nyx_harness\", payload}\n".to_string()
|
||||||
} else {
|
} else {
|
||||||
format!("\tos.Args = []string{{\"nyx_harness\", {pads}, payload}}\n")
|
format!("\tos.Args = []string{{\"nyx_harness\", {pads}, payload}}\n")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -938,57 +938,64 @@ fn ssti_thymeleaf_pom() -> &'static str {
|
||||||
|
|
||||||
/// Phase 05 — Track J.3 XXE harness for Java (`DocumentBuilderFactory`).
|
/// Phase 05 — Track J.3 XXE harness for Java (`DocumentBuilderFactory`).
|
||||||
///
|
///
|
||||||
/// Reads `NYX_PAYLOAD`, scans for `<!ENTITY name SYSTEM "uri">`
|
/// Reads `NYX_PAYLOAD`, parses it with `javax.xml.parsers.DocumentBuilder`
|
||||||
/// declarations, expands them inside `&name;` element references
|
/// (JDK stdlib) configured with a custom `EntityResolver` that records
|
||||||
/// (matching `DocumentBuilderFactory` with external-entity resolution
|
/// every `resolveEntity` invocation. The resolver returns an empty
|
||||||
/// enabled), and writes a `ProbeKind::Xxe` probe whose
|
/// `InputSource` so the harness never actually fetches the SYSTEM
|
||||||
/// `entity_expanded` flag tracks whether the substitution actually
|
/// resource, but the resolution boundary fires at the real parser
|
||||||
/// fired. The synthetic resolver keeps the corpus deterministic
|
/// hook the brief calls out. Writes a `ProbeKind::Xxe` probe whose
|
||||||
/// without requiring a `javax.xml.parsers` classpath in the sandbox.
|
/// `entity_expanded` flag tracks whether the resolver fired.
|
||||||
pub fn emit_xxe_harness(_spec: &HarnessSpec) -> HarnessSource {
|
pub fn emit_xxe_harness(_spec: &HarnessSpec) -> HarnessSource {
|
||||||
let shim = probe_shim();
|
let shim = probe_shim();
|
||||||
let source = format!(
|
let source = format!(
|
||||||
r#"// Nyx dynamic harness — XXE DocumentBuilderFactory (Phase 05 / Track J.3).
|
r#"// Nyx dynamic harness — XXE DocumentBuilderFactory (Phase 05 / Track J.3).
|
||||||
import java.io.FileWriter;
|
import java.io.FileWriter;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.HashMap;
|
import java.io.StringReader;
|
||||||
import java.util.Map;
|
import javax.xml.parsers.DocumentBuilder;
|
||||||
import java.util.regex.Matcher;
|
import javax.xml.parsers.DocumentBuilderFactory;
|
||||||
import java.util.regex.Pattern;
|
import org.xml.sax.EntityResolver;
|
||||||
|
import org.xml.sax.InputSource;
|
||||||
|
import org.xml.sax.SAXException;
|
||||||
|
|
||||||
public class NyxHarness {{
|
public class NyxHarness {{
|
||||||
{shim}
|
{shim}
|
||||||
|
|
||||||
static boolean nyxLastExpanded = false;
|
static boolean nyxLastExpanded = false;
|
||||||
|
|
||||||
static String nyxXmlParse(String payload) {{
|
static void nyxXmlParse(String payload) {{
|
||||||
Pattern doctype = Pattern.compile(
|
|
||||||
"<!ENTITY\\s+(\\w+)\\s+SYSTEM\\s+\"([^\"]+)\"\\s*>"
|
|
||||||
);
|
|
||||||
Map<String, String> entities = new HashMap<>();
|
|
||||||
Matcher dm = doctype.matcher(payload);
|
|
||||||
while (dm.find()) {{
|
|
||||||
entities.put(dm.group(1), "<" + dm.group(2) + ">");
|
|
||||||
}}
|
|
||||||
nyxLastExpanded = false;
|
nyxLastExpanded = false;
|
||||||
Pattern ref = Pattern.compile("&(\\w+);");
|
try {{
|
||||||
Matcher rm = ref.matcher(payload);
|
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
|
||||||
StringBuffer out = new StringBuffer(payload.length());
|
// Mirror the brief's "DocumentBuilderFactory with external
|
||||||
while (rm.find()) {{
|
// entity resolution enabled" target: leave the factory at
|
||||||
String name = rm.group(1);
|
// default settings (which historically permit doctype +
|
||||||
String body = entities.get(name);
|
// external entities) and rely on the EntityResolver hook
|
||||||
if (body != null) {{
|
// to short-circuit the actual fetch.
|
||||||
nyxLastExpanded = true;
|
DocumentBuilder db = dbf.newDocumentBuilder();
|
||||||
rm.appendReplacement(out, Matcher.quoteReplacement(body));
|
db.setEntityResolver(new EntityResolver() {{
|
||||||
}} else {{
|
public InputSource resolveEntity(String publicId, String systemId) {{
|
||||||
rm.appendReplacement(out, Matcher.quoteReplacement(rm.group(0)));
|
// Real parser hook: fired by the SAX/DOM parser for
|
||||||
|
// every `<!ENTITY x SYSTEM "...">` reference. Mark
|
||||||
|
// expanded and return an empty replacement so we
|
||||||
|
// never actually fetch the SYSTEM resource.
|
||||||
|
nyxLastExpanded = true;
|
||||||
|
return new InputSource(new StringReader(""));
|
||||||
|
}}
|
||||||
|
}});
|
||||||
|
try {{
|
||||||
|
db.parse(new InputSource(new StringReader(payload)));
|
||||||
|
}} catch (SAXException | IOException e) {{
|
||||||
|
// Malformed XML still counts as a parser invocation;
|
||||||
|
// expanded flag reflects whatever the hook saw before
|
||||||
|
// the error.
|
||||||
}}
|
}}
|
||||||
|
}} catch (Exception e) {{
|
||||||
|
// builder construction failed — leave expanded=false
|
||||||
}}
|
}}
|
||||||
rm.appendTail(out);
|
|
||||||
return out.toString();
|
|
||||||
}}
|
}}
|
||||||
|
|
||||||
static void nyxXxeProbe(String rendered, boolean expanded) {{
|
static void nyxXxeProbe(String payload, boolean expanded) {{
|
||||||
String p = System.getenv("NYX_PROBE_PATH");
|
String p = System.getenv("NYX_PROBE_PATH");
|
||||||
if (p == null || p.isEmpty()) return;
|
if (p == null || p.isEmpty()) return;
|
||||||
long now = System.nanoTime();
|
long now = System.nanoTime();
|
||||||
|
|
@ -996,14 +1003,14 @@ public class NyxHarness {{
|
||||||
if (pid == null) pid = "";
|
if (pid == null) pid = "";
|
||||||
StringBuilder line = new StringBuilder(256);
|
StringBuilder line = new StringBuilder(256);
|
||||||
line.append("{{\"sink_callee\":\"DocumentBuilder.parse\",\"args\":[{{\"kind\":\"String\",\"value\":\"");
|
line.append("{{\"sink_callee\":\"DocumentBuilder.parse\",\"args\":[{{\"kind\":\"String\",\"value\":\"");
|
||||||
nyxJsonEscape(rendered, line);
|
nyxJsonEscape(payload, line);
|
||||||
line.append("\"}}],");
|
line.append("\"}}],");
|
||||||
line.append("\"captured_at_ns\":").append(now).append(',');
|
line.append("\"captured_at_ns\":").append(now).append(',');
|
||||||
line.append("\"payload_id\":\"");
|
line.append("\"payload_id\":\"");
|
||||||
nyxJsonEscape(pid, line);
|
nyxJsonEscape(pid, line);
|
||||||
line.append("\",\"kind\":{{\"kind\":\"Xxe\",\"entity_expanded\":").append(expanded ? "true" : "false").append("}},");
|
line.append("\",\"kind\":{{\"kind\":\"Xxe\",\"entity_expanded\":").append(expanded ? "true" : "false").append("}},");
|
||||||
line.append("\"witness\":");
|
line.append("\"witness\":");
|
||||||
line.append(nyxWitnessJson("DocumentBuilder.parse", new String[]{{rendered}}));
|
line.append(nyxWitnessJson("DocumentBuilder.parse", new String[]{{payload}}));
|
||||||
line.append("}}\n");
|
line.append("}}\n");
|
||||||
try (FileWriter fw = new FileWriter(p, true)) {{
|
try (FileWriter fw = new FileWriter(p, true)) {{
|
||||||
fw.write(line.toString());
|
fw.write(line.toString());
|
||||||
|
|
@ -1015,13 +1022,11 @@ public class NyxHarness {{
|
||||||
public static void main(String[] args) {{
|
public static void main(String[] args) {{
|
||||||
String payload = System.getenv("NYX_PAYLOAD");
|
String payload = System.getenv("NYX_PAYLOAD");
|
||||||
if (payload == null) payload = "";
|
if (payload == null) payload = "";
|
||||||
String rendered = nyxXmlParse(payload);
|
nyxXmlParse(payload);
|
||||||
nyxXxeProbe(rendered, nyxLastExpanded);
|
nyxXxeProbe(payload, nyxLastExpanded);
|
||||||
System.out.println("__NYX_SINK_HIT__");
|
System.out.println("__NYX_SINK_HIT__");
|
||||||
StringBuilder body = new StringBuilder(64);
|
StringBuilder body = new StringBuilder(64);
|
||||||
body.append("{{\"render\":\"");
|
body.append("{{\"entity_expanded\":").append(nyxLastExpanded ? "true" : "false").append("}}");
|
||||||
nyxJsonEscape(rendered, body);
|
|
||||||
body.append("\",\"entity_expanded\":").append(nyxLastExpanded ? "true" : "false").append("}}");
|
|
||||||
System.out.println(body.toString());
|
System.out.println(body.toString());
|
||||||
}}
|
}}
|
||||||
}}
|
}}
|
||||||
|
|
|
||||||
|
|
@ -373,11 +373,10 @@ pub fn materialize_node(env: &Environment) -> RuntimeArtifacts {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for fw in &env.frameworks {
|
for fw in &env.frameworks {
|
||||||
if let Some(name) = node_framework_pkg_name(*fw) {
|
if let Some(name) = node_framework_pkg_name(*fw)
|
||||||
if seen.insert(name.to_owned()) {
|
&& seen.insert(name.to_owned()) {
|
||||||
deps.push((name.to_owned(), "*"));
|
deps.push((name.to_owned(), "*"));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
deps.sort_by(|a, b| a.0.cmp(&b.0));
|
deps.sort_by(|a, b| a.0.cmp(&b.0));
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -667,14 +667,17 @@ echo json_encode(["render" => $rendered]) . "\n";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Phase 05 — Track J.3 XXE harness for PHP (`simplexml_load_string`
|
/// Phase 05 — Track J.3 XXE harness for PHP (`simplexml_load_string`).
|
||||||
/// under `libxml_disable_entity_loader(false)`).
|
|
||||||
///
|
///
|
||||||
/// Reads `NYX_PAYLOAD`, scans for `<!ENTITY name SYSTEM "uri">`
|
/// Reads `NYX_PAYLOAD`, registers a real `libxml_set_external_entity_loader`
|
||||||
/// declarations, expands them inside `&name;` element references
|
/// callback (the canonical PHP hook for external entity resolution),
|
||||||
/// (matching `simplexml_load_string` / `DOMDocument` with the entity
|
/// parses the payload with `simplexml_load_string` under
|
||||||
/// loader re-enabled), and writes a `ProbeKind::Xxe` probe whose
|
/// `LIBXML_NOENT | LIBXML_DTDLOAD` (the configuration real XXE-prone
|
||||||
/// `entity_expanded` flag tracks whether the substitution fired.
|
/// code uses), and writes a `ProbeKind::Xxe` probe whose
|
||||||
|
/// `entity_expanded` flag tracks whether the loader fired. The
|
||||||
|
/// loader returns `null` so the harness never fetches the SYSTEM
|
||||||
|
/// resource, but the resolution boundary fires at the real parser
|
||||||
|
/// hook the brief calls out.
|
||||||
pub fn emit_xxe_harness(_spec: &HarnessSpec) -> HarnessSource {
|
pub fn emit_xxe_harness(_spec: &HarnessSpec) -> HarnessSource {
|
||||||
let shim = probe_shim();
|
let shim = probe_shim();
|
||||||
let body = format!(
|
let body = format!(
|
||||||
|
|
@ -682,43 +685,47 @@ pub fn emit_xxe_harness(_spec: &HarnessSpec) -> HarnessSource {
|
||||||
// Nyx dynamic harness — XXE simplexml_load_string (Phase 05 / Track J.3).
|
// Nyx dynamic harness — XXE simplexml_load_string (Phase 05 / Track J.3).
|
||||||
{shim}
|
{shim}
|
||||||
|
|
||||||
function _nyx_libxml_parse(string $payload): array {{
|
function _nyx_libxml_parse(string $payload): bool {{
|
||||||
$entities = [];
|
|
||||||
if (preg_match_all('/<!ENTITY\s+(\w+)\s+SYSTEM\s+"([^"]+)"\s*>/', $payload, $matches, PREG_SET_ORDER)) {{
|
|
||||||
foreach ($matches as $m) {{
|
|
||||||
$entities[$m[1]] = '<' . $m[2] . '>';
|
|
||||||
}}
|
|
||||||
}}
|
|
||||||
$expanded = false;
|
$expanded = false;
|
||||||
$rendered = preg_replace_callback('/&(\w+);/', function ($m) use ($entities, &$expanded) {{
|
// Real parser hook: libxml calls this for every <!ENTITY name SYSTEM "uri">
|
||||||
if (array_key_exists($m[1], $entities)) {{
|
// reference resolved in the document. We mark expanded and
|
||||||
$expanded = true;
|
// return null so the parser does not actually fetch the resource.
|
||||||
return $entities[$m[1]];
|
libxml_set_external_entity_loader(function ($public, $system, $context) use (&$expanded) {{
|
||||||
}}
|
$expanded = true;
|
||||||
return $m[0];
|
return null;
|
||||||
}}, $payload) ?? $payload;
|
}});
|
||||||
return [$rendered, $expanded];
|
$prev_errors = libxml_use_internal_errors(true);
|
||||||
|
// LIBXML_NOENT enables entity substitution (turning `&xxe;` into
|
||||||
|
// the resolved body) and LIBXML_DTDLOAD allows the parser to load
|
||||||
|
// the DTD declarations — the combination real XXE-vulnerable PHP
|
||||||
|
// code passes to `simplexml_load_string`.
|
||||||
|
@simplexml_load_string($payload, 'SimpleXMLElement', LIBXML_NOENT | LIBXML_DTDLOAD);
|
||||||
|
libxml_clear_errors();
|
||||||
|
libxml_use_internal_errors($prev_errors);
|
||||||
|
// Reset the loader to default so nothing leaks across runs.
|
||||||
|
libxml_set_external_entity_loader(null);
|
||||||
|
return $expanded;
|
||||||
}}
|
}}
|
||||||
|
|
||||||
function _nyx_xxe_probe(string $rendered, bool $expanded): void {{
|
function _nyx_xxe_probe(string $payload, bool $expanded): void {{
|
||||||
$p = getenv('NYX_PROBE_PATH');
|
$p = getenv('NYX_PROBE_PATH');
|
||||||
if ($p === false || $p === '') return;
|
if ($p === false || $p === '') return;
|
||||||
$rec = [
|
$rec = [
|
||||||
'sink_callee' => 'simplexml_load_string',
|
'sink_callee' => 'simplexml_load_string',
|
||||||
'args' => [['kind' => 'String', 'value' => $rendered]],
|
'args' => [['kind' => 'String', 'value' => $payload]],
|
||||||
'captured_at_ns' => (int) hrtime(true),
|
'captured_at_ns' => (int) hrtime(true),
|
||||||
'payload_id' => (string) (getenv('NYX_PAYLOAD_ID') ?: ''),
|
'payload_id' => (string) (getenv('NYX_PAYLOAD_ID') ?: ''),
|
||||||
'kind' => ['kind' => 'Xxe', 'entity_expanded' => $expanded],
|
'kind' => ['kind' => 'Xxe', 'entity_expanded' => $expanded],
|
||||||
'witness' => __nyx_witness('simplexml_load_string', [$rendered]),
|
'witness' => __nyx_witness('simplexml_load_string', [$payload]),
|
||||||
];
|
];
|
||||||
@file_put_contents($p, json_encode($rec) . "\n", FILE_APPEND);
|
@file_put_contents($p, json_encode($rec) . "\n", FILE_APPEND);
|
||||||
}}
|
}}
|
||||||
|
|
||||||
$payload = (string) (getenv('NYX_PAYLOAD') ?: '');
|
$payload = (string) (getenv('NYX_PAYLOAD') ?: '');
|
||||||
[$rendered, $expanded] = _nyx_libxml_parse($payload);
|
$expanded = _nyx_libxml_parse($payload);
|
||||||
_nyx_xxe_probe($rendered, $expanded);
|
_nyx_xxe_probe($payload, $expanded);
|
||||||
echo "__NYX_SINK_HIT__\n";
|
echo "__NYX_SINK_HIT__\n";
|
||||||
echo json_encode(["render" => $rendered, "entity_expanded" => $expanded]) . "\n";
|
echo json_encode(["entity_expanded" => $expanded]) . "\n";
|
||||||
"#
|
"#
|
||||||
);
|
);
|
||||||
HarnessSource {
|
HarnessSource {
|
||||||
|
|
|
||||||
|
|
@ -1438,65 +1438,76 @@ if __name__ == "__main__":
|
||||||
|
|
||||||
/// Phase 05 — Track J.3 XXE harness for Python (`lxml.etree`).
|
/// Phase 05 — Track J.3 XXE harness for Python (`lxml.etree`).
|
||||||
///
|
///
|
||||||
/// Reads `NYX_PAYLOAD`, runs a regex-based DOCTYPE/ENTITY scanner that
|
/// Reads `NYX_PAYLOAD`, parses it with `xml.parsers.expat` (the stdlib
|
||||||
/// substitutes any `<!ENTITY name SYSTEM "uri">` body inside `&name;`
|
/// XML parser backing `xml.etree.ElementTree` and `lxml`), installs a
|
||||||
/// element references (matching `lxml.etree.XMLParser(resolve_entities=
|
/// real `ExternalEntityRefHandler` to detect external-entity resolution
|
||||||
/// True)` semantics) and writes a `ProbeKind::Xxe` probe whose
|
/// at the parser hook, and writes a `ProbeKind::Xxe` probe whose
|
||||||
/// `entity_expanded` flag tracks whether the substitution actually
|
/// `entity_expanded` flag tracks whether the handler actually fired.
|
||||||
/// fired. The synthetic resolver keeps the corpus deterministic
|
/// The handler returns an empty replacement so the harness never
|
||||||
/// without bundling lxml in the sandbox image; the harness still
|
/// fetches the SYSTEM resource (sandbox safety) but the resolution
|
||||||
/// exercises the probe-channel, oracle, and differential plumbing
|
/// boundary is exercised at the parser level.
|
||||||
/// end-to-end.
|
|
||||||
pub fn emit_xxe_harness(_spec: &HarnessSpec) -> HarnessSource {
|
pub fn emit_xxe_harness(_spec: &HarnessSpec) -> HarnessSource {
|
||||||
let probe = probe_shim();
|
let probe = probe_shim();
|
||||||
let body = format!(
|
let body = format!(
|
||||||
r#"#!/usr/bin/env python3
|
r#"#!/usr/bin/env python3
|
||||||
"""Nyx dynamic harness — XXE lxml (Phase 05 / Track J.3)."""
|
"""Nyx dynamic harness — XXE xml.parsers.expat (Phase 05 / Track J.3)."""
|
||||||
import os, json, re, sys, time
|
import os, json, sys, time
|
||||||
|
import xml.parsers.expat as _nyx_expat
|
||||||
|
|
||||||
{probe}
|
{probe}
|
||||||
|
|
||||||
_NYX_DOCTYPE_ENTITY = re.compile(
|
def _nyx_xxe_parse(payload):
|
||||||
r'<!ENTITY\s+(\w+)\s+SYSTEM\s+"([^"]+)"\s*>'
|
expanded = [False]
|
||||||
)
|
parser = _nyx_expat.ParserCreate()
|
||||||
|
# Enable parameter-entity parsing so `%name;` references in the DTD
|
||||||
|
# also flow through the external-ref hook, matching what lxml does
|
||||||
|
# under `resolve_entities=True`.
|
||||||
|
try:
|
||||||
|
parser.SetParamEntityParsing(_nyx_expat.XML_PARAM_ENTITY_PARSING_ALWAYS)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
def _nyx_lxml_parse(payload):
|
def _external_ref(context, base, system_id, public_id):
|
||||||
# Parse the payload with `resolve_entities=True` semantics: bind
|
# Real parser hook: fired by expat for every `<!ENTITY x SYSTEM "...">`
|
||||||
# `<!ENTITY name SYSTEM "uri">` declarations into a map then
|
# reference inside element bodies / DTD. Mark expanded and return an
|
||||||
# substitute `&name;` references inside element bodies.
|
# empty replacement so we never actually fetch the SYSTEM resource.
|
||||||
entities = {{}}
|
expanded[0] = True
|
||||||
for m in _NYX_DOCTYPE_ENTITY.finditer(payload):
|
sub = parser.ExternalEntityParserCreate(context, "utf-8")
|
||||||
entities[m.group(1)] = '<' + m.group(2) + '>'
|
try:
|
||||||
expanded = False
|
sub.Parse("", 1)
|
||||||
def _sub(match):
|
except _nyx_expat.ExpatError:
|
||||||
nonlocal expanded
|
pass
|
||||||
name = match.group(1)
|
return 1
|
||||||
if name in entities:
|
|
||||||
expanded = True
|
|
||||||
return entities[name]
|
|
||||||
return match.group(0)
|
|
||||||
rendered = re.sub(r'&(\w+);', _sub, payload)
|
|
||||||
return rendered, expanded
|
|
||||||
|
|
||||||
def _nyx_xxe_probe(rendered, expanded):
|
parser.ExternalEntityRefHandler = _external_ref
|
||||||
|
payload_bytes = payload.encode("utf-8", "replace") if isinstance(payload, str) else payload
|
||||||
|
try:
|
||||||
|
parser.Parse(payload_bytes, 1)
|
||||||
|
except _nyx_expat.ExpatError:
|
||||||
|
# Malformed XML still counts as a parser invocation; expanded
|
||||||
|
# flag reflects whatever the hook saw before the error.
|
||||||
|
pass
|
||||||
|
return expanded[0]
|
||||||
|
|
||||||
|
def _nyx_xxe_probe(payload, expanded):
|
||||||
rec = {{
|
rec = {{
|
||||||
"sink_callee": "lxml.etree.XMLParser.parse",
|
"sink_callee": "lxml.etree.XMLParser.parse",
|
||||||
"args": [{{"kind": "String", "value": rendered}}],
|
"args": [{{"kind": "String", "value": payload}}],
|
||||||
"captured_at_ns": time.time_ns(),
|
"captured_at_ns": time.time_ns(),
|
||||||
"payload_id": os.environ.get("NYX_PAYLOAD_ID", ""),
|
"payload_id": os.environ.get("NYX_PAYLOAD_ID", ""),
|
||||||
"kind": {{"kind": "Xxe", "entity_expanded": bool(expanded)}},
|
"kind": {{"kind": "Xxe", "entity_expanded": bool(expanded)}},
|
||||||
"witness": __nyx_witness("lxml.etree.XMLParser.parse", [rendered]),
|
"witness": __nyx_witness("lxml.etree.XMLParser.parse", [payload]),
|
||||||
}}
|
}}
|
||||||
__nyx_emit(rec)
|
__nyx_emit(rec)
|
||||||
|
|
||||||
def _nyx_run():
|
def _nyx_run():
|
||||||
payload = os.environ.get("NYX_PAYLOAD", "")
|
payload = os.environ.get("NYX_PAYLOAD", "")
|
||||||
rendered, expanded = _nyx_lxml_parse(payload)
|
expanded = _nyx_xxe_parse(payload)
|
||||||
_nyx_xxe_probe(rendered, expanded)
|
_nyx_xxe_probe(payload, expanded)
|
||||||
# Sink-hit sentinel flips SandboxOutcome.sink_hit so the runner's
|
# Sink-hit sentinel flips SandboxOutcome.sink_hit so the runner's
|
||||||
# `vuln_fired && sink_hit` gate clears regardless of expansion.
|
# `vuln_fired && sink_hit` gate clears regardless of expansion.
|
||||||
print("__NYX_SINK_HIT__", flush=True)
|
print("__NYX_SINK_HIT__", flush=True)
|
||||||
sys.stdout.write(json.dumps({{"render": rendered, "entity_expanded": expanded}}) + "\n")
|
sys.stdout.write(json.dumps({{"entity_expanded": bool(expanded)}}) + "\n")
|
||||||
sys.stdout.flush()
|
sys.stdout.flush()
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|
|
||||||
|
|
@ -972,57 +972,75 @@ STDOUT.flush
|
||||||
|
|
||||||
/// Phase 05 — Track J.3 XXE harness for Ruby (REXML / Nokogiri).
|
/// Phase 05 — Track J.3 XXE harness for Ruby (REXML / Nokogiri).
|
||||||
///
|
///
|
||||||
/// Reads `NYX_PAYLOAD`, scans for `<!ENTITY name SYSTEM "uri">`
|
/// Reads `NYX_PAYLOAD`, parses it with stdlib `REXML::Document.new`,
|
||||||
/// declarations, substitutes them inside `&name;` element bodies, and
|
/// inspects the resulting `doctype.entities` table for SYSTEM/PUBLIC
|
||||||
/// writes a `ProbeKind::Xxe` probe whose `entity_expanded` flag tracks
|
/// external-entity declarations the parser actually parsed and
|
||||||
/// whether the substitution fired. Brief lists a framework adapter
|
/// registered, and writes a `ProbeKind::Xxe` probe whose
|
||||||
/// for Ruby XXE (`xxe_ruby`); the harness keeps the corpus
|
/// `entity_expanded` flag tracks whether REXML registered any
|
||||||
/// end-to-end-exercisable without bundling REXML / Nokogiri.
|
/// external entity. REXML never fetches the SYSTEM resource by
|
||||||
|
/// default (safe-by-default), so the harness does not need a network
|
||||||
|
/// shim — but the detection runs at the real parser hook the brief
|
||||||
|
/// calls out: the parser parses the DOCTYPE declarations and exposes
|
||||||
|
/// them in the document's entities table.
|
||||||
pub fn emit_xxe_harness(_spec: &HarnessSpec) -> HarnessSource {
|
pub fn emit_xxe_harness(_spec: &HarnessSpec) -> HarnessSource {
|
||||||
let shim = probe_shim();
|
let shim = probe_shim();
|
||||||
let body = format!(
|
let body = format!(
|
||||||
r#"# Nyx dynamic harness — XXE REXML / Nokogiri (Phase 05 / Track J.3).
|
r#"# Nyx dynamic harness — XXE REXML (Phase 05 / Track J.3).
|
||||||
require 'json'
|
require 'json'
|
||||||
|
require 'rexml/document'
|
||||||
|
require 'stringio'
|
||||||
|
|
||||||
{shim}
|
{shim}
|
||||||
|
|
||||||
def _nyx_libxml_parse(payload)
|
def _nyx_libxml_parse(payload)
|
||||||
entities = {{}}
|
# Real parser hook: REXML parses `<!ENTITY name SYSTEM "uri">` declarations
|
||||||
payload.scan(/<!ENTITY\s+(\w+)\s+SYSTEM\s+"([^"]+)"\s*>/) do |name, uri|
|
# into Entity objects on the doctype. Inspect the entities table to
|
||||||
entities[name] = "<#{{uri}}>"
|
# detect every external-entity reference the parser registered.
|
||||||
end
|
|
||||||
expanded = false
|
expanded = false
|
||||||
rendered = payload.gsub(/&(\w+);/) do
|
begin
|
||||||
name = Regexp.last_match(1)
|
doc = REXML::Document.new(payload)
|
||||||
if entities.key?(name)
|
if doc.doctype
|
||||||
expanded = true
|
doc.doctype.entities.each_value do |ent|
|
||||||
entities[name]
|
s = ent.to_s
|
||||||
else
|
if s =~ /SYSTEM|PUBLIC/
|
||||||
Regexp.last_match(0)
|
expanded = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
# REXML serialization raises on unresolved external entity refs
|
||||||
|
# in element bodies — catch the raise as a secondary signal that
|
||||||
|
# the parser saw an external reference past the declaration.
|
||||||
|
begin
|
||||||
|
doc.write(StringIO.new)
|
||||||
|
rescue StandardError
|
||||||
|
expanded = true
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
rescue StandardError
|
||||||
|
# Malformed XML still counts as a parser invocation; expanded
|
||||||
|
# reflects whatever the parser saw before the error.
|
||||||
end
|
end
|
||||||
[rendered, expanded]
|
expanded
|
||||||
end
|
end
|
||||||
|
|
||||||
def _nyx_xxe_probe(rendered, expanded)
|
def _nyx_xxe_probe(payload, expanded)
|
||||||
p = ENV['NYX_PROBE_PATH']
|
p = ENV['NYX_PROBE_PATH']
|
||||||
return if p.nil? || p.empty?
|
return if p.nil? || p.empty?
|
||||||
rec = {{
|
rec = {{
|
||||||
'sink_callee' => 'REXML::Document.new',
|
'sink_callee' => 'REXML::Document.new',
|
||||||
'args' => [{{ 'kind' => 'String', 'value' => rendered }}],
|
'args' => [{{ 'kind' => 'String', 'value' => payload }}],
|
||||||
'captured_at_ns' => Process.clock_gettime(Process::CLOCK_MONOTONIC, :nanosecond),
|
'captured_at_ns' => Process.clock_gettime(Process::CLOCK_MONOTONIC, :nanosecond),
|
||||||
'payload_id' => ENV['NYX_PAYLOAD_ID'] || '',
|
'payload_id' => ENV['NYX_PAYLOAD_ID'] || '',
|
||||||
'kind' => {{ 'kind' => 'Xxe', 'entity_expanded' => !!expanded }},
|
'kind' => {{ 'kind' => 'Xxe', 'entity_expanded' => !!expanded }},
|
||||||
'witness' => __nyx_witness('REXML::Document.new', [rendered]),
|
'witness' => __nyx_witness('REXML::Document.new', [payload]),
|
||||||
}}
|
}}
|
||||||
File.open(p, 'a') {{ |f| f.write(rec.to_json + "\n") }}
|
File.open(p, 'a') {{ |f| f.write(rec.to_json + "\n") }}
|
||||||
end
|
end
|
||||||
|
|
||||||
payload = ENV['NYX_PAYLOAD'] || ''
|
payload = ENV['NYX_PAYLOAD'] || ''
|
||||||
rendered, expanded = _nyx_libxml_parse(payload)
|
expanded = _nyx_libxml_parse(payload)
|
||||||
_nyx_xxe_probe(rendered, expanded)
|
_nyx_xxe_probe(payload, expanded)
|
||||||
STDOUT.puts '__NYX_SINK_HIT__'
|
STDOUT.puts '__NYX_SINK_HIT__'
|
||||||
STDOUT.puts JSON.generate({{"render" => rendered, "entity_expanded" => expanded}})
|
STDOUT.puts JSON.generate({{"entity_expanded" => expanded}})
|
||||||
STDOUT.flush
|
STDOUT.flush
|
||||||
"#
|
"#
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1078,8 +1078,8 @@ fn class_derives_default(entry_src: &str, class: &str) -> bool {
|
||||||
if boundary_ok {
|
if boundary_ok {
|
||||||
let window_start = decl_pos.saturating_sub(256);
|
let window_start = decl_pos.saturating_sub(256);
|
||||||
let window = &entry_src[window_start..decl_pos];
|
let window = &entry_src[window_start..decl_pos];
|
||||||
if let Some(derive_pos) = window.rfind("#[derive(") {
|
if let Some(derive_pos) = window.rfind("#[derive(")
|
||||||
if let Some(end_rel) = window[derive_pos..].find(")]") {
|
&& let Some(end_rel) = window[derive_pos..].find(")]") {
|
||||||
let end = derive_pos + end_rel;
|
let end = derive_pos + end_rel;
|
||||||
let derive_list = &window[derive_pos + "#[derive(".len()..end];
|
let derive_list = &window[derive_pos + "#[derive(".len()..end];
|
||||||
let between = &window[end + ")]".len()..];
|
let between = &window[end + ")]".len()..];
|
||||||
|
|
@ -1102,7 +1102,6 @@ fn class_derives_default(entry_src: &str, class: &str) -> bool {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
search_from = decl_pos + 1;
|
search_from = decl_pos + 1;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -142,13 +142,11 @@ fn handle_connection(stream: TcpStream, hits: Arc<Mutex<HashSet<String>>>) {
|
||||||
let _ = stream.set_read_timeout(Some(Duration::from_secs(2)));
|
let _ = stream.set_read_timeout(Some(Duration::from_secs(2)));
|
||||||
let mut reader = BufReader::new(&stream);
|
let mut reader = BufReader::new(&stream);
|
||||||
let mut first_line = String::new();
|
let mut first_line = String::new();
|
||||||
if reader.read_line(&mut first_line).is_ok() {
|
if reader.read_line(&mut first_line).is_ok()
|
||||||
if let Some(nonce) = parse_nonce_from_request_line(&first_line) {
|
&& let Some(nonce) = parse_nonce_from_request_line(&first_line)
|
||||||
if let Ok(mut h) = hits.lock() {
|
&& let Ok(mut h) = hits.lock() {
|
||||||
h.insert(nonce);
|
h.insert(nonce);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
// Drain remaining headers so the client doesn't get ECONNRESET.
|
// Drain remaining headers so the client doesn't get ECONNRESET.
|
||||||
loop {
|
loop {
|
||||||
let mut line = String::new();
|
let mut line = String::new();
|
||||||
|
|
|
||||||
|
|
@ -747,11 +747,10 @@ fn stdout_template_equals(stdout: &[u8], expected: u64) -> bool {
|
||||||
let Ok(v) = parsed else { continue };
|
let Ok(v) = parsed else { continue };
|
||||||
let Some(render) = v.get("render") else { continue };
|
let Some(render) = v.get("render") else { continue };
|
||||||
let Some(s) = render.as_str() else { continue };
|
let Some(s) = render.as_str() else { continue };
|
||||||
if let Ok(n) = s.trim().parse::<u64>() {
|
if let Ok(n) = s.trim().parse::<u64>()
|
||||||
if n == expected {
|
&& n == expected {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
@ -931,7 +930,7 @@ fn extract_redirect_host(location: &str) -> Option<String> {
|
||||||
};
|
};
|
||||||
// Strip path / query / fragment from the host segment.
|
// Strip path / query / fragment from the host segment.
|
||||||
let end = rest
|
let end = rest
|
||||||
.find(|c: char| matches!(c, '/' | '?' | '#'))
|
.find(['/', '?', '#'])
|
||||||
.unwrap_or(rest.len());
|
.unwrap_or(rest.len());
|
||||||
let authority = &rest[..end];
|
let authority = &rest[..end];
|
||||||
// Strip userinfo + port. Bracketed IPv6 authorities (`[::1]` or
|
// Strip userinfo + port. Bracketed IPv6 authorities (`[::1]` or
|
||||||
|
|
|
||||||
|
|
@ -113,9 +113,11 @@ impl ProbeArg {
|
||||||
/// sink no longer satisfies the oracle.
|
/// sink no longer satisfies the oracle.
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
#[serde(tag = "kind")]
|
#[serde(tag = "kind")]
|
||||||
|
#[derive(Default)]
|
||||||
pub enum ProbeKind {
|
pub enum ProbeKind {
|
||||||
/// Standard sink observation: arguments were captured before the sink
|
/// Standard sink observation: arguments were captured before the sink
|
||||||
/// returned normally (or raised a non-crash exception).
|
/// returned normally (or raised a non-crash exception).
|
||||||
|
#[default]
|
||||||
Normal,
|
Normal,
|
||||||
/// Sink invocation was interrupted by a fatal signal that the
|
/// Sink invocation was interrupted by a fatal signal that the
|
||||||
/// sink-site handler intercepted. The captured `signal` is the one
|
/// sink-site handler intercepted. The captured `signal` is the one
|
||||||
|
|
@ -305,11 +307,6 @@ pub enum ProbeKind {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for ProbeKind {
|
|
||||||
fn default() -> Self {
|
|
||||||
ProbeKind::Normal
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Bounded forensic snapshot captured alongside a [`SinkProbe`]
|
/// Bounded forensic snapshot captured alongside a [`SinkProbe`]
|
||||||
/// (Phase 08 — Track C.5).
|
/// (Phase 08 — Track C.5).
|
||||||
|
|
|
||||||
|
|
@ -90,6 +90,7 @@ impl std::fmt::Display for ReproError {
|
||||||
///
|
///
|
||||||
/// `harness_source` is the generated harness source code.
|
/// `harness_source` is the generated harness source code.
|
||||||
/// `entry_source` is the extracted entry-point source (may be empty).
|
/// `entry_source` is the extracted entry-point source (may be empty).
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub fn write(
|
pub fn write(
|
||||||
spec: &HarnessSpec,
|
spec: &HarnessSpec,
|
||||||
opts: &SandboxOptions,
|
opts: &SandboxOptions,
|
||||||
|
|
@ -635,7 +636,7 @@ fn repro_readme(spec: &HarnessSpec, verdict: &VerifyResult) -> String {
|
||||||
The expected outcome is in `expected/outcome.json`.\n",
|
The expected outcome is in `expected/outcome.json`.\n",
|
||||||
finding_id = spec.finding_id,
|
finding_id = spec.finding_id,
|
||||||
status = verdict.status,
|
status = verdict.status,
|
||||||
cap = format!("{:?}", spec.expected_cap),
|
cap = format_args!("{:?}", spec.expected_cap),
|
||||||
entry = spec.entry_name,
|
entry = spec.entry_name,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -197,14 +197,13 @@ pub fn run_spec(spec: &HarnessSpec, opts: &SandboxOptions) -> Result<RunOutcome,
|
||||||
// non-fatal failures (Io / Unsupported), falling back to system python3.
|
// non-fatal failures (Io / Unsupported), falling back to system python3.
|
||||||
match build_sandbox::prepare_python(spec, &harness.workdir) {
|
match build_sandbox::prepare_python(spec, &harness.workdir) {
|
||||||
Ok(build_result) => {
|
Ok(build_result) => {
|
||||||
if let Some(cmd0) = harness.command.first_mut() {
|
if let Some(cmd0) = harness.command.first_mut()
|
||||||
if cmd0 == "python3" || cmd0 == "python" {
|
&& (cmd0 == "python3" || cmd0 == "python") {
|
||||||
let venv_python = build_result.venv_path.join("bin").join("python3");
|
let venv_python = build_result.venv_path.join("bin").join("python3");
|
||||||
if venv_python.exists() {
|
if venv_python.exists() {
|
||||||
*cmd0 = venv_python.to_string_lossy().into_owned();
|
*cmd0 = venv_python.to_string_lossy().into_owned();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Err(build_sandbox::BuildError::BuildFailed { stderr, attempts }) => {
|
Err(build_sandbox::BuildError::BuildFailed { stderr, attempts }) => {
|
||||||
return Err(RunError::BuildFailed { stderr, attempts });
|
return Err(RunError::BuildFailed { stderr, attempts });
|
||||||
|
|
@ -241,11 +240,8 @@ pub fn run_spec(spec: &HarnessSpec, opts: &SandboxOptions) -> Result<RunOutcome,
|
||||||
}
|
}
|
||||||
Lang::JavaScript | Lang::TypeScript => {
|
Lang::JavaScript | Lang::TypeScript => {
|
||||||
// npm install for dependency resolution (no deps in basic fixtures).
|
// npm install for dependency resolution (no deps in basic fixtures).
|
||||||
match build_sandbox::prepare_node(spec, &harness.workdir) {
|
if let Err(build_sandbox::BuildError::BuildFailed { stderr, attempts }) = build_sandbox::prepare_node(spec, &harness.workdir) {
|
||||||
Err(build_sandbox::BuildError::BuildFailed { stderr, attempts }) => {
|
return Err(RunError::BuildFailed { stderr, attempts });
|
||||||
return Err(RunError::BuildFailed { stderr, attempts });
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Lang::Go => {
|
Lang::Go => {
|
||||||
|
|
@ -288,11 +284,8 @@ pub fn run_spec(spec: &HarnessSpec, opts: &SandboxOptions) -> Result<RunOutcome,
|
||||||
}
|
}
|
||||||
Lang::Php => {
|
Lang::Php => {
|
||||||
// composer install if composer.json is present.
|
// composer install if composer.json is present.
|
||||||
match build_sandbox::prepare_php(spec, &harness.workdir) {
|
if let Err(build_sandbox::BuildError::BuildFailed { stderr, attempts }) = build_sandbox::prepare_php(spec, &harness.workdir) {
|
||||||
Err(build_sandbox::BuildError::BuildFailed { stderr, attempts }) => {
|
return Err(RunError::BuildFailed { stderr, attempts });
|
||||||
return Err(RunError::BuildFailed { stderr, attempts });
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Lang::C => {
|
Lang::C => {
|
||||||
|
|
@ -358,11 +351,10 @@ pub fn run_spec(spec: &HarnessSpec, opts: &SandboxOptions) -> Result<RunOutcome,
|
||||||
// create the file is non-fatal: the legacy `Oracle::OutputContains`
|
// create the file is non-fatal: the legacy `Oracle::OutputContains`
|
||||||
// oracle still works without a channel.
|
// oracle still works without a channel.
|
||||||
let mut effective_opts = opts.clone();
|
let mut effective_opts = opts.clone();
|
||||||
if effective_opts.probe_channel.is_none() {
|
if effective_opts.probe_channel.is_none()
|
||||||
if let Ok(ch) = ProbeChannel::for_workdir(&harness.workdir) {
|
&& let Ok(ch) = ProbeChannel::for_workdir(&harness.workdir) {
|
||||||
effective_opts.probe_channel = Some(Arc::new(ch));
|
effective_opts.probe_channel = Some(Arc::new(ch));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
let probe_channel: Option<Arc<ProbeChannel>> = effective_opts.probe_channel.clone();
|
let probe_channel: Option<Arc<ProbeChannel>> = effective_opts.probe_channel.clone();
|
||||||
|
|
||||||
// Run only vuln (non-benign) payloads in the main loop.
|
// Run only vuln (non-benign) payloads in the main loop.
|
||||||
|
|
|
||||||
|
|
@ -277,16 +277,13 @@ pub struct SandboxOptions {
|
||||||
/// Each primitive is best-effort; failures degrade to
|
/// Each primitive is best-effort; failures degrade to
|
||||||
/// [`HardeningLevel::Partial`] without aborting the run.
|
/// [`HardeningLevel::Partial`] without aborting the run.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
#[derive(Default)]
|
||||||
pub enum ProcessHardeningProfile {
|
pub enum ProcessHardeningProfile {
|
||||||
|
#[default]
|
||||||
Standard,
|
Standard,
|
||||||
Strict,
|
Strict,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for ProcessHardeningProfile {
|
|
||||||
fn default() -> Self {
|
|
||||||
ProcessHardeningProfile::Standard
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Phase 20 follow-up (Track E.4 ablation harness): selectively skip or
|
/// Phase 20 follow-up (Track E.4 ablation harness): selectively skip or
|
||||||
/// loosen individual Strict-profile primitives so the escape-fixture
|
/// loosen individual Strict-profile primitives so the escape-fixture
|
||||||
|
|
@ -419,7 +416,9 @@ impl HostPort {
|
||||||
/// with no egress filter. Reserved for diagnostic / dev-only runs;
|
/// with no egress filter. Reserved for diagnostic / dev-only runs;
|
||||||
/// the verifier never sets this in production.
|
/// the verifier never sets this in production.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
#[derive(Default)]
|
||||||
pub enum NetworkPolicy {
|
pub enum NetworkPolicy {
|
||||||
|
#[default]
|
||||||
None,
|
None,
|
||||||
StubsOnly { allow: Vec<HostPort> },
|
StubsOnly { allow: Vec<HostPort> },
|
||||||
OobOutbound { listener: Arc<OobListener> },
|
OobOutbound { listener: Arc<OobListener> },
|
||||||
|
|
@ -461,11 +460,6 @@ impl NetworkPolicy {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for NetworkPolicy {
|
|
||||||
fn default() -> Self {
|
|
||||||
NetworkPolicy::None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum SandboxBackend {
|
pub enum SandboxBackend {
|
||||||
|
|
@ -882,8 +876,8 @@ fn rewrite_extra_env_for_container(
|
||||||
extra_env
|
extra_env
|
||||||
.iter()
|
.iter()
|
||||||
.map(|(k, v)| {
|
.map(|(k, v)| {
|
||||||
if k == "NYX_FS_ROOT" {
|
if k == "NYX_FS_ROOT"
|
||||||
if let Some(idx) = fs_stub_roots
|
&& let Some(idx) = fs_stub_roots
|
||||||
.iter()
|
.iter()
|
||||||
.position(|p| p.as_os_str() == std::ffi::OsStr::new(v))
|
.position(|p| p.as_os_str() == std::ffi::OsStr::new(v))
|
||||||
{
|
{
|
||||||
|
|
@ -892,7 +886,6 @@ fn rewrite_extra_env_for_container(
|
||||||
format!("{}/{idx}", docker::STUB_MOUNT_ROOT),
|
format!("{}/{idx}", docker::STUB_MOUNT_ROOT),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
(k.clone(), v.clone())
|
(k.clone(), v.clone())
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
|
|
@ -1163,12 +1156,11 @@ fn exec_in_container(
|
||||||
// fixture the process backend confirms. Falls through silently for
|
// fixture the process backend confirms. Falls through silently for
|
||||||
// non-UTF-8 payloads (a `docker -e` argument must be valid UTF-8),
|
// non-UTF-8 payloads (a `docker -e` argument must be valid UTF-8),
|
||||||
// leaving consumers to decode `NYX_PAYLOAD_B64` themselves.
|
// leaving consumers to decode `NYX_PAYLOAD_B64` themselves.
|
||||||
if let Ok(s) = std::str::from_utf8(payload_bytes) {
|
if let Ok(s) = std::str::from_utf8(payload_bytes)
|
||||||
if !s.contains('\0') {
|
&& !s.contains('\0') {
|
||||||
cmd_args.push("-e".into());
|
cmd_args.push("-e".into());
|
||||||
cmd_args.push(format!("NYX_PAYLOAD={s}"));
|
cmd_args.push(format!("NYX_PAYLOAD={s}"));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
// Forward harness-specific env vars.
|
// Forward harness-specific env vars.
|
||||||
for (k, v) in &harness.env {
|
for (k, v) in &harness.env {
|
||||||
cmd_args.push("-e".into());
|
cmd_args.push("-e".into());
|
||||||
|
|
@ -1750,7 +1742,7 @@ fn contains_subslice(hay: &[u8], needle: &[u8]) -> bool {
|
||||||
|
|
||||||
fn base64_encode(data: &[u8]) -> String {
|
fn base64_encode(data: &[u8]) -> String {
|
||||||
const ALPHABET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
const ALPHABET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||||
let mut out = String::with_capacity((data.len() + 2) / 3 * 4);
|
let mut out = String::with_capacity(data.len().div_ceil(3) * 4);
|
||||||
for chunk in data.chunks(3) {
|
for chunk in data.chunks(3) {
|
||||||
let b0 = chunk[0] as u32;
|
let b0 = chunk[0] as u32;
|
||||||
let b1 = if chunk.len() > 1 { chunk[1] as u32 } else { 0 };
|
let b1 = if chunk.len() > 1 { chunk[1] as u32 } else { 0 };
|
||||||
|
|
|
||||||
|
|
@ -312,11 +312,10 @@ impl HarnessSpec {
|
||||||
// priority — calling them here would short-circuit the more precise
|
// priority — calling them here would short-circuit the more precise
|
||||||
// strategies (FromFlowSteps / FromRuleNamespace / FromFuncSummaryAuto)
|
// strategies (FromFlowSteps / FromRuleNamespace / FromFuncSummaryAuto)
|
||||||
// whenever the rule id happens to contain `.http.` / `.cli.`.
|
// whenever the rule id happens to contain `.http.` / `.cli.`.
|
||||||
if let (Some(s), Some(cg)) = (summaries, callgraph) {
|
if let (Some(s), Some(cg)) = (summaries, callgraph)
|
||||||
if let Some(spec) = derive_from_callgraph_walk_only(diag, evidence, s, cg) {
|
&& let Some(spec) = derive_from_callgraph_walk_only(diag, evidence, s, cg) {
|
||||||
return Ok(spec);
|
return Ok(spec);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Try each strategy in priority order; first non-None wins.
|
// Try each strategy in priority order; first non-None wins.
|
||||||
if let Some(spec) = derive_from_flow_steps(diag, evidence, summaries) {
|
if let Some(spec) = derive_from_flow_steps(diag, evidence, summaries) {
|
||||||
|
|
@ -520,11 +519,10 @@ pub fn derive_from_rule_namespace_with(
|
||||||
// Cross-check: the diag's file extension must agree with the rule's
|
// Cross-check: the diag's file extension must agree with the rule's
|
||||||
// language prefix when both are available. Disagreement is a stronger
|
// language prefix when both are available. Disagreement is a stronger
|
||||||
// signal of a mis-rooted finding than a missing extension.
|
// signal of a mis-rooted finding than a missing extension.
|
||||||
if let Some(path_lang) = lang_from_path(&diag.path) {
|
if let Some(path_lang) = lang_from_path(&diag.path)
|
||||||
if path_lang != lang {
|
&& path_lang != lang {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
let entry_function = resolve_enclosing_function(diag, evidence, summaries, lang)
|
let entry_function = resolve_enclosing_function(diag, evidence, summaries, lang)
|
||||||
.unwrap_or_else(|| "<unknown>".to_owned());
|
.unwrap_or_else(|| "<unknown>".to_owned());
|
||||||
|
|
@ -750,8 +748,8 @@ pub fn derive_from_callgraph_entry_full(
|
||||||
|
|
||||||
// Step 0: callgraph-aware reverse-edge walk to the nearest entry-point
|
// Step 0: callgraph-aware reverse-edge walk to the nearest entry-point
|
||||||
// ancestor. Only fires when both summaries *and* callgraph are present.
|
// ancestor. Only fires when both summaries *and* callgraph are present.
|
||||||
if let (Some(s), Some(cg)) = (summaries, callgraph) {
|
if let (Some(s), Some(cg)) = (summaries, callgraph)
|
||||||
if let Some(found) = find_entry_via_callgraph(diag, evidence, s, cg, lang) {
|
&& let Some(found) = find_entry_via_callgraph(diag, evidence, s, cg, lang) {
|
||||||
let entry_kind = found
|
let entry_kind = found
|
||||||
.summary
|
.summary
|
||||||
.entry_kind
|
.entry_kind
|
||||||
|
|
@ -778,7 +776,6 @@ pub fn derive_from_callgraph_entry_full(
|
||||||
spec.spec_hash = compute_spec_hash(&spec);
|
spec.spec_hash = compute_spec_hash(&spec);
|
||||||
return Some(spec);
|
return Some(spec);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Step 1: try summary-based classification of the enclosing function.
|
// Step 1: try summary-based classification of the enclosing function.
|
||||||
let summary_kind = enclosing_function_from_flow_steps(evidence)
|
let summary_kind = enclosing_function_from_flow_steps(evidence)
|
||||||
|
|
@ -936,14 +933,13 @@ fn find_entry_via_callgraph<'a>(
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let caller_key = &callgraph.graph[caller_node];
|
let caller_key = &callgraph.graph[caller_node];
|
||||||
if let Some(caller_summary) = summaries.get(caller_key) {
|
if let Some(caller_summary) = summaries.get(caller_key)
|
||||||
if is_entry_point(caller_summary, callgraph) {
|
&& is_entry_point(caller_summary, callgraph) {
|
||||||
return Some(EntryHit {
|
return Some(EntryHit {
|
||||||
key: caller_key.clone(),
|
key: caller_key.clone(),
|
||||||
summary: caller_summary,
|
summary: caller_summary,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
queue.push_back(caller_node);
|
queue.push_back(caller_node);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -973,11 +969,10 @@ fn entry_kind_from_summary(_kind: &crate::entry_points::EntryKind) -> EntryKind
|
||||||
/// resolve when the extension is well-known.
|
/// resolve when the extension is well-known.
|
||||||
fn lang_from_path(path: &str) -> Option<Lang> {
|
fn lang_from_path(path: &str) -> Option<Lang> {
|
||||||
let p = Path::new(path);
|
let p = Path::new(path);
|
||||||
if let Some(ext) = p.extension().and_then(|e| e.to_str()) {
|
if let Some(ext) = p.extension().and_then(|e| e.to_str())
|
||||||
if let Some(lang) = Lang::from_extension(ext) {
|
&& let Some(lang) = Lang::from_extension(ext) {
|
||||||
return Some(lang);
|
return Some(lang);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
// Fall back to a shebang / content sniff over the file head.
|
// Fall back to a shebang / content sniff over the file head.
|
||||||
let head = read_file_head(p, 200);
|
let head = read_file_head(p, 200);
|
||||||
if head.is_empty() {
|
if head.is_empty() {
|
||||||
|
|
@ -1308,16 +1303,14 @@ fn lang_slug(lang: Lang) -> &'static str {
|
||||||
/// outermost callable that receives the tainted input.
|
/// outermost callable that receives the tainted input.
|
||||||
pub fn outermost_entry(steps: &[crate::evidence::FlowStep]) -> Option<EntryRef> {
|
pub fn outermost_entry(steps: &[crate::evidence::FlowStep]) -> Option<EntryRef> {
|
||||||
for step in steps {
|
for step in steps {
|
||||||
if matches!(step.kind, FlowStepKind::Source) {
|
if matches!(step.kind, FlowStepKind::Source)
|
||||||
if let Some(ref func) = step.function {
|
&& let Some(ref func) = step.function
|
||||||
if !func.is_empty() {
|
&& !func.is_empty() {
|
||||||
return Some(EntryRef {
|
return Some(EntryRef {
|
||||||
file: step.file.clone(),
|
file: step.file.clone(),
|
||||||
function: func.clone(),
|
function: func.clone(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
@ -1340,10 +1333,9 @@ pub fn default_toolchain_id(lang: Lang) -> &'static str {
|
||||||
|
|
||||||
/// Blake3 hash of the spec's key fields, truncated to 8 bytes and hex-encoded.
|
/// Blake3 hash of the spec's key fields, truncated to 8 bytes and hex-encoded.
|
||||||
///
|
///
|
||||||
/// Inputs (in order):
|
/// Inputs (in order): [`SPEC_FORMAT_VERSION`] (u32 LE), entry_file,
|
||||||
/// `SPEC_FORMAT_VERSION` (u32 LE), entry_file, entry_name, payload_slot tag
|
/// entry_name, payload_slot tag + value, expected_cap bits (u32 LE),
|
||||||
/// + value, expected_cap bits (u32 LE), sorted constraint_hints,
|
/// sorted constraint_hints, toolchain_id, [`CORPUS_VERSION`] (u32 LE).
|
||||||
/// toolchain_id, `CORPUS_VERSION` (u32 LE).
|
|
||||||
///
|
///
|
||||||
/// Bump [`SPEC_FORMAT_VERSION`] when the inputs or semantics change.
|
/// Bump [`SPEC_FORMAT_VERSION`] when the inputs or semantics change.
|
||||||
fn compute_spec_hash(spec: &HarnessSpec) -> String {
|
fn compute_spec_hash(spec: &HarnessSpec) -> String {
|
||||||
|
|
|
||||||
|
|
@ -226,11 +226,10 @@ fn accept_loop(
|
||||||
let _ = stream.set_read_timeout(Some(Duration::from_secs(2)));
|
let _ = stream.set_read_timeout(Some(Duration::from_secs(2)));
|
||||||
let _ = stream.set_write_timeout(Some(Duration::from_secs(2)));
|
let _ = stream.set_write_timeout(Some(Duration::from_secs(2)));
|
||||||
|
|
||||||
if let Some(ev) = handle_connection(stream, MAX_REQUEST_BYTES) {
|
if let Some(ev) = handle_connection(stream, MAX_REQUEST_BYTES)
|
||||||
if let Ok(mut g) = events.lock() {
|
&& let Ok(mut g) = events.lock() {
|
||||||
g.push(ev);
|
g.push(ev);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -261,21 +260,18 @@ fn handle_connection(mut stream: TcpStream, max_bytes: usize) -> Option<StubEven
|
||||||
if let Some(rest) = trimmed
|
if let Some(rest) = trimmed
|
||||||
.to_ascii_lowercase()
|
.to_ascii_lowercase()
|
||||||
.strip_prefix("content-length:")
|
.strip_prefix("content-length:")
|
||||||
{
|
&& let Ok(n) = rest.trim().parse::<usize>() {
|
||||||
if let Ok(n) = rest.trim().parse::<usize>() {
|
|
||||||
content_length = n.min(max_bytes);
|
content_length = n.min(max_bytes);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
headers.push(trimmed.to_owned());
|
headers.push(trimmed.to_owned());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Body, capped at content_length (already clamped to max_bytes).
|
// Body, capped at content_length (already clamped to max_bytes).
|
||||||
let mut body = vec![0u8; content_length];
|
let mut body = vec![0u8; content_length];
|
||||||
if content_length > 0 {
|
if content_length > 0
|
||||||
if reader.read_exact(&mut body).is_err() {
|
&& reader.read_exact(&mut body).is_err() {
|
||||||
body.clear();
|
body.clear();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Always reply 200 OK with no body.
|
// Always reply 200 OK with no body.
|
||||||
let _ = stream.write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n");
|
let _ = stream.write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n");
|
||||||
|
|
|
||||||
|
|
@ -115,11 +115,10 @@ fn try_rust_toolchain_toml(root: &Path) -> Option<ToolchainResolution> {
|
||||||
if line.starts_with('[') {
|
if line.starts_with('[') {
|
||||||
in_toolchain = false;
|
in_toolchain = false;
|
||||||
}
|
}
|
||||||
if in_toolchain && line.starts_with("channel") {
|
if in_toolchain && line.starts_with("channel")
|
||||||
if let Some(ver) = extract_version_from_toml_value(line) {
|
&& let Some(ver) = extract_version_from_toml_value(line) {
|
||||||
return Some(map_rust_version(&ver, RustPinOrigin::RustToolchainToml));
|
return Some(map_rust_version(&ver, RustPinOrigin::RustToolchainToml));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
@ -138,11 +137,10 @@ fn try_cargo_toml_rust_version(root: &Path) -> Option<ToolchainResolution> {
|
||||||
let content = std::fs::read_to_string(root.join("Cargo.toml")).ok()?;
|
let content = std::fs::read_to_string(root.join("Cargo.toml")).ok()?;
|
||||||
for line in content.lines() {
|
for line in content.lines() {
|
||||||
let line = line.trim();
|
let line = line.trim();
|
||||||
if line.starts_with("rust-version") {
|
if line.starts_with("rust-version")
|
||||||
if let Some(ver) = extract_version_from_toml_value(line) {
|
&& let Some(ver) = extract_version_from_toml_value(line) {
|
||||||
return Some(map_rust_version(&ver, RustPinOrigin::CargoToml));
|
return Some(map_rust_version(&ver, RustPinOrigin::CargoToml));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
@ -248,11 +246,10 @@ fn try_pyproject_toml(root: &Path) -> Option<ToolchainResolution> {
|
||||||
// Look for `requires-python = ">=3.11"` or `python = "3.11"`.
|
// Look for `requires-python = ">=3.11"` or `python = "3.11"`.
|
||||||
for line in content.lines() {
|
for line in content.lines() {
|
||||||
let line = line.trim();
|
let line = line.trim();
|
||||||
if line.starts_with("requires-python") || (line.starts_with("python") && line.contains('=') && !line.starts_with("python_requires")) {
|
if (line.starts_with("requires-python") || (line.starts_with("python") && line.contains('=') && !line.starts_with("python_requires")))
|
||||||
if let Some(ver) = extract_version_from_toml_value(line) {
|
&& let Some(ver) = extract_version_from_toml_value(line) {
|
||||||
return Some(map_version(&ver, PinOrigin::PyprojectToml));
|
return Some(map_version(&ver, PinOrigin::PyprojectToml));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
@ -269,11 +266,10 @@ fn try_pipfile(root: &Path) -> Option<ToolchainResolution> {
|
||||||
if line.starts_with('[') {
|
if line.starts_with('[') {
|
||||||
in_requires = false;
|
in_requires = false;
|
||||||
}
|
}
|
||||||
if in_requires && line.starts_with("python_version") {
|
if in_requires && line.starts_with("python_version")
|
||||||
if let Some(ver) = extract_version_from_toml_value(line) {
|
&& let Some(ver) = extract_version_from_toml_value(line) {
|
||||||
return Some(map_version(&ver, PinOrigin::Pipfile));
|
return Some(map_version(&ver, PinOrigin::Pipfile));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
@ -302,7 +298,7 @@ fn default_python() -> ToolchainResolution {
|
||||||
/// `requires-python = ">=3.11"` → `"3.11"`
|
/// `requires-python = ">=3.11"` → `"3.11"`
|
||||||
/// `python_version = "3.11"` → `"3.11"`
|
/// `python_version = "3.11"` → `"3.11"`
|
||||||
fn extract_version_from_toml_value(line: &str) -> Option<String> {
|
fn extract_version_from_toml_value(line: &str) -> Option<String> {
|
||||||
let after_eq = line.splitn(2, '=').nth(1)?;
|
let after_eq = line.split_once('=')?.1;
|
||||||
let raw = after_eq.trim().trim_matches('"').trim_matches('\'');
|
let raw = after_eq.trim().trim_matches('"').trim_matches('\'');
|
||||||
if raw.is_empty() {
|
if raw.is_empty() {
|
||||||
return None;
|
return None;
|
||||||
|
|
@ -335,7 +331,7 @@ fn map_version(version: &str, origin: PinOrigin) -> ToolchainResolution {
|
||||||
("3", Some("12")) => ("python-3.12".to_owned(), false),
|
("3", Some("12")) => ("python-3.12".to_owned(), false),
|
||||||
("3", Some("13")) => ("python-3.13".to_owned(), false),
|
("3", Some("13")) => ("python-3.13".to_owned(), false),
|
||||||
// Older 3.x → nearest supported is 3.8
|
// Older 3.x → nearest supported is 3.8
|
||||||
("3", Some(m)) if m.parse::<u32>().map_or(false, |v| v < 8) => {
|
("3", Some(m)) if m.parse::<u32>().is_ok_and(|v| v < 8) => {
|
||||||
("python-3.8".to_owned(), true)
|
("python-3.8".to_owned(), true)
|
||||||
}
|
}
|
||||||
// Newer 3.x beyond catalog → use 3.13 as closest
|
// Newer 3.x beyond catalog → use 3.13 as closest
|
||||||
|
|
@ -466,7 +462,7 @@ fn json_line_has_key(line: &str, key: &str) -> bool {
|
||||||
/// Extract a version string from a JSON value like `">=18"` or `"20.x"`.
|
/// Extract a version string from a JSON value like `">=18"` or `"20.x"`.
|
||||||
fn extract_version_from_json_value(line: &str) -> Option<String> {
|
fn extract_version_from_json_value(line: &str) -> Option<String> {
|
||||||
// Find the second quoted value after the colon.
|
// Find the second quoted value after the colon.
|
||||||
let after_colon = line.splitn(2, ':').nth(1)?;
|
let after_colon = line.split_once(':')?.1;
|
||||||
let raw = after_colon.trim().trim_matches('"').trim_matches('\'');
|
let raw = after_colon.trim().trim_matches('"').trim_matches('\'');
|
||||||
let ver = raw.trim_start_matches(|c: char| !c.is_ascii_digit());
|
let ver = raw.trim_start_matches(|c: char| !c.is_ascii_digit());
|
||||||
// Strip trailing junk: stop at the first char that isn't a version char.
|
// Strip trailing junk: stop at the first char that isn't a version char.
|
||||||
|
|
@ -535,10 +531,10 @@ fn map_go_version(version: &str, origin: PinOrigin) -> ToolchainResolution {
|
||||||
("1", Some("21")) => ("go-1.21".to_owned(), false),
|
("1", Some("21")) => ("go-1.21".to_owned(), false),
|
||||||
("1", Some("22")) => ("go-1.22".to_owned(), false),
|
("1", Some("22")) => ("go-1.22".to_owned(), false),
|
||||||
("1", Some("23")) => ("go-1.23".to_owned(), false),
|
("1", Some("23")) => ("go-1.23".to_owned(), false),
|
||||||
("1", Some(m)) if m.parse::<u32>().map_or(false, |v| v >= 24) => {
|
("1", Some(m)) if m.parse::<u32>().is_ok_and(|v| v >= 24) => {
|
||||||
(format!("go-1.{m}"), true)
|
(format!("go-1.{m}"), true)
|
||||||
}
|
}
|
||||||
("1", Some(m)) if m.parse::<u32>().map_or(false, |v| v < 21) => {
|
("1", Some(m)) if m.parse::<u32>().is_ok_and(|v| v < 21) => {
|
||||||
(format!("go-1.{m}"), true)
|
(format!("go-1.{m}"), true)
|
||||||
}
|
}
|
||||||
_ => ("go-stable".to_owned(), false),
|
_ => ("go-stable".to_owned(), false),
|
||||||
|
|
@ -575,14 +571,13 @@ fn try_pom_xml(root: &Path) -> Option<ToolchainResolution> {
|
||||||
for line in content.lines() {
|
for line in content.lines() {
|
||||||
let trimmed = line.trim();
|
let trimmed = line.trim();
|
||||||
for tag in &["<java.version>", "<maven.compiler.source>", "<maven.compiler.release>"] {
|
for tag in &["<java.version>", "<maven.compiler.source>", "<maven.compiler.release>"] {
|
||||||
if trimmed.starts_with(tag) {
|
if trimmed.starts_with(tag)
|
||||||
if let Some(inner) = trimmed.strip_prefix(tag) {
|
&& let Some(inner) = trimmed.strip_prefix(tag) {
|
||||||
let version = inner.split('<').next().unwrap_or("").trim();
|
let version = inner.split('<').next().unwrap_or("").trim();
|
||||||
if !version.is_empty() {
|
if !version.is_empty() {
|
||||||
return Some(map_java_version(version, PinOrigin::PomXml));
|
return Some(map_java_version(version, PinOrigin::PomXml));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
|
|
@ -597,11 +592,10 @@ fn try_build_gradle(root: &Path) -> Option<ToolchainResolution> {
|
||||||
let trimmed = line.trim();
|
let trimmed = line.trim();
|
||||||
// Groovy: sourceCompatibility = '21' or JavaVersion.VERSION_21
|
// Groovy: sourceCompatibility = '21' or JavaVersion.VERSION_21
|
||||||
// Kotlin: sourceCompatibility = JavaVersion.VERSION_21
|
// Kotlin: sourceCompatibility = JavaVersion.VERSION_21
|
||||||
if trimmed.starts_with("sourceCompatibility") || trimmed.starts_with("languageVersion") {
|
if (trimmed.starts_with("sourceCompatibility") || trimmed.starts_with("languageVersion"))
|
||||||
if let Some(ver) = extract_java_version_from_gradle_line(trimmed) {
|
&& let Some(ver) = extract_java_version_from_gradle_line(trimmed) {
|
||||||
return Some(map_java_version(&ver, PinOrigin::BuildGradle));
|
return Some(map_java_version(&ver, PinOrigin::BuildGradle));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
|
|
@ -610,7 +604,7 @@ fn try_build_gradle(root: &Path) -> Option<ToolchainResolution> {
|
||||||
fn extract_java_version_from_gradle_line(line: &str) -> Option<String> {
|
fn extract_java_version_from_gradle_line(line: &str) -> Option<String> {
|
||||||
// Handle: sourceCompatibility = '21' or sourceCompatibility = 21
|
// Handle: sourceCompatibility = '21' or sourceCompatibility = 21
|
||||||
// and: languageVersion.set(JavaLanguageVersion.of(21))
|
// and: languageVersion.set(JavaLanguageVersion.of(21))
|
||||||
let after_eq = line.splitn(2, '=').nth(1).unwrap_or(line);
|
let after_eq = line.split_once('=').map(|x| x.1).unwrap_or(line);
|
||||||
// Try to find a number in the value.
|
// Try to find a number in the value.
|
||||||
let digits: String = after_eq.chars()
|
let digits: String = after_eq.chars()
|
||||||
.skip_while(|c| !c.is_ascii_digit())
|
.skip_while(|c| !c.is_ascii_digit())
|
||||||
|
|
@ -687,13 +681,12 @@ fn try_composer_json(root: &Path) -> Option<ToolchainResolution> {
|
||||||
if json_line_has_key(trimmed, "require") {
|
if json_line_has_key(trimmed, "require") {
|
||||||
in_require = true;
|
in_require = true;
|
||||||
}
|
}
|
||||||
if in_require && trimmed.contains("\"php\"") {
|
if in_require && trimmed.contains("\"php\"")
|
||||||
if let Some(ver) = extract_version_from_json_value(trimmed) {
|
&& let Some(ver) = extract_version_from_json_value(trimmed) {
|
||||||
return Some(map_php_version(&ver, PinOrigin::ComposerJson));
|
return Some(map_php_version(&ver, PinOrigin::ComposerJson));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
// Stop at closing brace of require block.
|
// Stop at closing brace of require block.
|
||||||
if in_require && trimmed == "}," || (in_require && trimmed == "}") {
|
if in_require && (trimmed == "}," || trimmed == "}") {
|
||||||
in_require = false;
|
in_require = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -713,8 +713,8 @@ pub fn verify_finding(diag: &Diag, opts: &VerifyOptions) -> VerifyResult {
|
||||||
// Verdict cache lookup (§12 Q5): skip execution when a valid cached result exists.
|
// Verdict cache lookup (§12 Q5): skip execution when a valid cached result exists.
|
||||||
let entry_hash = compute_entry_content_hash(&spec.entry_file);
|
let entry_hash = compute_entry_content_hash(&spec.entry_file);
|
||||||
let import_digest = transitive_import_digest_placeholder();
|
let import_digest = transitive_import_digest_placeholder();
|
||||||
if let Some(ref db_path) = opts.db_path {
|
if let Some(ref db_path) = opts.db_path
|
||||||
if let Some(cached) = lookup_verdict_cache(
|
&& let Some(cached) = lookup_verdict_cache(
|
||||||
db_path,
|
db_path,
|
||||||
&spec.spec_hash,
|
&spec.spec_hash,
|
||||||
&entry_hash,
|
&entry_hash,
|
||||||
|
|
@ -723,7 +723,6 @@ pub fn verify_finding(diag: &Diag, opts: &VerifyOptions) -> VerifyResult {
|
||||||
) {
|
) {
|
||||||
return cached;
|
return cached;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Phase 10 (Track D.3): spawn the boundary stubs the spec
|
// Phase 10 (Track D.3): spawn the boundary stubs the spec
|
||||||
// demands *before* the sandbox runs. When `stubs_required` is
|
// demands *before* the sandbox runs. When `stubs_required` is
|
||||||
|
|
@ -998,14 +997,14 @@ fn build_verdict(
|
||||||
);
|
);
|
||||||
|
|
||||||
// If repro write fails, downgrade to NonReproducible.
|
// If repro write fails, downgrade to NonReproducible.
|
||||||
if repro_result.is_err() {
|
if let Err(err) = repro_result {
|
||||||
return VerifyResult {
|
return VerifyResult {
|
||||||
finding_id: finding_id.to_owned(),
|
finding_id: finding_id.to_owned(),
|
||||||
status: VerifyStatus::Inconclusive,
|
status: VerifyStatus::Inconclusive,
|
||||||
triggered_payload: None,
|
triggered_payload: None,
|
||||||
reason: None,
|
reason: None,
|
||||||
inconclusive_reason: Some(InconclusiveReason::NonReproducible),
|
inconclusive_reason: Some(InconclusiveReason::NonReproducible),
|
||||||
detail: Some(format!("repro write failed: {}", repro_result.unwrap_err())),
|
detail: Some(format!("repro write failed: {err}")),
|
||||||
attempts,
|
attempts,
|
||||||
toolchain_match: Some(toolchain_match.to_owned()),
|
toolchain_match: Some(toolchain_match.to_owned()),
|
||||||
differential: run.differential,
|
differential: run.differential,
|
||||||
|
|
|
||||||
|
|
@ -315,11 +315,10 @@ pub fn build_sarif_with_chains(
|
||||||
// this finding participates in (if any). Stable across
|
// this finding participates in (if any). Stable across
|
||||||
// reruns because both the finding's `stable_hash` and the
|
// reruns because both the finding's `stable_hash` and the
|
||||||
// chain's `stable_hash` are byte-deterministic.
|
// chain's `stable_hash` are byte-deterministic.
|
||||||
if d.stable_hash != 0 {
|
if d.stable_hash != 0
|
||||||
if let Some(chain_hash) = chain_member_of.get(&d.stable_hash) {
|
&& let Some(chain_hash) = chain_member_of.get(&d.stable_hash) {
|
||||||
props.insert("chain_member_of".into(), json!(chain_hash));
|
props.insert("chain_member_of".into(), json!(chain_hash));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
result["properties"] = Value::Object(props);
|
result["properties"] = Value::Object(props);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -40,8 +40,8 @@ pub fn detect_rails_routes(
|
||||||
|
|
||||||
fn detect_routes_dsl(root: Node, bytes: &[u8], file_rel: &str, out: &mut Vec<SurfaceNode>) {
|
fn detect_routes_dsl(root: Node, bytes: &[u8], file_rel: &str, out: &mut Vec<SurfaceNode>) {
|
||||||
fn recurse(node: Node, bytes: &[u8], file_rel: &str, out: &mut Vec<SurfaceNode>) {
|
fn recurse(node: Node, bytes: &[u8], file_rel: &str, out: &mut Vec<SurfaceNode>) {
|
||||||
if matches!(node.kind(), "call" | "method_call") {
|
if matches!(node.kind(), "call" | "method_call")
|
||||||
if let Some(method_node) = node.child_by_field_name("method")
|
&& let Some(method_node) = node.child_by_field_name("method")
|
||||||
&& let Ok(method_text) = method_node.utf8_text(bytes)
|
&& let Ok(method_text) = method_node.utf8_text(bytes)
|
||||||
&& let Some((_, method)) = VERBS.iter().find(|(v, _)| *v == method_text)
|
&& let Some((_, method)) = VERBS.iter().find(|(v, _)| *v == method_text)
|
||||||
{
|
{
|
||||||
|
|
@ -73,7 +73,6 @@ fn detect_routes_dsl(root: Node, bytes: &[u8], file_rel: &str, out: &mut Vec<Sur
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
let mut cursor = node.walk();
|
let mut cursor = node.walk();
|
||||||
for child in node.children(&mut cursor) {
|
for child in node.children(&mut cursor) {
|
||||||
recurse(child, bytes, file_rel, out);
|
recurse(child, bytes, file_rel, out);
|
||||||
|
|
|
||||||
|
|
@ -53,9 +53,9 @@ fn is_app_router_route(path: &Path) -> bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_pages_api_route(path: &Path) -> bool {
|
fn is_pages_api_route(path: &Path) -> bool {
|
||||||
let mut comps = path.components().peekable();
|
let comps = path.components().peekable();
|
||||||
let mut saw_pages = false;
|
let mut saw_pages = false;
|
||||||
while let Some(c) = comps.next() {
|
for c in comps {
|
||||||
if c.as_os_str().to_string_lossy() == "pages" {
|
if c.as_os_str().to_string_lossy() == "pages" {
|
||||||
saw_pages = true;
|
saw_pages = true;
|
||||||
} else if saw_pages && c.as_os_str().to_string_lossy() == "api" {
|
} else if saw_pages && c.as_os_str().to_string_lossy() == "api" {
|
||||||
|
|
|
||||||
|
|
@ -341,11 +341,10 @@ impl SurfaceMap {
|
||||||
/// Returns the absolute path verbatim when the file is outside the
|
/// Returns the absolute path verbatim when the file is outside the
|
||||||
/// scan root or when path stripping fails.
|
/// scan root or when path stripping fails.
|
||||||
pub fn relative_path_string(path: &Path, scan_root: Option<&Path>) -> String {
|
pub fn relative_path_string(path: &Path, scan_root: Option<&Path>) -> String {
|
||||||
if let Some(root) = scan_root {
|
if let Some(root) = scan_root
|
||||||
if let Ok(rel) = path.strip_prefix(root) {
|
&& let Ok(rel) = path.strip_prefix(root) {
|
||||||
return rel.to_string_lossy().replace('\\', "/");
|
return rel.to_string_lossy().replace('\\', "/");
|
||||||
}
|
}
|
||||||
}
|
|
||||||
path.to_string_lossy().replace('\\', "/")
|
path.to_string_lossy().replace('\\', "/")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -114,11 +114,10 @@ impl Lang {
|
||||||
/// Used by [`crate::dynamic::spec`] so spec derivation no longer rejects
|
/// Used by [`crate::dynamic::spec`] so spec derivation no longer rejects
|
||||||
/// CLI entry points and other extensionless / non-canonical files.
|
/// CLI entry points and other extensionless / non-canonical files.
|
||||||
pub fn from_path_or_content(path: &Path, head_bytes: &[u8]) -> Option<Lang> {
|
pub fn from_path_or_content(path: &Path, head_bytes: &[u8]) -> Option<Lang> {
|
||||||
if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
|
if let Some(ext) = path.extension().and_then(|e| e.to_str())
|
||||||
if let Some(lang) = Self::from_extension(ext) {
|
&& let Some(lang) = Self::from_extension(ext) {
|
||||||
return Some(lang);
|
return Some(lang);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if let Some(lang) = lang_from_shebang(head_bytes) {
|
if let Some(lang) = lang_from_shebang(head_bytes) {
|
||||||
return Some(lang);
|
return Some(lang);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -256,7 +256,7 @@ pub struct FixtureSpec<'a> {
|
||||||
///
|
///
|
||||||
/// Captures the fields a regression test must pin: status + typed reasons
|
/// Captures the fields a regression test must pin: status + typed reasons
|
||||||
/// + whether a payload triggered. Excludes machine-dependent fields
|
/// + whether a payload triggered. Excludes machine-dependent fields
|
||||||
/// (`finding_id`, `detail`, `attempts`, `toolchain_match`).
|
/// (`finding_id`, `detail`, `attempts`, `toolchain_match`).
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
pub struct GoldenVerdict {
|
pub struct GoldenVerdict {
|
||||||
pub status: VerifyStatus,
|
pub status: VerifyStatus,
|
||||||
|
|
|
||||||
|
|
@ -24,11 +24,13 @@ use std::collections::BTreeSet;
|
||||||
const RUN_COUNT: usize = 10;
|
const RUN_COUNT: usize = 10;
|
||||||
|
|
||||||
fn deny_diag(stable_hash: u64) -> Diag {
|
fn deny_diag(stable_hash: u64) -> Diag {
|
||||||
let mut ev = Evidence::default();
|
|
||||||
// Triggers the credentials deny rule via the AWS-key regex from
|
// Triggers the credentials deny rule via the AWS-key regex from
|
||||||
// `crate::utils::redact::contains_secret`. The deny rule fires
|
// `crate::utils::redact::contains_secret`. The deny rule fires
|
||||||
// deterministically because the rule lookup table is `const`.
|
// deterministically because the rule lookup table is `const`.
|
||||||
ev.notes = vec!["secret=AKIAFAKEDETERM00000000".to_owned()];
|
let ev = Evidence {
|
||||||
|
notes: vec!["secret=AKIAFAKEDETERM00000000".to_owned()],
|
||||||
|
..Evidence::default()
|
||||||
|
};
|
||||||
Diag {
|
Diag {
|
||||||
path: "src/handler.py".to_owned(),
|
path: "src/handler.py".to_owned(),
|
||||||
line: 42,
|
line: 42,
|
||||||
|
|
@ -84,9 +86,11 @@ fn ten_runs_produce_byte_identical_telemetry_minus_timestamps() {
|
||||||
|
|
||||||
let diag = deny_diag(0x0123_4567_89ab_cdef);
|
let diag = deny_diag(0x0123_4567_89ab_cdef);
|
||||||
|
|
||||||
let mut opts = VerifyOptions::default();
|
let opts = VerifyOptions {
|
||||||
opts.telemetry_policy = SamplingPolicy::keep_all();
|
telemetry_policy: SamplingPolicy::keep_all(),
|
||||||
opts.trace_verbose = false;
|
trace_verbose: false,
|
||||||
|
..VerifyOptions::default()
|
||||||
|
};
|
||||||
|
|
||||||
let mut verdict_jsons: BTreeSet<String> = BTreeSet::new();
|
let mut verdict_jsons: BTreeSet<String> = BTreeSet::new();
|
||||||
for _ in 0..RUN_COUNT {
|
for _ in 0..RUN_COUNT {
|
||||||
|
|
|
||||||
|
|
@ -127,11 +127,10 @@ mod parity_tests {
|
||||||
// BackendUnavailable into Unsupported OR Inconclusive depending on
|
// BackendUnavailable into Unsupported OR Inconclusive depending on
|
||||||
// where the error surfaces, so the skip predicate looks at the
|
// where the error surfaces, so the skip predicate looks at the
|
||||||
// reason text, not the verdict status.
|
// reason text, not the verdict status.
|
||||||
if let Some(ref r) = docker_result.reason {
|
if let Some(ref r) = docker_result.reason
|
||||||
if format!("{r:?}").contains("BackendUnavailable") {
|
&& format!("{r:?}").contains("BackendUnavailable") {
|
||||||
return; // Docker absent — skip comparison.
|
return; // Docker absent — skip comparison.
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
process_result.status, docker_result.status,
|
process_result.status, docker_result.status,
|
||||||
|
|
|
||||||
|
|
@ -189,8 +189,10 @@ mod verify_e2e {
|
||||||
|
|
||||||
let diag = taint_diag_with_cap(Cap::CRYPTO);
|
let diag = taint_diag_with_cap(Cap::CRYPTO);
|
||||||
let trace = Arc::new(VerifyTrace::new());
|
let trace = Arc::new(VerifyTrace::new());
|
||||||
let mut opts = VerifyOptions::default();
|
let opts = VerifyOptions {
|
||||||
opts.trace_sink = Some(Arc::clone(&trace));
|
trace_sink: Some(Arc::clone(&trace)),
|
||||||
|
..VerifyOptions::default()
|
||||||
|
};
|
||||||
|
|
||||||
let _result = verify_finding(&diag, &opts);
|
let _result = verify_finding(&diag, &opts);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ fn scan_with_hashes(dir: &Path) -> Vec<nyx_scanner::commands::scan::Diag> {
|
||||||
|
|
||||||
/// Attach a simulated dynamic verdict to every finding in the list.
|
/// Attach a simulated dynamic verdict to every finding in the list.
|
||||||
fn set_verdict(
|
fn set_verdict(
|
||||||
diags: &mut Vec<nyx_scanner::commands::scan::Diag>,
|
diags: &mut [nyx_scanner::commands::scan::Diag],
|
||||||
status: VerifyStatus,
|
status: VerifyStatus,
|
||||||
) {
|
) {
|
||||||
for d in diags.iter_mut() {
|
for d in diags.iter_mut() {
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
#![allow(deprecated)]
|
||||||
//! Marker uniqueness test (§4.1, §17.4).
|
//! Marker uniqueness test (§4.1, §17.4).
|
||||||
//!
|
//!
|
||||||
//! Asserts that no `NYX_PWN_*` marker from one cap's corpus is a substring
|
//! Asserts that no `NYX_PWN_*` marker from one cap's corpus is a substring
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
#![allow(clippy::field_reassign_with_default)]
|
||||||
//! Phase 30 (Track C — security): coverage for
|
//! Phase 30 (Track C — security): coverage for
|
||||||
//! [`crate::dynamic::policy::evaluate`] deny rules.
|
//! [`crate::dynamic::policy::evaluate`] deny rules.
|
||||||
//!
|
//!
|
||||||
|
|
|
||||||
|
|
@ -142,12 +142,13 @@ fn flask_eval_verdict() -> VerifyResult {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn flask_eval_sandbox_options() -> SandboxOptions {
|
fn flask_eval_sandbox_options() -> SandboxOptions {
|
||||||
let mut opts = SandboxOptions::default();
|
SandboxOptions {
|
||||||
opts.backend = SandboxBackend::Docker;
|
backend: SandboxBackend::Docker,
|
||||||
opts.env_passthrough = vec!["NYX_PAYLOAD".into()];
|
env_passthrough: vec!["NYX_PAYLOAD".into()],
|
||||||
opts.timeout = Duration::from_secs(30);
|
timeout: Duration::from_secs(30),
|
||||||
opts.memory_mib = 256;
|
memory_mib: 256,
|
||||||
opts
|
..SandboxOptions::default()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn workspace_root() -> PathBuf {
|
fn workspace_root() -> PathBuf {
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
#![allow(clippy::field_reassign_with_default)]
|
||||||
//! Phase 04 acceptance: callgraph-aware
|
//! Phase 04 acceptance: callgraph-aware
|
||||||
//! [`SpecDerivationStrategy::FromCallgraphEntry`].
|
//! [`SpecDerivationStrategy::FromCallgraphEntry`].
|
||||||
//!
|
//!
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
#![allow(clippy::field_reassign_with_default)]
|
||||||
//! Phase 01, Track A.1: integration coverage for
|
//! Phase 01, Track A.1: integration coverage for
|
||||||
//! `HarnessSpec::from_finding_opts` strategy fall-through.
|
//! `HarnessSpec::from_finding_opts` strategy fall-through.
|
||||||
//!
|
//!
|
||||||
|
|
|
||||||
|
|
@ -27,34 +27,36 @@ use nyx_scanner::patterns::{FindingCategory, Severity};
|
||||||
/// and a synthetic per-name summary, so the framework adapter registry
|
/// and a synthetic per-name summary, so the framework adapter registry
|
||||||
/// resolves a binding when the fixture's source matches an adapter.
|
/// 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 {
|
fn make_diag(path: &str, handler: &str, line: usize, cap: Cap, rule_id: &str) -> Diag {
|
||||||
let mut ev = Evidence::default();
|
let ev = Evidence {
|
||||||
ev.flow_steps = vec![
|
flow_steps: vec![
|
||||||
FlowStep {
|
FlowStep {
|
||||||
step: 0,
|
step: 0,
|
||||||
kind: FlowStepKind::Source,
|
kind: FlowStepKind::Source,
|
||||||
file: path.into(),
|
file: path.into(),
|
||||||
line: line as u32,
|
line: line as u32,
|
||||||
col: 0,
|
col: 0,
|
||||||
snippet: None,
|
snippet: None,
|
||||||
variable: None,
|
variable: None,
|
||||||
callee: None,
|
callee: None,
|
||||||
function: Some(handler.into()),
|
function: Some(handler.into()),
|
||||||
is_cross_file: false,
|
is_cross_file: false,
|
||||||
},
|
},
|
||||||
FlowStep {
|
FlowStep {
|
||||||
step: 1,
|
step: 1,
|
||||||
kind: FlowStepKind::Sink,
|
kind: FlowStepKind::Sink,
|
||||||
file: path.into(),
|
file: path.into(),
|
||||||
line: line as u32,
|
line: line as u32,
|
||||||
col: 0,
|
col: 0,
|
||||||
snippet: None,
|
snippet: None,
|
||||||
variable: None,
|
variable: None,
|
||||||
callee: None,
|
callee: None,
|
||||||
function: Some(handler.into()),
|
function: Some(handler.into()),
|
||||||
is_cross_file: false,
|
is_cross_file: false,
|
||||||
},
|
},
|
||||||
];
|
],
|
||||||
ev.sink_caps = cap.bits();
|
sink_caps: cap.bits(),
|
||||||
|
..Evidence::default()
|
||||||
|
};
|
||||||
Diag {
|
Diag {
|
||||||
path: path.into(),
|
path: path.into(),
|
||||||
line,
|
line,
|
||||||
|
|
|
||||||
|
|
@ -48,8 +48,7 @@ fn read_fixture(stub_dir: &str, name: &str) -> String {
|
||||||
/// begin with `//`; the payload is the surviving line.
|
/// begin with `//`; the payload is the surviving line.
|
||||||
fn extract_payload(s: &str) -> String {
|
fn extract_payload(s: &str) -> String {
|
||||||
s.lines()
|
s.lines()
|
||||||
.filter(|l| !l.trim().is_empty() && !l.trim_start().starts_with("//"))
|
.rfind(|l| !l.trim().is_empty() && !l.trim_start().starts_with("//"))
|
||||||
.last()
|
|
||||||
.unwrap_or("")
|
.unwrap_or("")
|
||||||
.trim()
|
.trim()
|
||||||
.to_owned()
|
.to_owned()
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue