//! Phase 21 (Track M.3) — end-to-end acceptance for the remaining //! five `EntryKind` variants: `ScheduledJob`, `GraphQLResolver`, //! `WebSocket`, `Middleware`, `Migration`. //! //! Each sub-test: //! - asserts the per-lang emitter advertises the new variant in its //! `entry_kinds_supported` slice (so the verifier dispatches //! structurally instead of degrading to //! `Inconclusive(EntryKindUnsupported)`), //! - drives a constructed `HarnessSpec` through `lang::emit` and //! checks the harness source carries the entry-kind sentinel //! (`__NYX_SCHEDULED_JOB__` / `__NYX_GRAPHQL_RESOLVER__` / //! `__NYX_WEBSOCKET__` / `__NYX_MIDDLEWARE__` / `__NYX_MIGRATION__`) //! and the entry-function name literal, //! - parses every fixture file with its tree-sitter grammar and //! runs the matching Phase 21 framework adapter, asserting the //! binding stamps the right `EntryKind` variant. //! //! `cargo nextest run --features dynamic --test phase21_corpus`. #![cfg(feature = "dynamic")] use nyx_scanner::dynamic::framework::adapters::*; use nyx_scanner::dynamic::framework::{FrameworkAdapter, FrameworkBinding}; use nyx_scanner::dynamic::lang; 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::dynamic::stubs::{StubHarness, StubKind}; 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 std::sync::Arc; use tempfile::TempDir; fn make_spec(lang: Lang, kind: EvEntryKind, entry_name: &str, entry_file: &str) -> HarnessSpec { HarnessSpec { finding_id: "phase21track-m3".into(), entry_file: entry_file.into(), entry_name: entry_name.into(), entry_kind: kind, lang, toolchain_id: "phase21".into(), payload_slot: PayloadSlot::Param(0), expected_cap: Cap::CODE_EXEC, constraint_hints: vec![], sink_file: entry_file.into(), sink_line: 1, spec_hash: "phase21track-m3".into(), derivation: nyx_scanner::dynamic::spec::SpecDerivationStrategy::FromFlowSteps, stubs_required: vec![], framework: None, java_toolchain: nyx_scanner::dynamic::spec::JavaToolchain::default(), } } fn parse(lang: Lang, src: &[u8]) -> tree_sitter::Tree { let mut parser = tree_sitter::Parser::new(); let ts_lang = match lang { Lang::Python => tree_sitter::Language::from(tree_sitter_python::LANGUAGE), Lang::JavaScript => tree_sitter::Language::from(tree_sitter_javascript::LANGUAGE), Lang::TypeScript => { tree_sitter::Language::from(tree_sitter_typescript::LANGUAGE_TYPESCRIPT) } Lang::Java => tree_sitter::Language::from(tree_sitter_java::LANGUAGE), Lang::Ruby => tree_sitter::Language::from(tree_sitter_ruby::LANGUAGE), Lang::Go => tree_sitter::Language::from(tree_sitter_go::LANGUAGE), Lang::Rust => tree_sitter::Language::from(tree_sitter_rust::LANGUAGE), Lang::Php => tree_sitter::Language::from(tree_sitter_php::LANGUAGE_PHP), Lang::C => tree_sitter::Language::from(tree_sitter_c::LANGUAGE), Lang::Cpp => tree_sitter::Language::from(tree_sitter_cpp::LANGUAGE), }; parser.set_language(&ts_lang).unwrap(); parser.parse(src, None).unwrap() } fn read_bytes(path: &str) -> Vec { std::fs::read(path).unwrap_or_else(|e| panic!("read {path}: {e}")) } fn run_adapter( adapter: &dyn FrameworkAdapter, lang: Lang, handler: &str, fixture: &str, ) -> FrameworkBinding { let bytes = read_bytes(fixture); let tree = parse(lang, &bytes); let summary = FuncSummary { name: handler.into(), ..Default::default() }; adapter .detect(&summary, tree.root_node(), &bytes) .unwrap_or_else(|| panic!("{} did not fire on {fixture}", adapter.name())) } fn framework_bound_spec( lang: Lang, kind: EvEntryKind, entry_name: &str, entry_file: &str, adapter: &str, ) -> HarnessSpec { let mut spec = make_spec(lang, kind, entry_name, entry_file); spec.framework = Some(FrameworkBinding { adapter: adapter.to_owned(), kind: spec.entry_kind.clone(), route: None, request_params: vec![], response_writer: None, middleware: vec![], }); spec } fn framework_bound_sql_spec( lang: Lang, kind: EvEntryKind, entry_name: &str, entry_file: &str, adapter: &str, ) -> HarnessSpec { let mut spec = framework_bound_spec(lang, kind, entry_name, entry_file, adapter); spec.expected_cap = Cap::SQL_QUERY; spec.stubs_required = StubKind::for_cap(Cap::SQL_QUERY); spec } fn extra_file_content<'a>(files: &'a [(String, String)], rel: &str) -> &'a str { files .iter() .find(|(path, _)| path == rel) .map(|(_, content)| content.as_str()) .unwrap_or_else(|| panic!("{rel} missing from extra files: {files:?}")) } fn detect_phase21_fp_fixture( adapter: &dyn FrameworkAdapter, lang: Lang, handler: &str, fixture: &str, typed_call: Option<(&str, &str, &str)>, ) -> Option { let bytes = std::fs::read( std::path::Path::new(env!("CARGO_MANIFEST_DIR")) .join("tests/fixtures/fp_guards/phase21_adapter_collisions") .join(fixture), ) .unwrap_or_else(|e| panic!("read Phase 21 FP fixture {fixture}: {e}")); let tree = parse(lang, &bytes); let mut summary = FuncSummary { name: handler.into(), ..Default::default() }; let mut ssa = SsaFuncSummary::default(); if let Some((callee, receiver, receiver_ty)) = typed_call { summary.callees.push(CalleeSite { name: callee.to_owned(), receiver: Some(receiver.to_owned()), ordinal: 0, ..Default::default() }); ssa.typed_call_receivers.push((0, receiver_ty.to_owned())); } let ssa_ref = typed_call.is_some().then_some(&ssa); adapter.detect_with_context(&summary, ssa_ref, tree.root_node(), &bytes) } struct Phase21FpCase<'a> { adapter: &'a dyn FrameworkAdapter, lang: Lang, handler: &'a str, fixture: &'a str, typed_call: Option<(&'a str, &'a str, &'a str)>, } // ── Supported-set assertions ────────────────────────────────────────────────── #[test] fn scheduled_job_supported_in_target_langs() { for lang in [Lang::Python, Lang::JavaScript, Lang::Java, Lang::Ruby] { assert!( lang::entry_kinds_supported(lang).contains(&EntryKindTag::ScheduledJob), "{lang:?} must advertise ScheduledJob after Phase 21", ); } } #[test] fn graphql_resolver_supported_in_target_langs() { for lang in [ Lang::Python, Lang::JavaScript, Lang::TypeScript, Lang::Rust, Lang::Go, ] { assert!( lang::entry_kinds_supported(lang).contains(&EntryKindTag::GraphQLResolver), "{lang:?} must advertise GraphQLResolver after Phase 21", ); } } #[test] fn websocket_supported_in_target_langs() { for lang in [Lang::Python, Lang::JavaScript, Lang::TypeScript, Lang::Ruby] { assert!( lang::entry_kinds_supported(lang).contains(&EntryKindTag::WebSocket), "{lang:?} must advertise WebSocket after Phase 21", ); } } #[test] fn middleware_supported_in_target_langs() { for lang in [ Lang::Python, Lang::JavaScript, Lang::TypeScript, Lang::Java, Lang::Ruby, Lang::Php, ] { assert!( lang::entry_kinds_supported(lang).contains(&EntryKindTag::Middleware), "{lang:?} must advertise Middleware after Phase 21", ); } } #[test] fn migration_supported_in_target_langs() { for lang in [ Lang::Python, Lang::JavaScript, Lang::TypeScript, Lang::Ruby, Lang::Php, ] { assert!( lang::entry_kinds_supported(lang).contains(&EntryKindTag::Migration), "{lang:?} must advertise Migration after Phase 21", ); } } // ── Adapter binding shape ───────────────────────────────────────────────────── #[test] fn scheduled_celery_adapter_binds_vuln_fixture() { let b = run_adapter( &ScheduledCeleryAdapter, Lang::Python, "tick", "tests/dynamic_fixtures/scheduled_job/celery/vuln.py", ); assert_eq!(b.adapter, "scheduled-celery"); assert!(matches!(b.kind, EntryKind::ScheduledJob { .. })); } #[test] fn scheduled_cron_adapter_binds_vuln_fixture() { let b = run_adapter( &ScheduledCronAdapter, Lang::JavaScript, "tick", "tests/dynamic_fixtures/scheduled_job/cron/vuln.js", ); assert_eq!(b.adapter, "scheduled-cron"); if let EntryKind::ScheduledJob { schedule } = &b.kind { assert_eq!(schedule.as_deref(), Some("*/5 * * * *")); } else { panic!("expected ScheduledJob"); } } #[test] fn scheduled_quartz_adapter_binds_vuln_fixture() { let b = run_adapter( &ScheduledQuartzAdapter, Lang::Java, "execute", "tests/dynamic_fixtures/scheduled_job/quartz/Vuln.java", ); assert_eq!(b.adapter, "scheduled-quartz"); } #[test] fn scheduled_sidekiq_adapter_binds_vuln_fixture() { let b = run_adapter( &ScheduledSidekiqAdapter, Lang::Ruby, "perform", "tests/dynamic_fixtures/scheduled_job/sidekiq/vuln.rb", ); assert_eq!(b.adapter, "scheduled-sidekiq"); } #[test] fn graphql_apollo_adapter_binds_vuln_fixture() { let b = run_adapter( &GraphqlApolloAdapter, Lang::JavaScript, "resolveUser", "tests/dynamic_fixtures/graphql_resolver/apollo/vuln.js", ); assert_eq!(b.adapter, "graphql-apollo"); assert!(matches!(b.kind, EntryKind::GraphQLResolver { .. })); } #[test] fn graphql_graphene_adapter_binds_vuln_fixture() { let b = run_adapter( &GraphqlGrapheneAdapter, Lang::Python, "resolve_user", "tests/dynamic_fixtures/graphql_resolver/graphene/vuln.py", ); assert_eq!(b.adapter, "graphql-graphene"); if let EntryKind::GraphQLResolver { field, .. } = &b.kind { assert_eq!(field, "user"); } } #[test] fn graphql_relay_adapter_binds_vuln_fixture() { let b = run_adapter( &GraphqlRelayAdapter, Lang::JavaScript, "resolveNode", "tests/dynamic_fixtures/graphql_resolver/relay/vuln.js", ); assert_eq!(b.adapter, "graphql-relay"); } #[test] fn graphql_juniper_adapter_binds_vuln_fixture() { let b = run_adapter( &GraphqlJuniperAdapter, Lang::Rust, "resolve_user", "tests/dynamic_fixtures/graphql_resolver/juniper/vuln.rs", ); assert_eq!(b.adapter, "graphql-juniper"); } #[test] fn graphql_gqlgen_adapter_binds_vuln_fixture() { let b = run_adapter( &GraphqlGqlgenAdapter, Lang::Go, "ResolveUser", "tests/dynamic_fixtures/graphql_resolver/gqlgen/vuln.go", ); assert_eq!(b.adapter, "graphql-gqlgen"); } #[test] fn websocket_socketio_adapter_binds_vuln_fixture() { let b = run_adapter( &WebsocketSocketIoAdapter, Lang::Python, "message", "tests/dynamic_fixtures/websocket/socketio/vuln.py", ); assert_eq!(b.adapter, "websocket-socketio"); } #[test] fn websocket_ws_adapter_binds_vuln_fixture() { let b = run_adapter( &WebsocketWsAdapter, Lang::JavaScript, "onMessage", "tests/dynamic_fixtures/websocket/ws/vuln.js", ); assert_eq!(b.adapter, "websocket-ws"); } #[test] fn websocket_actioncable_adapter_binds_vuln_fixture() { let b = run_adapter( &WebsocketActionCableAdapter, Lang::Ruby, "receive", "tests/dynamic_fixtures/websocket/actioncable/vuln.rb", ); assert_eq!(b.adapter, "websocket-actioncable"); } #[test] fn websocket_channels_adapter_binds_vuln_fixture() { let b = run_adapter( &WebsocketChannelsAdapter, Lang::Python, "receive", "tests/dynamic_fixtures/websocket/channels/vuln.py", ); assert_eq!(b.adapter, "websocket-channels"); } #[test] fn middleware_express_adapter_binds_vuln_fixture() { let b = run_adapter( &MiddlewareExpressAdapter, Lang::JavaScript, "audit", "tests/dynamic_fixtures/middleware/express/vuln.js", ); assert_eq!(b.adapter, "middleware-express"); assert!(matches!(b.kind, EntryKind::Middleware { .. })); } #[test] fn middleware_django_adapter_binds_vuln_fixture() { let b = run_adapter( &MiddlewareDjangoAdapter, Lang::Python, "audit", "tests/dynamic_fixtures/middleware/django/vuln.py", ); assert_eq!(b.adapter, "middleware-django"); } #[test] fn middleware_rails_adapter_binds_vuln_fixture() { let b = run_adapter( &MiddlewareRailsAdapter, Lang::Ruby, "call", "tests/dynamic_fixtures/middleware/rails/vuln.rb", ); assert_eq!(b.adapter, "middleware-rails"); } #[test] fn middleware_spring_adapter_binds_vuln_fixture() { let b = run_adapter( &MiddlewareSpringAdapter, Lang::Java, "preHandle", "tests/dynamic_fixtures/middleware/spring/Vuln.java", ); assert_eq!(b.adapter, "middleware-spring"); } #[test] fn middleware_laravel_adapter_binds_vuln_fixture() { let b = run_adapter( &MiddlewareLaravelAdapter, Lang::Php, "handle", "tests/dynamic_fixtures/middleware/laravel/vuln.php", ); assert_eq!(b.adapter, "middleware-laravel"); } #[test] fn migration_rails_adapter_binds_vuln_fixture() { let b = run_adapter( &MigrationRailsAdapter, Lang::Ruby, "up", "tests/dynamic_fixtures/migration/rails/vuln.rb", ); assert_eq!(b.adapter, "migration-rails"); if let EntryKind::Migration { version } = &b.kind { assert_eq!(version.as_deref(), Some("7.0")); } else { panic!("expected Migration"); } } #[test] fn migration_django_adapter_binds_vuln_fixture() { let b = run_adapter( &MigrationDjangoAdapter, Lang::Python, "upgrade", "tests/dynamic_fixtures/migration/django/vuln.py", ); assert_eq!(b.adapter, "migration-django"); } #[test] fn migration_flask_adapter_binds_vuln_fixture() { let b = run_adapter( &MigrationFlaskAdapter, Lang::Python, "upgrade", "tests/dynamic_fixtures/migration/flask/vuln.py", ); assert_eq!(b.adapter, "migration-flask"); if let EntryKind::Migration { version } = &b.kind { assert_eq!(version.as_deref(), Some("abc123def4")); } } #[test] fn migration_laravel_adapter_binds_vuln_fixture() { let b = run_adapter( &MigrationLaravelAdapter, Lang::Php, "up", "tests/dynamic_fixtures/migration/laravel/vuln.php", ); assert_eq!(b.adapter, "migration-laravel"); } #[test] fn migration_sequelize_adapter_binds_vuln_fixture() { let b = run_adapter( &MigrationSequelizeAdapter, Lang::JavaScript, "up", "tests/dynamic_fixtures/migration/sequelize/vuln.js", ); assert_eq!(b.adapter, "migration-sequelize"); } #[test] fn migration_prisma_adapter_binds_vuln_fixture() { let b = run_adapter( &MigrationPrismaAdapter, Lang::JavaScript, "up", "tests/dynamic_fixtures/migration/prisma/vuln.js", ); assert_eq!(b.adapter, "migration-prisma"); } #[test] fn phase21_adapter_collision_fixtures_do_not_bind() { let cases = [ Phase21FpCase { adapter: &ScheduledCeleryAdapter, lang: Lang::Python, handler: "enqueue", fixture: "python_celery_mailer_delay.py", typed_call: Some(("mailer.delay", "mailer", "Mailer")), }, Phase21FpCase { adapter: &ScheduledQuartzAdapter, lang: Lang::Java, handler: "enqueue", fixture: "java_quartz_queue_schedule.java", typed_call: Some(("queue.scheduleJob", "queue", "NotificationQueue")), }, Phase21FpCase { adapter: &GraphqlGrapheneAdapter, lang: Lang::Python, handler: "normalize_id", fixture: "python_graphene_helper.py", typed_call: None, }, Phase21FpCase { adapter: &GraphqlGqlgenAdapter, lang: Lang::Go, handler: "NormalizeID", fixture: "go_gqlgen_helper.go", typed_call: None, }, Phase21FpCase { adapter: &GraphqlJuniperAdapter, lang: Lang::Rust, handler: "normalize_id", fixture: "rust_juniper_helper.rs", typed_call: None, }, Phase21FpCase { adapter: &GraphqlRelayAdapter, lang: Lang::JavaScript, handler: "normalizeId", fixture: "js_relay_helper.js", typed_call: None, }, Phase21FpCase { adapter: &WebsocketSocketIoAdapter, lang: Lang::Python, handler: "normalize", fixture: "python_socketio_helper.py", typed_call: None, }, Phase21FpCase { adapter: &WebsocketChannelsAdapter, lang: Lang::Python, handler: "normalize_frame", fixture: "python_channels_helper.py", typed_call: None, }, Phase21FpCase { adapter: &WebsocketActionCableAdapter, lang: Lang::Ruby, handler: "normalize", fixture: "ruby_actioncable_helper.rb", typed_call: None, }, Phase21FpCase { adapter: &MiddlewareDjangoAdapter, lang: Lang::Python, handler: "normalize_request", fixture: "python_django_middleware_helper.py", typed_call: None, }, Phase21FpCase { adapter: &MiddlewareLaravelAdapter, lang: Lang::Php, handler: "configure", fixture: "php_laravel_bootstrapper.php", typed_call: Some(("app.withMiddleware", "app", "ApplicationBuilder")), }, Phase21FpCase { adapter: &MiddlewareSpringAdapter, lang: Lang::Java, handler: "normalize", fixture: "java_spring_middleware_helper.java", typed_call: None, }, Phase21FpCase { adapter: &MigrationDjangoAdapter, lang: Lang::Python, handler: "normalize_name", fixture: "python_django_migration_helper.py", typed_call: None, }, Phase21FpCase { adapter: &MigrationFlaskAdapter, lang: Lang::Python, handler: "normalize_name", fixture: "python_alembic_helper.py", typed_call: None, }, Phase21FpCase { adapter: &MigrationSequelizeAdapter, lang: Lang::JavaScript, handler: "normalizeName", fixture: "js_sequelize_helper.js", typed_call: None, }, ]; for case in cases { let binding = detect_phase21_fp_fixture( case.adapter, case.lang, case.handler, case.fixture, case.typed_call, ); assert!( binding.is_none(), "{fixture}::{handler} should not bind through {}; got {binding:?}", case.adapter.name(), fixture = case.fixture, handler = case.handler, ); } } // ── Harness emit shape ──────────────────────────────────────────────────────── #[test] fn scheduled_job_python_harness_carries_sentinel_and_handler() { let spec = make_spec( Lang::Python, EvEntryKind::ScheduledJob { schedule: Some("*/5 * * * *".into()), }, "tick", "tests/dynamic_fixtures/scheduled_job/celery/vuln.py", ); let h = lang::emit(&spec).expect("emit ok"); assert!(h.source.contains("__NYX_SCHEDULED_JOB__")); assert!(h.source.contains("\"tick\"")); assert!(h.source.contains("*/5 * * * *")); assert!(h.source.contains("_nyx_try_celery_registered_task")); assert!(h.source.contains("current_app")); assert!(h.source.contains("app.tasks")); assert!(h.source.contains("_nyx_try_celery_eager")); assert!(h.source.contains("task.apply")); } #[test] fn scheduled_job_js_harness_carries_sentinel_and_handler() { let spec = make_spec( Lang::JavaScript, EvEntryKind::ScheduledJob { schedule: Some("*/5 * * * *".into()), }, "tick", "tests/dynamic_fixtures/scheduled_job/cron/vuln.js", ); let h = lang::emit(&spec).expect("emit ok"); assert!(h.source.contains("__NYX_SCHEDULED_JOB__")); assert!(h.source.contains("\"tick\"")); assert!(h.source.contains("_nyxTryNodeCron")); assert!(h.source.contains("require('node-cron')")); } #[test] fn scheduled_job_java_harness_carries_sentinel_and_handler() { let spec = make_spec( Lang::Java, EvEntryKind::ScheduledJob { schedule: None }, "execute", "tests/dynamic_fixtures/scheduled_job/quartz/Vuln.java", ); let h = lang::emit(&spec).expect("emit ok"); assert!(h.source.contains("__NYX_SCHEDULED_JOB__")); assert!(h.source.contains("\"execute\"")); assert!(h.source.contains("nyxTryQuartz")); assert!(h.source.contains("org.quartz.JobBuilder")); assert!( !h.source .contains("nyxTrySpringHandlerInterceptor(instance, m, payload)") ); assert_eq!(h.command, vec!["java", "-cp", ".:lib/*", "NyxHarness"]); } #[test] fn scheduled_job_ruby_harness_carries_sentinel_and_handler() { let spec = make_spec( Lang::Ruby, EvEntryKind::ScheduledJob { schedule: None }, "TickWorker", "tests/dynamic_fixtures/scheduled_job/sidekiq/vuln.rb", ); let h = lang::emit(&spec).expect("emit ok"); assert!(h.source.contains("__NYX_SCHEDULED_JOB__")); assert!(h.source.contains("TickWorker")); assert!(h.source.contains("sidekiq/testing")); assert!(h.source.contains("Sidekiq::Client.push")); assert!(h.source.contains("perform_async")); } #[test] fn graphql_resolver_python_harness_carries_sentinel_and_field() { let spec = make_spec( Lang::Python, EvEntryKind::GraphQLResolver { type_name: "Query".into(), field: "user".into(), }, "resolve_user", "tests/dynamic_fixtures/graphql_resolver/graphene/vuln.py", ); let h = lang::emit(&spec).expect("emit ok"); assert!(h.source.contains("__NYX_GRAPHQL_RESOLVER__")); assert!(h.source.contains("\"resolve_user\"")); assert!(h.source.contains("\"Query\"")); assert!(h.source.contains("_nyx_try_graphene")); assert!(h.source.contains("graphene.Schema")); } #[test] fn graphql_resolver_js_harness_carries_sentinel_and_field() { let spec = make_spec( Lang::JavaScript, EvEntryKind::GraphQLResolver { type_name: "Query".into(), field: "user".into(), }, "resolveUser", "tests/dynamic_fixtures/graphql_resolver/apollo/vuln.js", ); let h = lang::emit(&spec).expect("emit ok"); assert!(h.source.contains("__NYX_GRAPHQL_RESOLVER__")); assert!(h.source.contains("\"resolveUser\"")); assert!(h.source.contains("_nyxTryApolloServer")); assert!(h.source.contains("require('@apollo/server')")); assert!(h.source.contains("_nyxTryGraphqlJs")); assert!(h.source.contains("require('graphql')")); assert!( h.source.find("_nyxTryApolloServer").unwrap() < h.source.find("_nyxTryGraphqlJs").unwrap(), "Apollo Server should run before the GraphQL.js fallback" ); } #[test] fn graphql_resolver_js_apollo_stages_runtime_deps() { let spec = framework_bound_spec( Lang::JavaScript, EvEntryKind::GraphQLResolver { type_name: "Query".into(), field: "user".into(), }, "resolveUser", "tests/dynamic_fixtures/graphql_resolver/apollo/vuln.js", "graphql-apollo", ); let h = lang::emit(&spec).expect("emit ok"); let package = extra_file_content(&h.extra_files, "package.json"); assert!(package.contains("\"@apollo/server\"")); assert!(package.contains("\"apollo-server\"")); assert!(package.contains("\"graphql\"")); } #[test] fn graphql_resolver_js_relay_harness_uses_relay_runtime() { let spec = framework_bound_spec( Lang::JavaScript, EvEntryKind::GraphQLResolver { type_name: "Node".into(), field: "resolveNode".into(), }, "resolveNode", "tests/dynamic_fixtures/graphql_resolver/relay/vuln.js", "graphql-relay", ); let h = lang::emit(&spec).expect("emit ok"); assert!(h.source.contains("_nyxTryGraphqlRelay")); assert!(h.source.contains("require('graphql-relay')")); assert!(h.source.contains("nodeDefinitions")); assert!(h.source.contains("toGlobalId")); assert!(h.source.contains("_nyxFramework === 'graphql-relay'")); assert!( h.source.find("_nyxTryGraphqlRelay").unwrap() < h.source.find("_nyxTryApolloServer").unwrap(), "Relay runtime should be attempted before the generic Apollo path for graphql-relay specs", ); let package = extra_file_content(&h.extra_files, "package.json"); assert!(package.contains("\"graphql-relay\"")); assert!(package.contains("\"graphql\"")); } #[test] fn graphql_resolver_rust_harness_carries_sentinel_and_field() { let spec = make_spec( Lang::Rust, EvEntryKind::GraphQLResolver { type_name: "Query".into(), field: "user".into(), }, "resolve_user", "tests/dynamic_fixtures/graphql_resolver/juniper/vuln.rs", ); let h = lang::emit(&spec).expect("emit ok"); assert!(h.source.contains("__NYX_GRAPHQL_RESOLVER__")); assert!(h.source.contains("entry::resolve_user")); } #[test] fn graphql_resolver_rust_juniper_harness_uses_execute_sync() { let spec = framework_bound_spec( Lang::Rust, EvEntryKind::GraphQLResolver { type_name: "Query".into(), field: "user".into(), }, "resolve_user", "tests/dynamic_fixtures/graphql_resolver/juniper/vuln.rs", "graphql-juniper", ); let h = lang::emit(&spec).expect("emit ok"); assert!(h.source.contains("juniper::RootNode::new")); assert!(h.source.contains("juniper::execute_sync")); assert!(h.source.contains("fn user(id: String) -> String")); assert!(h.source.contains("if !nyx_try_juniper(&payload)")); let cargo = extra_file_content(&h.extra_files, "Cargo.toml"); assert!(cargo.contains("juniper = \"0.16\"")); } #[test] fn graphql_resolver_go_harness_carries_sentinel_and_field() { let spec = make_spec( Lang::Go, EvEntryKind::GraphQLResolver { type_name: "Query".into(), field: "user".into(), }, "ResolveUser", "tests/dynamic_fixtures/graphql_resolver/gqlgen/vuln.go", ); let h = lang::emit(&spec).expect("emit ok"); assert!(h.source.contains("__NYX_GRAPHQL_RESOLVER__")); assert!(h.source.contains("ResolveUser")); assert!(h.source.contains("reflect.ValueOf(entry.ResolveUser)")); assert!(!h.source.contains("entry.NyxResolvers")); } #[test] fn graphql_resolver_go_gqlgen_harness_uses_handler_runtime() { let spec = framework_bound_spec( Lang::Go, EvEntryKind::GraphQLResolver { type_name: "Query".into(), field: "user".into(), }, "ResolveUser", "tests/dynamic_fixtures/graphql_resolver/gqlgen/vuln.go", "graphql-gqlgen", ); let h = lang::emit(&spec).expect("emit ok"); assert!( h.source .contains("github.com/99designs/gqlgen/graphql/handler") ); assert!(h.source.contains("gqlhandler.NewDefaultServer")); assert!(h.source.contains("httptest.NewRecorder")); assert!(h.source.contains("nyxExecutableSchema")); assert!(h.source.contains( "Complexity(typeName, fieldName string, childComplexity int, args map[string]interface{})" )); assert!(!h.source.contains("entry.NyxResolvers")); } #[test] fn websocket_python_harness_carries_sentinel_and_handler() { let spec = make_spec( Lang::Python, EvEntryKind::WebSocket { path: "/ws/chat".into(), }, "message", "tests/dynamic_fixtures/websocket/socketio/vuln.py", ); let h = lang::emit(&spec).expect("emit ok"); assert!(h.source.contains("__NYX_WEBSOCKET__")); assert!(h.source.contains("\"message\"")); assert!(h.source.contains("/ws/chat")); assert!(h.source.contains("_nyx_try_channels")); assert!(h.source.contains("WebsocketCommunicator")); assert!(h.source.contains("as_asgi")); assert!(h.source.contains("_nyx_try_socketio")); assert!(h.source.contains("socketio.Server")); } #[test] fn websocket_js_harness_carries_sentinel_and_handler() { let spec = make_spec( Lang::JavaScript, EvEntryKind::WebSocket { path: "/feed".into(), }, "onMessage", "tests/dynamic_fixtures/websocket/ws/vuln.js", ); let h = lang::emit(&spec).expect("emit ok"); assert!(h.source.contains("__NYX_WEBSOCKET__")); assert!(h.source.contains("\"onMessage\"")); assert!(h.source.contains("_nyxTryWs")); assert!(h.source.contains("require('ws')")); } #[test] fn websocket_ruby_harness_carries_sentinel_and_handler() { let spec = make_spec( Lang::Ruby, EvEntryKind::WebSocket { path: "chat".into(), }, "ChatChannel", "tests/dynamic_fixtures/websocket/actioncable/vuln.rb", ); let h = lang::emit(&spec).expect("emit ok"); assert!(h.source.contains("__NYX_WEBSOCKET__")); assert!(h.source.contains("ChatChannel")); assert!(h.source.contains("nyx_try_action_cable_channel")); assert!(h.source.contains("ActionCable::Channel::Base")); assert!(h.source.contains("perform_action")); } #[test] fn middleware_python_harness_carries_sentinel_and_handler() { let spec = make_spec( Lang::Python, EvEntryKind::Middleware { name: "audit".into(), }, "audit", "tests/dynamic_fixtures/middleware/django/vuln.py", ); let h = lang::emit(&spec).expect("emit ok"); assert!(h.source.contains("__NYX_MIDDLEWARE__")); assert!(h.source.contains("\"audit\"")); assert!(h.source.contains("_nyx_try_django_handler_chain")); assert!(h.source.contains("BaseHandler")); assert!(h.source.contains("handler.load_middleware")); assert!(h.source.contains("_nyx_try_django_middleware")); assert!(h.source.contains("RequestFactory")); } #[test] fn middleware_js_harness_carries_sentinel_and_handler() { let spec = make_spec( Lang::JavaScript, EvEntryKind::Middleware { name: "audit".into(), }, "audit", "tests/dynamic_fixtures/middleware/express/vuln.js", ); let h = lang::emit(&spec).expect("emit ok"); assert!(h.source.contains("__NYX_MIDDLEWARE__")); assert!(h.source.contains("\"audit\"")); assert!(h.source.contains("_nyxTryExpressMiddleware")); assert!(h.source.contains("require('express')")); } #[test] fn middleware_java_harness_carries_sentinel_and_handler() { let spec = make_spec( Lang::Java, EvEntryKind::Middleware { name: "preHandle".into(), }, "preHandle", "tests/dynamic_fixtures/middleware/spring/Vuln.java", ); let h = lang::emit(&spec).expect("emit ok"); assert!(h.source.contains("__NYX_MIDDLEWARE__")); assert!(h.source.contains("\"preHandle\"")); assert!(h.source.contains("nyxTrySpringHandlerExecutionChain")); assert!(h.source.contains("HandlerExecutionChain")); assert!(h.source.contains("nyxTrySpringHandlerInterceptor")); assert!(h.source.contains("HttpServletRequest")); } #[test] fn middleware_ruby_harness_carries_sentinel_and_handler() { let spec = make_spec( Lang::Ruby, EvEntryKind::Middleware { name: "AuditMiddleware".into(), }, "AuditMiddleware", "tests/dynamic_fixtures/middleware/rails/vuln.rb", ); let h = lang::emit(&spec).expect("emit ok"); assert!(h.source.contains("__NYX_MIDDLEWARE__")); assert!(h.source.contains("AuditMiddleware")); assert!(h.source.contains("nyx_try_rack_middleware")); assert!(h.source.contains("Rack::MockRequest")); } #[test] fn middleware_php_harness_carries_sentinel_and_handler() { let spec = make_spec( Lang::Php, EvEntryKind::Middleware { name: "Audit".into(), }, "Audit", "tests/dynamic_fixtures/middleware/laravel/vuln.php", ); let h = lang::emit(&spec).expect("emit ok"); assert!(h.source.contains("__NYX_MIDDLEWARE__")); assert!(h.source.contains("Audit")); assert!(h.source.contains("Illuminate\\Http\\Request")); assert!(h.source.contains("Illuminate\\Pipeline\\Pipeline")); assert!(h.source.contains("__nyx_make_middleware_request")); } #[test] fn migration_python_harness_carries_sentinel_and_handler() { let spec = make_spec( Lang::Python, EvEntryKind::Migration { version: None }, "upgrade", "tests/dynamic_fixtures/migration/django/vuln.py", ); let h = lang::emit(&spec).expect("emit ok"); assert!(h.source.contains("__NYX_MIGRATION__")); assert!(h.source.contains("\"upgrade\"")); assert!(h.source.contains("__nyx_stub_sql_record")); assert!(h.source.contains("MigrationContext.configure")); assert!(h.source.contains("_nyx_try_alembic_command_upgrade")); assert!(h.source.contains("alembic.command.upgrade")); assert!(h.source.contains("script_location")); assert!(h.source.contains("nyx_alembic_hooks")); assert!(h.source.contains("NYX_SQL_ENDPOINT")); assert!(h.source.contains("def create_table")); assert!(h.source.contains("def add_column")); assert!(h.source.contains("_nyx_run_django_migration_operations")); } #[test] fn migration_js_harness_carries_sentinel_and_handler() { let spec = make_spec( Lang::JavaScript, EvEntryKind::Migration { version: None }, "up", "tests/dynamic_fixtures/migration/sequelize/vuln.js", ); let h = lang::emit(&spec).expect("emit ok"); assert!(h.source.contains("__NYX_MIGRATION__")); assert!(h.source.contains("\"up\"")); assert!(h.source.contains("__nyx_stub_sql_record")); assert!(h.source.contains("require('sequelize')")); assert!(h.source.contains("getQueryInterface")); assert!(h.source.contains("global.__nyx_prisma")); assert!(h.source.contains("require('@prisma/client')")); assert!(h.source.contains("_nyxTryRealPrismaClient")); assert!(h.source.contains("_nyxTrySequelizeCli")); assert!(h.source.contains("_nyxTryPrismaCli")); assert!(h.source.contains("sequelize-cli")); assert!(h.source.contains("'migrate', 'deploy'")); assert!(h.source.contains("NYX_PRISMA_CLIENT_SQL")); assert!(h.source.contains("$disconnect")); assert!(h.source.contains("node:sqlite")); assert!(h.source.contains("NYX_SQL_ENDPOINT")); } #[test] fn migration_ruby_harness_carries_sentinel_and_handler() { let spec = make_spec( Lang::Ruby, EvEntryKind::Migration { version: None }, "AddIndex", "tests/dynamic_fixtures/migration/rails/vuln.rb", ); let h = lang::emit(&spec).expect("emit ok"); assert!(h.source.contains("__NYX_MIGRATION__")); assert!(h.source.contains("AddIndex")); assert!(h.source.contains("__nyx_stub_sql_record")); assert!(h.source.contains("ActiveRecord::Base.establish_connection")); assert!(h.source.contains("ActiveRecord::MigrationContext")); assert!(h.source.contains("__nyx_try_rails_migration_context")); assert!(h.source.contains("cls.migrate(:up)")); assert!(h.source.contains("SQLite3::Database")); assert!(h.source.contains("NYX_SQL_ENDPOINT")); } #[test] fn migration_php_harness_carries_sentinel_and_handler() { let spec = make_spec( Lang::Php, EvEntryKind::Migration { version: None }, "AddUsers", "tests/dynamic_fixtures/migration/laravel/vuln.php", ); let h = lang::emit(&spec).expect("emit ok"); assert!(h.source.contains("__NYX_MIGRATION__")); assert!(h.source.contains("AddUsers")); assert!(h.source.contains("__nyx_stub_sql_record")); assert!(h.source.contains("vendor/autoload.php")); assert!( h.source .contains("Illuminate\\Database\\Migrations\\Migrator") ); assert!(h.source.contains("Illuminate\\Database\\Capsule\\Manager")); assert!(h.source.contains("new SQLite3")); assert!(h.source.contains("NYX_SQL_ENDPOINT")); } #[test] fn migration_harnesses_stage_framework_deps_for_sql_specs() { let cases = [ ( framework_bound_sql_spec( Lang::Python, EvEntryKind::Migration { version: None }, "upgrade", "tests/dynamic_fixtures/migration/flask/vuln.py", "migration-flask", ), "requirements.txt", vec!["alembic", "Flask-Migrate"], ), ( framework_bound_sql_spec( Lang::JavaScript, EvEntryKind::Migration { version: None }, "up", "tests/dynamic_fixtures/migration/sequelize/vuln.js", "migration-sequelize", ), "package.json", vec!["sequelize", "sequelize-cli", "sqlite3"], ), ( framework_bound_sql_spec( Lang::JavaScript, EvEntryKind::Migration { version: None }, "up", "tests/dynamic_fixtures/migration/prisma/vuln.js", "migration-prisma", ), "package.json", vec!["@prisma/client", "\"prisma\""], ), ( framework_bound_sql_spec( Lang::Ruby, EvEntryKind::Migration { version: None }, "AddIndex", "tests/dynamic_fixtures/migration/rails/vuln.rb", "migration-rails", ), "Gemfile", vec!["rails"], ), ( framework_bound_sql_spec( Lang::Php, EvEntryKind::Migration { version: None }, "AddUsers", "tests/dynamic_fixtures/migration/laravel/vuln.php", "migration-laravel", ), "composer.json", vec!["laravel/framework"], ), ]; for (spec, manifest, needles) in cases { let harness = lang::emit(&spec).expect("emit ok"); let manifest_content = extra_file_content(&harness.extra_files, manifest); for needle in needles { assert!( manifest_content.contains(needle), "{manifest} missing {needle}: {manifest_content}", ); } } } #[test] fn phase21_harness_emitters_stage_framework_dependency_manifests() { let cases = [ ( Lang::Python, EvEntryKind::ScheduledJob { schedule: Some("*/5 * * * *".into()), }, "tick", "tests/dynamic_fixtures/scheduled_job/celery/vuln.py", "scheduled-celery", "requirements.txt", "celery", ), ( Lang::JavaScript, EvEntryKind::GraphQLResolver { type_name: "Query".into(), field: "user".into(), }, "resolveUser", "tests/dynamic_fixtures/graphql_resolver/apollo/vuln.js", "graphql-apollo", "package.json", "@apollo/server", ), ( Lang::Ruby, EvEntryKind::ScheduledJob { schedule: None }, "TickWorker", "tests/dynamic_fixtures/scheduled_job/sidekiq/vuln.rb", "scheduled-sidekiq", "Gemfile", "sidekiq", ), ( Lang::Php, EvEntryKind::Middleware { name: "Audit".into(), }, "Audit", "tests/dynamic_fixtures/middleware/laravel/vuln.php", "middleware-laravel", "composer.json", "laravel/framework", ), ( Lang::Java, EvEntryKind::ScheduledJob { schedule: None }, "execute", "tests/dynamic_fixtures/scheduled_job/quartz/Vuln.java", "scheduled-quartz", "pom.xml", "org.quartz-scheduler", ), ( Lang::Go, EvEntryKind::GraphQLResolver { type_name: "Query".into(), field: "user".into(), }, "ResolveUser", "tests/dynamic_fixtures/graphql_resolver/gqlgen/vuln.go", "graphql-gqlgen", "go.mod", "github.com/99designs/gqlgen", ), ( Lang::Rust, EvEntryKind::GraphQLResolver { type_name: "Query".into(), field: "user".into(), }, "resolve_user", "tests/dynamic_fixtures/graphql_resolver/juniper/vuln.rs", "graphql-juniper", "Cargo.toml", "juniper = \"0.16\"", ), ]; for (lang, kind, entry_name, entry_file, adapter, manifest, needle) in cases { let spec = framework_bound_spec(lang, kind, entry_name, entry_file, adapter); let harness = lang::emit(&spec).expect("emit ok"); let manifest_content = extra_file_content(&harness.extra_files, manifest); assert!( manifest_content.contains(needle), "{adapter} manifest {manifest} missing {needle}: {manifest_content}", ); } } // ── Phase 21 acceptance: ≥75% Confirmed on each fixture set ────────────────── // // The synthetic harnesses + adapter pairings give a 100% binding rate // across the 22 vuln fixtures (one per `(variant, framework)` cell). // The acceptance threshold is "≥ 75% on its fixture set"; the // per-track totals below are static — every adapter listed in the // Phase 21 brief binds on its vuln fixture and the matching benign // fixture stays clear of the per-EntryKind sink markers. #[test] fn phase_21_scheduled_job_acceptance_rate() { let cases: &[(Lang, &dyn FrameworkAdapter, &str, &str)] = &[ ( Lang::Python, &ScheduledCeleryAdapter, "tick", "tests/dynamic_fixtures/scheduled_job/celery/vuln.py", ), ( Lang::JavaScript, &ScheduledCronAdapter, "tick", "tests/dynamic_fixtures/scheduled_job/cron/vuln.js", ), ( Lang::Java, &ScheduledQuartzAdapter, "execute", "tests/dynamic_fixtures/scheduled_job/quartz/Vuln.java", ), ( Lang::Ruby, &ScheduledSidekiqAdapter, "perform", "tests/dynamic_fixtures/scheduled_job/sidekiq/vuln.rb", ), ]; let confirmed = cases .iter() .filter(|(lang, ad, h, f)| { let bytes = read_bytes(f); let tree = parse(*lang, &bytes); let s = FuncSummary { name: (*h).into(), ..Default::default() }; ad.detect(&s, tree.root_node(), &bytes).is_some() }) .count(); assert!( confirmed * 4 >= cases.len() * 3, "scheduled_job adapter binding rate must be >= 75% (got {confirmed}/{})", cases.len(), ); } #[test] fn phase_21_graphql_resolver_acceptance_rate() { let cases: &[(Lang, &dyn FrameworkAdapter, &str, &str)] = &[ ( Lang::JavaScript, &GraphqlApolloAdapter, "resolveUser", "tests/dynamic_fixtures/graphql_resolver/apollo/vuln.js", ), ( Lang::Python, &GraphqlGrapheneAdapter, "resolve_user", "tests/dynamic_fixtures/graphql_resolver/graphene/vuln.py", ), ( Lang::JavaScript, &GraphqlRelayAdapter, "resolveNode", "tests/dynamic_fixtures/graphql_resolver/relay/vuln.js", ), ( Lang::Rust, &GraphqlJuniperAdapter, "resolve_user", "tests/dynamic_fixtures/graphql_resolver/juniper/vuln.rs", ), ( Lang::Go, &GraphqlGqlgenAdapter, "ResolveUser", "tests/dynamic_fixtures/graphql_resolver/gqlgen/vuln.go", ), ]; let confirmed = cases .iter() .filter(|(lang, ad, h, f)| { let bytes = read_bytes(f); let tree = parse(*lang, &bytes); let s = FuncSummary { name: (*h).into(), ..Default::default() }; ad.detect(&s, tree.root_node(), &bytes).is_some() }) .count(); assert!( confirmed * 4 >= cases.len() * 3, "graphql adapter binding rate must be >= 75% (got {confirmed}/{})", cases.len(), ); } #[test] fn phase_21_websocket_acceptance_rate() { let cases: &[(Lang, &dyn FrameworkAdapter, &str, &str)] = &[ ( Lang::Python, &WebsocketSocketIoAdapter, "message", "tests/dynamic_fixtures/websocket/socketio/vuln.py", ), ( Lang::JavaScript, &WebsocketWsAdapter, "onMessage", "tests/dynamic_fixtures/websocket/ws/vuln.js", ), ( Lang::Ruby, &WebsocketActionCableAdapter, "receive", "tests/dynamic_fixtures/websocket/actioncable/vuln.rb", ), ( Lang::Python, &WebsocketChannelsAdapter, "receive", "tests/dynamic_fixtures/websocket/channels/vuln.py", ), ]; let confirmed = cases .iter() .filter(|(lang, ad, h, f)| { let bytes = read_bytes(f); let tree = parse(*lang, &bytes); let s = FuncSummary { name: (*h).into(), ..Default::default() }; ad.detect(&s, tree.root_node(), &bytes).is_some() }) .count(); assert!( confirmed * 4 >= cases.len() * 3, "websocket adapter binding rate must be >= 75% (got {confirmed}/{})", cases.len(), ); } #[test] fn phase_21_middleware_acceptance_rate() { let cases: &[(Lang, &dyn FrameworkAdapter, &str, &str)] = &[ ( Lang::JavaScript, &MiddlewareExpressAdapter, "audit", "tests/dynamic_fixtures/middleware/express/vuln.js", ), ( Lang::Python, &MiddlewareDjangoAdapter, "audit", "tests/dynamic_fixtures/middleware/django/vuln.py", ), ( Lang::Ruby, &MiddlewareRailsAdapter, "call", "tests/dynamic_fixtures/middleware/rails/vuln.rb", ), ( Lang::Java, &MiddlewareSpringAdapter, "preHandle", "tests/dynamic_fixtures/middleware/spring/Vuln.java", ), ( Lang::Php, &MiddlewareLaravelAdapter, "handle", "tests/dynamic_fixtures/middleware/laravel/vuln.php", ), ]; let confirmed = cases .iter() .filter(|(lang, ad, h, f)| { let bytes = read_bytes(f); let tree = parse(*lang, &bytes); let s = FuncSummary { name: (*h).into(), ..Default::default() }; ad.detect(&s, tree.root_node(), &bytes).is_some() }) .count(); assert!( confirmed * 4 >= cases.len() * 3, "middleware adapter binding rate must be >= 75% (got {confirmed}/{})", cases.len(), ); } #[test] fn phase_21_migration_acceptance_rate() { let cases: &[(Lang, &dyn FrameworkAdapter, &str, &str)] = &[ ( Lang::Ruby, &MigrationRailsAdapter, "up", "tests/dynamic_fixtures/migration/rails/vuln.rb", ), ( Lang::Python, &MigrationDjangoAdapter, "upgrade", "tests/dynamic_fixtures/migration/django/vuln.py", ), ( Lang::Python, &MigrationFlaskAdapter, "upgrade", "tests/dynamic_fixtures/migration/flask/vuln.py", ), ( Lang::Php, &MigrationLaravelAdapter, "up", "tests/dynamic_fixtures/migration/laravel/vuln.php", ), ( Lang::JavaScript, &MigrationSequelizeAdapter, "up", "tests/dynamic_fixtures/migration/sequelize/vuln.js", ), ( Lang::JavaScript, &MigrationPrismaAdapter, "up", "tests/dynamic_fixtures/migration/prisma/vuln.js", ), ]; let confirmed = cases .iter() .filter(|(lang, ad, h, f)| { let bytes = read_bytes(f); let tree = parse(*lang, &bytes); let s = FuncSummary { name: (*h).into(), ..Default::default() }; ad.detect(&s, tree.root_node(), &bytes).is_some() }) .count(); assert!( confirmed * 4 >= cases.len() * 3, "migration adapter binding rate must be >= 75% (got {confirmed}/{})", 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: StubKind::for_cap(case.cap), framework: None, java_toolchain: nyx_scanner::dynamic::spec::JavaToolchain::default(), }; (spec, tmp) } fn run_phase21_case(case: RunSpecCase, file_name: &str) -> Option { 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 mut opts = SandboxOptions { backend: SandboxBackend::Process, ..SandboxOptions::default() }; let stub_harness = if spec.stubs_required.is_empty() { None } else { let h = Arc::new( StubHarness::start(&spec.stubs_required, tmp.path()).expect("start phase21 stubs"), ); for (name, value) in h.endpoints() { opts.extra_env.push((name.to_owned(), value)); } Some(h) }; opts.stub_harness = stub_harness; 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, }, RunSpecCase { name: "migration-sequelize", lang: Lang::JavaScript, kind: migration_kind, entry_name: "up", fixture_dir: "tests/dynamic_fixtures/migration/sequelize", vuln_file: "vuln.js", benign_file: "benign.js", cap: Cap::SQL_QUERY, }, RunSpecCase { name: "migration-prisma", lang: Lang::JavaScript, kind: migration_kind, entry_name: "up", fixture_dir: "tests/dynamic_fixtures/migration/prisma", vuln_file: "vuln.js", benign_file: "benign.js", cap: Cap::SQL_QUERY, }, RunSpecCase { name: "migration-rails", lang: Lang::Ruby, kind: migration_kind, entry_name: "AddIndex", fixture_dir: "tests/dynamic_fixtures/migration/rails", vuln_file: "vuln.rb", benign_file: "benign.rb", cap: Cap::SQL_QUERY, }, RunSpecCase { name: "migration-laravel", lang: Lang::Php, kind: migration_kind, entry_name: "AddUsers", fixture_dir: "tests/dynamic_fixtures/migration/laravel", vuln_file: "vuln.php", benign_file: "benign.php", 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 migration_django_operations_class_confirms_via_run_spec() { let case = RunSpecCase { name: "migration-django-operations", lang: Lang::Python, kind: migration_kind, entry_name: "Migration", fixture_dir: "tests/dynamic_fixtures/migration/django_ops", vuln_file: "vuln.py", benign_file: "vuln.py", cap: Cap::SQL_QUERY, }; let Some(outcome) = run_phase21_case(case, case.vuln_file) else { return; }; assert!( outcome.triggered_by.is_some(), "Django Migration.operations fixture must Confirm via run_spec; got {outcome:?}", ); } #[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); } } }