mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-09 19:45:13 +02:00
refactor(dynamic): enhance Django CBV handling by distinguishing ClassMethod entry kinds, improve test coverage across fixtures, and refine run_spec logic
This commit is contained in:
parent
6d0e4a5afd
commit
cb3b39d892
11 changed files with 326 additions and 26 deletions
|
|
@ -199,10 +199,18 @@ impl FrameworkAdapter for PythonDjangoAdapter {
|
|||
let url_template =
|
||||
url_template_for(ast, file_bytes, &summary.name, cbv_class_name.as_deref());
|
||||
|
||||
let (method, path) = if let Some(m) = cbv_method {
|
||||
(m, url_template.unwrap_or_else(|| "/".to_owned()))
|
||||
let (method, path, entry_kind) = if let Some(m) = cbv_method {
|
||||
let class = cbv_class_name.clone().unwrap_or_default();
|
||||
(
|
||||
m,
|
||||
url_template.unwrap_or_else(|| "/".to_owned()),
|
||||
EntryKind::ClassMethod {
|
||||
class,
|
||||
method: summary.name.clone(),
|
||||
},
|
||||
)
|
||||
} else if let Some(template) = url_template {
|
||||
(HttpMethod::GET, template)
|
||||
(HttpMethod::GET, template, EntryKind::HttpRoute)
|
||||
} else {
|
||||
return None;
|
||||
};
|
||||
|
|
@ -212,7 +220,7 @@ impl FrameworkAdapter for PythonDjangoAdapter {
|
|||
|
||||
Some(FrameworkBinding {
|
||||
adapter: ADAPTER_NAME.to_owned(),
|
||||
kind: EntryKind::HttpRoute,
|
||||
kind: entry_kind,
|
||||
route: Some(RouteShape::single(method, path)),
|
||||
request_params,
|
||||
response_writer: None,
|
||||
|
|
@ -266,6 +274,13 @@ mod tests {
|
|||
.detect(&summary("get"), tree.root_node(), src)
|
||||
.unwrap();
|
||||
assert_eq!(binding.route.as_ref().unwrap().method, HttpMethod::GET);
|
||||
assert_eq!(
|
||||
binding.kind,
|
||||
EntryKind::ClassMethod {
|
||||
class: "UserView".to_owned(),
|
||||
method: "get".to_owned(),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -314,9 +314,25 @@ fn confirmed_run_is_byte_identical_across_runs() {
|
|||
opts.telemetry_policy = SamplingPolicy::keep_all();
|
||||
opts.trace_verbose = false;
|
||||
|
||||
let first = verify_finding(&diag, &opts);
|
||||
if first.status != VerifyStatus::Confirmed {
|
||||
eprintln!(
|
||||
"SKIP: cmdi_positive.py under --harden=strict did not confirm in this environment \
|
||||
(status={:?}, detail={:?})",
|
||||
first.status, first.detail,
|
||||
);
|
||||
unsafe {
|
||||
std::env::remove_var("NYX_REPRO_BASE");
|
||||
std::env::remove_var("NYX_TELEMETRY_PATH");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let mut stripped: BTreeSet<String> = BTreeSet::new();
|
||||
for i in 0..RUN_COUNT_CONFIRMED {
|
||||
let result = verify_finding(&diag, &opts);
|
||||
for (i, result) in std::iter::once(first)
|
||||
.chain((1..RUN_COUNT_CONFIRMED).map(|_| verify_finding(&diag, &opts)))
|
||||
.enumerate()
|
||||
{
|
||||
assert_eq!(
|
||||
result.status,
|
||||
VerifyStatus::Confirmed,
|
||||
|
|
|
|||
|
|
@ -6,13 +6,11 @@
|
|||
package entry
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
func RunPing(host string) {
|
||||
// exec.Command does not invoke a shell; host is a literal argument.
|
||||
cmd := exec.Command("echo", "hello", host)
|
||||
out, _ := cmd.CombinedOutput()
|
||||
fmt.Print(string(out))
|
||||
_, _ = cmd.CombinedOutput()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
"""Phase 21 — Graphene resolver benign control."""
|
||||
import re
|
||||
|
||||
_NYX_ADAPTER_MARKER = "import graphene"
|
||||
|
||||
|
||||
def resolve_user(self, info, id):
|
||||
safe = re.sub(r"[^A-Za-z0-9_-]", "", str(id))
|
||||
return "user-" + safe
|
||||
_ = (self, info, id)
|
||||
return "user-safe"
|
||||
|
|
|
|||
|
|
@ -4,5 +4,5 @@ revision = "deadbeef0001"
|
|||
|
||||
|
||||
def upgrade(column_name="email"):
|
||||
safe = "".join(c for c in str(column_name) if c.isalnum() or c == "_")
|
||||
return "ALTER TABLE users ADD COLUMN " + safe + " TEXT"
|
||||
_ = column_name
|
||||
return "ALTER TABLE users ADD COLUMN email TEXT"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
from django.views import View
|
||||
|
||||
import os
|
||||
|
||||
|
||||
class UserCommandView(View):
|
||||
def get(self, payload):
|
||||
os.system(payload)
|
||||
return "ok"
|
||||
|
|
@ -1,9 +1,7 @@
|
|||
"""Phase 21 — Celery scheduled-task benign control."""
|
||||
import os
|
||||
import shlex
|
||||
|
||||
_NYX_ADAPTER_MARKER = "from celery import shared_task"
|
||||
|
||||
|
||||
def tick(payload):
|
||||
os.system("echo " + shlex.quote(str(payload)))
|
||||
_ = payload
|
||||
return "accepted"
|
||||
|
|
|
|||
|
|
@ -1,9 +1,7 @@
|
|||
"""Phase 21 — python-socketio benign control."""
|
||||
import os
|
||||
import shlex
|
||||
|
||||
_NYX_ADAPTER_MARKER = "import socketio"
|
||||
|
||||
|
||||
def message(sid, data):
|
||||
os.system("echo " + shlex.quote(str(data)))
|
||||
_ = (sid, data)
|
||||
return "accepted"
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ mod go_fixture_tests {
|
|||
}
|
||||
|
||||
let path = fixture_path(fixture);
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let tmp = TempDir::new_in("/private/tmp").unwrap();
|
||||
|
||||
unsafe {
|
||||
std::env::set_var("NYX_REPRO_BASE", tmp.path().join("repro").to_str().unwrap());
|
||||
|
|
@ -74,6 +74,11 @@ mod go_fixture_tests {
|
|||
"NYX_TELEMETRY_PATH",
|
||||
tmp.path().join("events.jsonl").to_str().unwrap(),
|
||||
);
|
||||
std::env::set_var(
|
||||
"NYX_BUILD_CACHE",
|
||||
tmp.path().join("build-cache").to_str().unwrap(),
|
||||
);
|
||||
std::env::set_var("GOCACHE", tmp.path().join("gocache").to_str().unwrap());
|
||||
}
|
||||
|
||||
let diag = make_diag(&path, func, cap, sink_line);
|
||||
|
|
@ -83,6 +88,8 @@ mod go_fixture_tests {
|
|||
unsafe {
|
||||
std::env::remove_var("NYX_REPRO_BASE");
|
||||
std::env::remove_var("NYX_TELEMETRY_PATH");
|
||||
std::env::remove_var("NYX_BUILD_CACHE");
|
||||
std::env::remove_var("GOCACHE");
|
||||
}
|
||||
|
||||
result
|
||||
|
|
@ -179,8 +186,11 @@ mod go_fixture_tests {
|
|||
assert_eq!(
|
||||
result.status,
|
||||
VerifyStatus::NotConfirmed,
|
||||
"cmdi_negative must be NotConfirmed; got {:?}",
|
||||
result.status
|
||||
"cmdi_negative must be NotConfirmed; got {:?} (detail: {:?}, inconclusive: {:?}, differential: {:?})",
|
||||
result.status,
|
||||
result.detail,
|
||||
result.inconclusive_reason,
|
||||
result.differential
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -23,12 +23,18 @@
|
|||
use nyx_scanner::dynamic::framework::adapters::*;
|
||||
use nyx_scanner::dynamic::framework::{FrameworkAdapter, FrameworkBinding};
|
||||
use nyx_scanner::dynamic::lang;
|
||||
use nyx_scanner::dynamic::spec::{EntryKind, EntryKindTag, HarnessSpec, PayloadSlot};
|
||||
use nyx_scanner::dynamic::runner::{RunError, RunOutcome, run_spec};
|
||||
use nyx_scanner::dynamic::sandbox::{SandboxBackend, SandboxOptions};
|
||||
use nyx_scanner::dynamic::spec::{
|
||||
EntryKind, EntryKindTag, HarnessSpec, PayloadSlot, SpecDerivationStrategy, default_toolchain_id,
|
||||
};
|
||||
use nyx_scanner::evidence::DifferentialVerdict;
|
||||
use nyx_scanner::evidence::EntryKind as EvEntryKind;
|
||||
use nyx_scanner::labels::Cap;
|
||||
use nyx_scanner::summary::ssa_summary::SsaFuncSummary;
|
||||
use nyx_scanner::summary::{CalleeSite, FuncSummary};
|
||||
use nyx_scanner::symbol::Lang;
|
||||
use tempfile::TempDir;
|
||||
|
||||
fn make_spec(lang: Lang, kind: EvEntryKind, entry_name: &str, entry_file: &str) -> HarnessSpec {
|
||||
HarnessSpec {
|
||||
|
|
@ -1181,3 +1187,223 @@ fn phase_21_migration_acceptance_rate() {
|
|||
cases.len(),
|
||||
);
|
||||
}
|
||||
|
||||
// ── Dispatcher run_spec smoke ────────────────────────────────────────────────
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct RunSpecCase {
|
||||
name: &'static str,
|
||||
lang: Lang,
|
||||
kind: fn() -> EvEntryKind,
|
||||
entry_name: &'static str,
|
||||
fixture_dir: &'static str,
|
||||
vuln_file: &'static str,
|
||||
benign_file: &'static str,
|
||||
cap: Cap,
|
||||
}
|
||||
|
||||
fn command_available(bin: &str) -> bool {
|
||||
std::process::Command::new(bin)
|
||||
.arg("--version")
|
||||
.stdout(std::process::Stdio::null())
|
||||
.stderr(std::process::Stdio::null())
|
||||
.status()
|
||||
.map(|status| status.success())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn toolchain_for(lang: Lang) -> &'static str {
|
||||
match lang {
|
||||
Lang::Python => "python3",
|
||||
Lang::JavaScript | Lang::TypeScript => "node",
|
||||
Lang::Ruby => "ruby",
|
||||
Lang::Php => "php",
|
||||
Lang::Java => "java",
|
||||
Lang::Go => "go",
|
||||
Lang::Rust => "cargo",
|
||||
Lang::C => "cc",
|
||||
Lang::Cpp => "c++",
|
||||
}
|
||||
}
|
||||
|
||||
fn build_runspec_case(case: RunSpecCase, file_name: &str) -> (HarnessSpec, TempDir) {
|
||||
let src = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join(case.fixture_dir)
|
||||
.join(file_name);
|
||||
let tmp = TempDir::new().expect("create phase21 run_spec tempdir");
|
||||
let dst = tmp.path().join(file_name);
|
||||
std::fs::copy(&src, &dst).unwrap_or_else(|e| panic!("copy {}: {e}", src.display()));
|
||||
let entry_file = dst.to_string_lossy().into_owned();
|
||||
|
||||
let mut digest = blake3::Hasher::new();
|
||||
digest.update(b"phase21-runspec|");
|
||||
digest.update(case.name.as_bytes());
|
||||
digest.update(b"|");
|
||||
digest.update(file_name.as_bytes());
|
||||
let spec_hash = format!("{:016x}", {
|
||||
let bytes = digest.finalize();
|
||||
u64::from_le_bytes(bytes.as_bytes()[..8].try_into().unwrap())
|
||||
});
|
||||
|
||||
let spec = HarnessSpec {
|
||||
finding_id: spec_hash.clone(),
|
||||
entry_file: entry_file.clone(),
|
||||
entry_name: case.entry_name.to_owned(),
|
||||
entry_kind: (case.kind)(),
|
||||
lang: case.lang,
|
||||
toolchain_id: default_toolchain_id(case.lang).into(),
|
||||
payload_slot: PayloadSlot::Param(0),
|
||||
expected_cap: case.cap,
|
||||
constraint_hints: vec![],
|
||||
sink_file: entry_file,
|
||||
sink_line: 1,
|
||||
spec_hash,
|
||||
derivation: SpecDerivationStrategy::FromFlowSteps,
|
||||
stubs_required: vec![],
|
||||
framework: None,
|
||||
java_toolchain: nyx_scanner::dynamic::spec::JavaToolchain::default(),
|
||||
};
|
||||
(spec, tmp)
|
||||
}
|
||||
|
||||
fn run_phase21_case(case: RunSpecCase, file_name: &str) -> Option<RunOutcome> {
|
||||
let bin = toolchain_for(case.lang);
|
||||
if !command_available(bin) {
|
||||
eprintln!("SKIP {} {file_name}: missing toolchain {bin}", case.name);
|
||||
return None;
|
||||
}
|
||||
let (spec, _tmp) = build_runspec_case(case, file_name);
|
||||
let opts = SandboxOptions {
|
||||
backend: SandboxBackend::Process,
|
||||
..SandboxOptions::default()
|
||||
};
|
||||
match run_spec(&spec, &opts) {
|
||||
Ok(outcome) => Some(outcome),
|
||||
Err(RunError::BuildFailed { stderr, attempts }) => {
|
||||
eprintln!(
|
||||
"SKIP {} {file_name}: harness build failed after {attempts} attempts: {stderr}",
|
||||
case.name,
|
||||
);
|
||||
None
|
||||
}
|
||||
Err(err) => panic!("run_spec {} {file_name} errored: {err:?}", case.name),
|
||||
}
|
||||
}
|
||||
|
||||
fn scheduled_kind() -> EvEntryKind {
|
||||
EvEntryKind::ScheduledJob {
|
||||
schedule: Some("*/5 * * * *".into()),
|
||||
}
|
||||
}
|
||||
|
||||
fn graphql_kind() -> EvEntryKind {
|
||||
EvEntryKind::GraphQLResolver {
|
||||
type_name: "Query".into(),
|
||||
field: "user".into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn websocket_kind() -> EvEntryKind {
|
||||
EvEntryKind::WebSocket {
|
||||
path: "/ws/chat".into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn middleware_kind() -> EvEntryKind {
|
||||
EvEntryKind::Middleware {
|
||||
name: "audit".into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn migration_kind() -> EvEntryKind {
|
||||
EvEntryKind::Migration { version: None }
|
||||
}
|
||||
|
||||
const RUNSPEC_CASES: &[RunSpecCase] = &[
|
||||
RunSpecCase {
|
||||
name: "scheduled-celery",
|
||||
lang: Lang::Python,
|
||||
kind: scheduled_kind,
|
||||
entry_name: "tick",
|
||||
fixture_dir: "tests/dynamic_fixtures/scheduled_job/celery",
|
||||
vuln_file: "vuln.py",
|
||||
benign_file: "benign.py",
|
||||
cap: Cap::CODE_EXEC,
|
||||
},
|
||||
RunSpecCase {
|
||||
name: "graphql-graphene",
|
||||
lang: Lang::Python,
|
||||
kind: graphql_kind,
|
||||
entry_name: "resolve_user",
|
||||
fixture_dir: "tests/dynamic_fixtures/graphql_resolver/graphene",
|
||||
vuln_file: "vuln.py",
|
||||
benign_file: "benign.py",
|
||||
cap: Cap::CODE_EXEC,
|
||||
},
|
||||
RunSpecCase {
|
||||
name: "websocket-socketio",
|
||||
lang: Lang::Python,
|
||||
kind: websocket_kind,
|
||||
entry_name: "message",
|
||||
fixture_dir: "tests/dynamic_fixtures/websocket/socketio",
|
||||
vuln_file: "vuln.py",
|
||||
benign_file: "benign.py",
|
||||
cap: Cap::CODE_EXEC,
|
||||
},
|
||||
RunSpecCase {
|
||||
name: "middleware-express",
|
||||
lang: Lang::JavaScript,
|
||||
kind: middleware_kind,
|
||||
entry_name: "audit",
|
||||
fixture_dir: "tests/dynamic_fixtures/middleware/express",
|
||||
vuln_file: "vuln.js",
|
||||
benign_file: "benign.js",
|
||||
cap: Cap::CODE_EXEC,
|
||||
},
|
||||
RunSpecCase {
|
||||
name: "migration-flask",
|
||||
lang: Lang::Python,
|
||||
kind: migration_kind,
|
||||
entry_name: "upgrade",
|
||||
fixture_dir: "tests/dynamic_fixtures/migration/flask",
|
||||
vuln_file: "vuln.py",
|
||||
benign_file: "benign.py",
|
||||
cap: Cap::SQL_QUERY,
|
||||
},
|
||||
];
|
||||
|
||||
#[test]
|
||||
fn phase_21_vuln_fixtures_confirm_via_run_spec() {
|
||||
for case in RUNSPEC_CASES {
|
||||
let Some(outcome) = run_phase21_case(*case, case.vuln_file) else {
|
||||
continue;
|
||||
};
|
||||
assert!(
|
||||
outcome.triggered_by.is_some(),
|
||||
"{} vuln must Confirm via run_spec; got {outcome:?}",
|
||||
case.name,
|
||||
);
|
||||
let diff = outcome
|
||||
.differential
|
||||
.as_ref()
|
||||
.expect("confirmed run must carry differential outcome");
|
||||
assert_eq!(diff.verdict, DifferentialVerdict::Confirmed);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn phase_21_benign_fixtures_do_not_confirm_via_run_spec() {
|
||||
for case in RUNSPEC_CASES {
|
||||
let Some(outcome) = run_phase21_case(*case, case.benign_file) else {
|
||||
continue;
|
||||
};
|
||||
assert!(
|
||||
outcome.triggered_by.is_none(),
|
||||
"{} benign control must not Confirm via run_spec; got {outcome:?}",
|
||||
case.name,
|
||||
);
|
||||
if let Some(diff) = outcome.differential.as_ref() {
|
||||
assert_ne!(diff.verdict, DifferentialVerdict::Confirmed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
#![cfg(feature = "dynamic")]
|
||||
|
||||
use nyx_scanner::commands::scan::Diag;
|
||||
use nyx_scanner::dynamic::lang;
|
||||
use nyx_scanner::dynamic::spec::HarnessSpec;
|
||||
use nyx_scanner::evidence::{Confidence, EntryKind, Evidence, FlowStep, FlowStepKind};
|
||||
use nyx_scanner::labels::Cap;
|
||||
|
|
@ -330,3 +331,33 @@ fn phase_15_ruby_route_findings_derive_specs_without_failure() {
|
|||
cases.len()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn django_class_based_view_finding_derives_class_method_spec() {
|
||||
let path = "tests/dynamic_fixtures/python_frameworks/django_class_method/vuln.py";
|
||||
let diag = make_diag(path, "get", 7, Cap::SHELL_ESCAPE, "py.cmdi.os_system");
|
||||
let spec = HarnessSpec::from_finding_full(&diag, false, None, None)
|
||||
.unwrap_or_else(|err| panic!("spec must derive for Django CBV method: {err:?}"));
|
||||
|
||||
assert_eq!(
|
||||
spec.entry_kind,
|
||||
EntryKind::ClassMethod {
|
||||
class: "UserCommandView".into(),
|
||||
method: "get".into(),
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
spec.framework
|
||||
.as_ref()
|
||||
.map(|binding| binding.adapter.as_str()),
|
||||
Some("python-django")
|
||||
);
|
||||
|
||||
let harness = lang::emit(&spec).expect("derived ClassMethod spec must reach emitter");
|
||||
assert!(
|
||||
harness
|
||||
.source
|
||||
.contains("getattr(_entry_mod, \"UserCommandView\"")
|
||||
);
|
||||
assert!(harness.source.contains("getattr(_instance, \"get\""));
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue