refactor(dynamic): enhance resolver detection for frameworks, refine SSA receiver validation, and expand test coverage

This commit is contained in:
elipeter 2026-05-24 14:34:39 -05:00
parent 3027c1afa7
commit f49211d788
38 changed files with 1198 additions and 137 deletions

View file

@ -0,0 +1,16 @@
{
"required_findings": [],
"forbidden_findings": [
{ "id_prefix": "taint-unsanitised-flow" }
],
"noise_budget": {
"max_total_findings": 0,
"max_high_findings": 0
},
"performance_expectations": {
"max_ms_no_index": 1000,
"max_ms_index_cold": 1500,
"max_ms_index_warm": 500,
"ci_mode": "lenient"
}
}

View file

@ -0,0 +1,19 @@
const { SQSClient } = require("@aws-sdk/client-sqs");
class MetricsPublisher {
send(event) {
return Promise.resolve({ ok: true, event });
}
}
const sqs = new SQSClient({});
const metrics = new MetricsPublisher();
function handler(event) {
return metrics.send({
type: "delivery_attempt",
requestId: event.requestId,
});
}
module.exports = { handler, sqs };

View file

@ -0,0 +1,16 @@
import boto3
sqs = boto3.client("sqs")
class AuditCache:
def process_message(self, envelope):
return {"stored": True, "id": envelope.get("id")}
cache = AuditCache()
def handler(envelope):
return cache.process_message(envelope)

View file

@ -0,0 +1,13 @@
import pika
class ReportWorker:
def process(self, report):
return {"status": "queued", "report_id": report.get("id")}
worker = ReportWorker()
def process(report):
return worker.process(report)

View file

@ -0,0 +1,16 @@
{
"required_findings": [],
"forbidden_findings": [
{ "id_prefix": "taint-unsanitised-flow" }
],
"noise_budget": {
"max_total_findings": 0,
"max_high_findings": 0
},
"performance_expectations": {
"max_ms_no_index": 1000,
"max_ms_index_cold": 1500,
"max_ms_index_warm": 500,
"ci_mode": "lenient"
}
}

View file

@ -0,0 +1,14 @@
package graph
import "context"
// import "github.com/99designs/gqlgen/graphql"
type queryResolver struct{}
func (r *queryResolver) User(ctx context.Context, id string) (string, error) {
return id, nil
}
func NormalizeID(id string) string {
return id
}

View file

@ -0,0 +1,15 @@
import org.quartz.Job;
import org.quartz.JobExecutionContext;
class TickJob implements Job {
public void execute(JobExecutionContext context) {}
public void enqueue(Object payload) {
NotificationQueue queue = new NotificationQueue();
queue.scheduleJob(payload);
}
}
class NotificationQueue {
void scheduleJob(Object payload) {}
}

View file

@ -0,0 +1,11 @@
import org.springframework.web.servlet.HandlerInterceptor;
class AuditInterceptor implements HandlerInterceptor {
public boolean preHandle(Object request, Object response, Object handler) {
return true;
}
public String normalize(String payload) {
return payload;
}
}

View file

@ -0,0 +1,11 @@
const { nodeDefinitions } = require('graphql-relay');
function resolveNode(globalId) {
return globalId;
}
function normalizeId(id) {
return String(id);
}
module.exports = { resolveNode, normalizeId, nodeDefinitions };

View file

@ -0,0 +1,15 @@
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('users', {});
},
async down(queryInterface, Sequelize) {
await queryInterface.dropTable('users');
},
};
function normalizeName(name) {
return String(name);
}
module.exports.normalizeName = normalizeName;

View file

@ -0,0 +1,9 @@
<?php
class Bootstrapper
{
public function configure($app)
{
return $app->withMiddleware([]);
}
}

View file

@ -0,0 +1,11 @@
from alembic import op
revision = "abc123def4"
def upgrade():
op.create_table("users")
def normalize_name(name):
return str(name)

View file

@ -0,0 +1,16 @@
from celery import shared_task
@shared_task
def tick(payload):
return payload
class Mailer:
def delay(self, payload):
return payload
def enqueue(payload):
mailer = Mailer()
return mailer.delay(payload)

View file

@ -0,0 +1,10 @@
from channels.generic.websocket import WebsocketConsumer
class ChatConsumer(WebsocketConsumer):
def receive(self, text_data=None, bytes_data=None):
return text_data
def normalize_frame(text_data):
return str(text_data)

View file

@ -0,0 +1,10 @@
from django.utils.deprecation import MiddlewareMixin
class AuditMiddleware(MiddlewareMixin):
def process_request(self, request):
return None
def normalize_request(request):
return request

View file

@ -0,0 +1,11 @@
from django.db import migrations
class Migration(migrations.Migration):
operations = [
migrations.CreateModel(name="User", fields=[]),
]
def normalize_name(name):
return str(name)

View file

@ -0,0 +1,12 @@
import graphene
class Query(graphene.ObjectType):
user = graphene.String()
def resolve_user(self, info, id):
return id
def normalize_id(raw):
return str(raw)

View file

@ -0,0 +1,12 @@
import socketio
sio = socketio.Server()
@sio.on("message")
def message(sid, data):
return data
def normalize(data):
return str(data)

View file

@ -0,0 +1,13 @@
class ChatChannel < ApplicationCable::Channel
def subscribed
stream_from "chat_room"
end
def receive(data)
data
end
def normalize(data)
data.to_s
end
end

View file

@ -0,0 +1,14 @@
use juniper::graphql_object;
pub struct Query;
#[graphql_object]
impl Query {
fn user(&self, id: String) -> String {
id
}
}
pub fn normalize_id(id: &str) -> String {
id.to_string()
}

View file

@ -926,6 +926,27 @@ fn fp_guard_framework_express_res_json() {
validate_expectations(&diags, &dir);
}
/// FP guard, broker-adapter receiver collisions: OSS-shaped handlers named
/// `handler` / `process` and a non-SQS `.send(...)` publisher must stay
/// ordinary helper code unless receiver facts prove the call is on a broker
/// runtime object.
#[test]
fn fp_guard_broker_adapter_receiver_collisions() {
let dir = fixture_path("fp_guards/broker_adapter_collisions");
let diags = scan_fixture_dir(&dir, AnalysisMode::Full);
validate_expectations(&diags, &dir);
}
/// FP guard, Phase 21 adapter collisions: framework-marked files can contain
/// ordinary helpers, controller bootstrappers, mailer queues, and migration
/// formatting functions that must not be promoted to dynamic entry kinds.
#[test]
fn fp_guard_phase21_adapter_collisions() {
let dir = fixture_path("fp_guards/phase21_adapter_collisions");
let diags = scan_fixture_dir(&dir, AnalysisMode::Full);
validate_expectations(&diags, &dir);
}
/// FP guard, FastAPI `dependencies=[Depends(requires_access_*)]`
/// route-level guard short-circuits `auth_check_covers_subject` so
/// the handler body's path-param ORM calls and row-variable method

View file

@ -17,11 +17,15 @@
mod common;
use nyx_scanner::dynamic::framework::registry::adapters_for;
use nyx_scanner::dynamic::framework::{FrameworkBinding, detect_binding};
use nyx_scanner::dynamic::framework::{
FrameworkBinding, detect_binding, detect_binding_with_context,
};
use nyx_scanner::dynamic::lang;
use nyx_scanner::dynamic::spec::{EntryKind, EntryKindTag, HarnessSpec, PayloadSlot};
use nyx_scanner::labels::Cap;
use nyx_scanner::summary::CalleeSite;
use nyx_scanner::summary::FuncSummary;
use nyx_scanner::summary::ssa_summary::SsaFuncSummary;
use nyx_scanner::symbol::Lang;
const SUPPORTED_LANGS: &[Lang] = &[
@ -215,6 +219,39 @@ fn detect_from_bytes(lang: Lang, bytes: &[u8], handler: &str) -> Option<Framewor
detect_binding(&summary, tree.root_node(), bytes, lang)
}
fn detect_collision_fixture_with_receiver(
lang: Lang,
fixture: &str,
handler: &str,
callee: &str,
receiver: &str,
receiver_ty: &str,
) -> Option<FrameworkBinding> {
let bytes = std::fs::read(
std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures/fp_guards/broker_adapter_collisions")
.join(fixture),
)
.expect("collision fixture exists");
let ts_lang = ts_language_for(lang);
let mut parser = tree_sitter::Parser::new();
parser.set_language(&ts_lang).unwrap();
let tree = parser.parse(&bytes, None).unwrap();
let mut summary = FuncSummary {
name: handler.into(),
..Default::default()
};
summary.callees.push(CalleeSite {
name: callee.to_owned(),
receiver: Some(receiver.to_owned()),
ordinal: 0,
..Default::default()
});
let mut ssa = SsaFuncSummary::default();
ssa.typed_call_receivers.push((0, receiver_ty.to_owned()));
detect_binding_with_context(&summary, Some(&ssa), tree.root_node(), &bytes, lang)
}
fn middleware_names(binding: &FrameworkBinding) -> Vec<String> {
binding
.middleware
@ -417,6 +454,51 @@ def on_message(ch, method, properties, body):\n validate_request(body)\n",
}
}
#[test]
fn phase20_broker_adapter_receiver_collisions_have_fixture_anchors() {
let cases: &[(Lang, &str, &str, &str, &str, &str)] = &[
(
Lang::Python,
"python_non_broker_handler.py",
"handler",
"cache.process_message",
"cache",
"AuditCache",
),
(
Lang::Python,
"python_non_rabbit_process.py",
"process",
"worker.process",
"worker",
"ReportWorker",
),
(
Lang::JavaScript,
"node_non_sqs_send.js",
"handler",
"metrics.send",
"metrics",
"MetricsPublisher",
),
];
for (lang, fixture, handler, callee, receiver, receiver_ty) in cases {
let binding = detect_collision_fixture_with_receiver(
*lang,
fixture,
handler,
callee,
receiver,
receiver_ty,
);
assert!(
binding.is_none(),
"{fixture} should not bind as a broker message handler; got {binding:?}",
);
}
}
#[test]
fn registry_slices_include_phase_20_adapters() {
let java_names: Vec<&'static str> = adapters_for(Lang::Java).iter().map(|a| a.name()).collect();

View file

@ -26,7 +26,8 @@ use nyx_scanner::dynamic::lang;
use nyx_scanner::dynamic::spec::{EntryKind, EntryKindTag, HarnessSpec, PayloadSlot};
use nyx_scanner::evidence::EntryKind as EvEntryKind;
use nyx_scanner::labels::Cap;
use nyx_scanner::summary::FuncSummary;
use nyx_scanner::summary::ssa_summary::SsaFuncSummary;
use nyx_scanner::summary::{CalleeSite, FuncSummary};
use nyx_scanner::symbol::Lang;
fn make_spec(lang: Lang, kind: EvEntryKind, entry_name: &str, entry_file: &str) -> HarnessSpec {
@ -91,6 +92,46 @@ fn run_adapter(
.unwrap_or_else(|| panic!("{} did not fire on {fixture}", adapter.name()))
}
fn detect_phase21_fp_fixture(
adapter: &dyn FrameworkAdapter,
lang: Lang,
handler: &str,
fixture: &str,
typed_call: Option<(&str, &str, &str)>,
) -> Option<FrameworkBinding> {
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]
@ -447,6 +488,134 @@ fn migration_prisma_adapter_binds_vuln_fixture() {
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]