mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-09 19:45:13 +02:00
[pitboss] phase 20: Track M.2 — MessageHandler end-to-end (Kafka / SQS / Pub-Sub / NATS / RabbitMQ)
This commit is contained in:
parent
fedc507e6a
commit
bd0135e423
45 changed files with 3227 additions and 25 deletions
115
src/dynamic/framework/adapters/kafka_java.rs
Normal file
115
src/dynamic/framework/adapters/kafka_java.rs
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
//! Phase 20 (Track M.2) — Java Kafka consumer adapter.
|
||||
//!
|
||||
//! Fires on Spring Kafka `@KafkaListener` annotations or
|
||||
//! `org.apache.kafka.clients.consumer.KafkaConsumer` references. Best-
|
||||
//! effort topic extraction reads the literal that follows `topics =
|
||||
//! "..."` / `topics = {"..."}` / `subscribe(Arrays.asList("..."))`.
|
||||
|
||||
use crate::dynamic::framework::{FrameworkAdapter, FrameworkBinding};
|
||||
use crate::evidence::EntryKind;
|
||||
use crate::summary::FuncSummary;
|
||||
use crate::symbol::Lang;
|
||||
|
||||
pub struct KafkaJavaAdapter;
|
||||
|
||||
const ADAPTER_NAME: &str = "kafka-java";
|
||||
|
||||
fn callee_is_kafka(name: &str) -> bool {
|
||||
let last = name.rsplit_once('.').map(|(_, s)| s).unwrap_or(name);
|
||||
matches!(
|
||||
last,
|
||||
"KafkaConsumer" | "subscribe" | "poll" | "onMessage" | "consume"
|
||||
)
|
||||
}
|
||||
|
||||
fn source_imports_kafka(file_bytes: &[u8]) -> bool {
|
||||
const NEEDLES: &[&[u8]] = &[
|
||||
b"org.apache.kafka",
|
||||
b"org.springframework.kafka",
|
||||
b"@KafkaListener",
|
||||
];
|
||||
NEEDLES
|
||||
.iter()
|
||||
.any(|n| file_bytes.windows(n.len()).any(|w| w == *n))
|
||||
}
|
||||
|
||||
fn extract_topic(file_bytes: &[u8]) -> String {
|
||||
let text = std::str::from_utf8(file_bytes).unwrap_or("");
|
||||
for needle in ["topics = \"", "topics=\"", "topics = {\"", "subscribe(Arrays.asList(\""] {
|
||||
if let Some(idx) = text.find(needle) {
|
||||
let after = &text[idx + needle.len()..];
|
||||
if let Some(end) = after.find('"') {
|
||||
return after[..end].to_owned();
|
||||
}
|
||||
}
|
||||
}
|
||||
String::new()
|
||||
}
|
||||
|
||||
impl FrameworkAdapter for KafkaJavaAdapter {
|
||||
fn name(&self) -> &'static str {
|
||||
ADAPTER_NAME
|
||||
}
|
||||
|
||||
fn lang(&self) -> Lang {
|
||||
Lang::Java
|
||||
}
|
||||
|
||||
fn detect(
|
||||
&self,
|
||||
summary: &FuncSummary,
|
||||
_ast: tree_sitter::Node<'_>,
|
||||
file_bytes: &[u8],
|
||||
) -> Option<FrameworkBinding> {
|
||||
let matches_call = super::any_callee_matches(summary, callee_is_kafka);
|
||||
let matches_source = source_imports_kafka(file_bytes);
|
||||
if matches_call || matches_source {
|
||||
Some(FrameworkBinding {
|
||||
adapter: ADAPTER_NAME.to_owned(),
|
||||
kind: EntryKind::MessageHandler {
|
||||
queue: extract_topic(file_bytes),
|
||||
message_schema: None,
|
||||
},
|
||||
route: None,
|
||||
request_params: Vec::new(),
|
||||
response_writer: None,
|
||||
middleware: Vec::new(),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn parse_java(src: &[u8]) -> tree_sitter::Tree {
|
||||
let mut parser = tree_sitter::Parser::new();
|
||||
let lang = tree_sitter::Language::from(tree_sitter_java::LANGUAGE);
|
||||
parser.set_language(&lang).unwrap();
|
||||
parser.parse(src, None).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fires_on_spring_kafka_listener() {
|
||||
let src: &[u8] = b"import org.springframework.kafka.annotation.KafkaListener;\n\
|
||||
public class Vuln {\n\
|
||||
@KafkaListener(topics = \"orders\")\n\
|
||||
public void onMessage(String body) {}\n\
|
||||
}\n";
|
||||
let tree = parse_java(src);
|
||||
let summary = FuncSummary {
|
||||
name: "onMessage".into(),
|
||||
..Default::default()
|
||||
};
|
||||
let binding = KafkaJavaAdapter
|
||||
.detect(&summary, tree.root_node(), src)
|
||||
.expect("@KafkaListener binds");
|
||||
assert!(matches!(binding.kind, EntryKind::MessageHandler { .. }));
|
||||
if let EntryKind::MessageHandler { queue, .. } = binding.kind {
|
||||
assert_eq!(queue, "orders");
|
||||
}
|
||||
}
|
||||
}
|
||||
136
src/dynamic/framework/adapters/kafka_python.rs
Normal file
136
src/dynamic/framework/adapters/kafka_python.rs
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
//! Phase 20 (Track M.2) — Python Kafka consumer adapter.
|
||||
//!
|
||||
//! Fires when the surrounding source imports the canonical Python
|
||||
//! Kafka clients (`kafka-python` or `confluent-kafka`) and the function
|
||||
//! body invokes a consumer-shaped callee. The binding's
|
||||
//! [`EntryKind::MessageHandler`] is stamped with a best-effort `queue`
|
||||
//! extracted from the source (a `KafkaConsumer('topic', ...)` /
|
||||
//! `Consumer({"group.id": ..., "topics": ["t"]}).subscribe([...])`
|
||||
//! literal); a missing topic falls back to the empty string.
|
||||
|
||||
use crate::dynamic::framework::{FrameworkAdapter, FrameworkBinding};
|
||||
use crate::evidence::EntryKind;
|
||||
use crate::summary::FuncSummary;
|
||||
use crate::symbol::Lang;
|
||||
|
||||
pub struct KafkaPythonAdapter;
|
||||
|
||||
const ADAPTER_NAME: &str = "kafka-python";
|
||||
|
||||
fn callee_is_kafka_consumer(name: &str) -> bool {
|
||||
let last = name.rsplit_once('.').map(|(_, s)| s).unwrap_or(name);
|
||||
matches!(
|
||||
last,
|
||||
"KafkaConsumer" | "Consumer" | "subscribe" | "poll" | "consume" | "process_message"
|
||||
)
|
||||
}
|
||||
|
||||
fn source_imports_kafka(file_bytes: &[u8]) -> bool {
|
||||
const NEEDLES: &[&[u8]] = &[
|
||||
b"from kafka",
|
||||
b"import kafka",
|
||||
b"from confluent_kafka",
|
||||
b"import confluent_kafka",
|
||||
];
|
||||
NEEDLES
|
||||
.iter()
|
||||
.any(|n| file_bytes.windows(n.len()).any(|w| w == *n))
|
||||
}
|
||||
|
||||
fn extract_topic_literal(file_bytes: &[u8]) -> String {
|
||||
let text = std::str::from_utf8(file_bytes).unwrap_or("");
|
||||
for needle in ["KafkaConsumer(", ".subscribe(", "topic="] {
|
||||
if let Some(idx) = text.find(needle) {
|
||||
let after = &text[idx + needle.len()..];
|
||||
for (open, close) in [('"', '"'), ('\'', '\'')] {
|
||||
if let Some(o) = after.find(open) {
|
||||
let rest = &after[o + 1..];
|
||||
if let Some(c) = rest.find(close) {
|
||||
return rest[..c].to_owned();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
String::new()
|
||||
}
|
||||
|
||||
impl FrameworkAdapter for KafkaPythonAdapter {
|
||||
fn name(&self) -> &'static str {
|
||||
ADAPTER_NAME
|
||||
}
|
||||
|
||||
fn lang(&self) -> Lang {
|
||||
Lang::Python
|
||||
}
|
||||
|
||||
fn detect(
|
||||
&self,
|
||||
summary: &FuncSummary,
|
||||
_ast: tree_sitter::Node<'_>,
|
||||
file_bytes: &[u8],
|
||||
) -> Option<FrameworkBinding> {
|
||||
let matches_call = super::any_callee_matches(summary, callee_is_kafka_consumer);
|
||||
let matches_source = source_imports_kafka(file_bytes);
|
||||
if matches_call || matches_source {
|
||||
Some(FrameworkBinding {
|
||||
adapter: ADAPTER_NAME.to_owned(),
|
||||
kind: EntryKind::MessageHandler {
|
||||
queue: extract_topic_literal(file_bytes),
|
||||
message_schema: None,
|
||||
},
|
||||
route: None,
|
||||
request_params: Vec::new(),
|
||||
response_writer: None,
|
||||
middleware: Vec::new(),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn parse_python(src: &[u8]) -> tree_sitter::Tree {
|
||||
let mut parser = tree_sitter::Parser::new();
|
||||
let lang = tree_sitter::Language::from(tree_sitter_python::LANGUAGE);
|
||||
parser.set_language(&lang).unwrap();
|
||||
parser.parse(src, None).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fires_on_kafka_python_consumer() {
|
||||
let src: &[u8] = b"from kafka import KafkaConsumer\n\n\
|
||||
def handler(msg):\n print(msg)\n\n\
|
||||
consumer = KafkaConsumer('orders', bootstrap_servers='broker:9092')\n";
|
||||
let tree = parse_python(src);
|
||||
let summary = FuncSummary {
|
||||
name: "handler".into(),
|
||||
..Default::default()
|
||||
};
|
||||
let binding = KafkaPythonAdapter
|
||||
.detect(&summary, tree.root_node(), src)
|
||||
.expect("kafka import binds");
|
||||
assert_eq!(binding.adapter, "kafka-python");
|
||||
assert!(matches!(binding.kind, EntryKind::MessageHandler { .. }));
|
||||
if let EntryKind::MessageHandler { queue, .. } = binding.kind {
|
||||
assert_eq!(queue, "orders");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skips_plain_function() {
|
||||
let src: &[u8] = b"def add(a, b):\n return a + b\n";
|
||||
let tree = parse_python(src);
|
||||
let summary = FuncSummary {
|
||||
name: "add".into(),
|
||||
..Default::default()
|
||||
};
|
||||
assert!(KafkaPythonAdapter
|
||||
.detect(&summary, tree.root_node(), src)
|
||||
.is_none());
|
||||
}
|
||||
}
|
||||
|
|
@ -36,9 +36,12 @@ pub mod js_handlebars;
|
|||
pub mod js_koa;
|
||||
pub mod js_nest;
|
||||
pub mod js_routes;
|
||||
pub mod kafka_java;
|
||||
pub mod kafka_python;
|
||||
pub mod ldap_php;
|
||||
pub mod ldap_python;
|
||||
pub mod ldap_spring;
|
||||
pub mod nats_go;
|
||||
pub mod php_codeigniter;
|
||||
pub mod php_laravel;
|
||||
pub mod php_routes;
|
||||
|
|
@ -48,6 +51,8 @@ pub mod php_unserialize;
|
|||
pub mod pp_json_deep_assign;
|
||||
pub mod pp_lodash_merge;
|
||||
pub mod pp_object_assign;
|
||||
pub mod pubsub_go;
|
||||
pub mod pubsub_python;
|
||||
pub mod python_django;
|
||||
pub mod python_fastapi;
|
||||
pub mod python_flask;
|
||||
|
|
@ -55,6 +60,8 @@ pub mod python_jinja2;
|
|||
pub mod python_pickle;
|
||||
pub mod python_routes;
|
||||
pub mod python_starlette;
|
||||
pub mod rabbit_java;
|
||||
pub mod rabbit_python;
|
||||
pub mod redirect_go;
|
||||
pub mod redirect_java;
|
||||
pub mod redirect_js;
|
||||
|
|
@ -73,6 +80,9 @@ pub mod rust_axum;
|
|||
pub mod rust_rocket;
|
||||
pub mod rust_routes;
|
||||
pub mod rust_warp;
|
||||
pub mod sqs_java;
|
||||
pub mod sqs_node;
|
||||
pub mod sqs_python;
|
||||
pub mod xpath_java;
|
||||
pub mod xpath_js;
|
||||
pub mod xpath_php;
|
||||
|
|
@ -105,9 +115,12 @@ pub use js_fastify::JsFastifyAdapter;
|
|||
pub use js_handlebars::JsHandlebarsAdapter;
|
||||
pub use js_koa::JsKoaAdapter;
|
||||
pub use js_nest::{JsNestAdapter, TsNestAdapter};
|
||||
pub use kafka_java::KafkaJavaAdapter;
|
||||
pub use kafka_python::KafkaPythonAdapter;
|
||||
pub use ldap_php::LdapPhpAdapter;
|
||||
pub use ldap_python::LdapPythonAdapter;
|
||||
pub use ldap_spring::LdapSpringAdapter;
|
||||
pub use nats_go::NatsGoAdapter;
|
||||
pub use php_codeigniter::PhpCodeIgniterAdapter;
|
||||
pub use php_laravel::PhpLaravelAdapter;
|
||||
pub use php_symfony::PhpSymfonyAdapter;
|
||||
|
|
@ -116,12 +129,16 @@ pub use php_unserialize::PhpUnserializeAdapter;
|
|||
pub use pp_json_deep_assign::{PpJsonDeepAssignJsAdapter, PpJsonDeepAssignTsAdapter};
|
||||
pub use pp_lodash_merge::{PpLodashMergeJsAdapter, PpLodashMergeTsAdapter};
|
||||
pub use pp_object_assign::{PpObjectAssignJsAdapter, PpObjectAssignTsAdapter};
|
||||
pub use pubsub_go::PubsubGoAdapter;
|
||||
pub use pubsub_python::PubsubPythonAdapter;
|
||||
pub use python_django::PythonDjangoAdapter;
|
||||
pub use python_fastapi::PythonFastApiAdapter;
|
||||
pub use python_flask::PythonFlaskAdapter;
|
||||
pub use python_jinja2::PythonJinja2Adapter;
|
||||
pub use python_pickle::PythonPickleAdapter;
|
||||
pub use python_starlette::PythonStarletteAdapter;
|
||||
pub use rabbit_java::RabbitJavaAdapter;
|
||||
pub use rabbit_python::RabbitPythonAdapter;
|
||||
pub use redirect_go::RedirectGoAdapter;
|
||||
pub use redirect_java::RedirectJavaAdapter;
|
||||
pub use redirect_js::RedirectJsAdapter;
|
||||
|
|
@ -138,6 +155,9 @@ pub use rust_actix::RustActixAdapter;
|
|||
pub use rust_axum::RustAxumAdapter;
|
||||
pub use rust_rocket::RustRocketAdapter;
|
||||
pub use rust_warp::RustWarpAdapter;
|
||||
pub use sqs_java::SqsJavaAdapter;
|
||||
pub use sqs_node::SqsNodeAdapter;
|
||||
pub use sqs_python::SqsPythonAdapter;
|
||||
pub use xpath_java::XpathJavaAdapter;
|
||||
pub use xpath_js::XpathJsAdapter;
|
||||
pub use xpath_php::XpathPhpAdapter;
|
||||
|
|
|
|||
108
src/dynamic/framework/adapters/nats_go.rs
Normal file
108
src/dynamic/framework/adapters/nats_go.rs
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
//! Phase 20 (Track M.2) — Go NATS subscriber adapter (`nats.go`).
|
||||
|
||||
use crate::dynamic::framework::{FrameworkAdapter, FrameworkBinding};
|
||||
use crate::evidence::EntryKind;
|
||||
use crate::summary::FuncSummary;
|
||||
use crate::symbol::Lang;
|
||||
|
||||
pub struct NatsGoAdapter;
|
||||
|
||||
const ADAPTER_NAME: &str = "nats-go";
|
||||
|
||||
fn callee_is_nats(name: &str) -> bool {
|
||||
let last = name.rsplit_once('.').map(|(_, s)| s).unwrap_or(name);
|
||||
matches!(
|
||||
last,
|
||||
"Subscribe" | "QueueSubscribe" | "Publish" | "HandleMessage" | "OnMessage"
|
||||
)
|
||||
}
|
||||
|
||||
fn source_imports_nats(file_bytes: &[u8]) -> bool {
|
||||
const NEEDLES: &[&[u8]] = &[
|
||||
b"github.com/nats-io/nats.go",
|
||||
b"nats.Connect",
|
||||
b"nats.Msg",
|
||||
];
|
||||
NEEDLES
|
||||
.iter()
|
||||
.any(|n| file_bytes.windows(n.len()).any(|w| w == *n))
|
||||
}
|
||||
|
||||
fn extract_subject(file_bytes: &[u8]) -> String {
|
||||
let text = std::str::from_utf8(file_bytes).unwrap_or("");
|
||||
for needle in [".Subscribe(\"", ".QueueSubscribe(\""] {
|
||||
if let Some(idx) = text.find(needle) {
|
||||
let after = &text[idx + needle.len()..];
|
||||
if let Some(end) = after.find('"') {
|
||||
return after[..end].to_owned();
|
||||
}
|
||||
}
|
||||
}
|
||||
String::new()
|
||||
}
|
||||
|
||||
impl FrameworkAdapter for NatsGoAdapter {
|
||||
fn name(&self) -> &'static str {
|
||||
ADAPTER_NAME
|
||||
}
|
||||
|
||||
fn lang(&self) -> Lang {
|
||||
Lang::Go
|
||||
}
|
||||
|
||||
fn detect(
|
||||
&self,
|
||||
summary: &FuncSummary,
|
||||
_ast: tree_sitter::Node<'_>,
|
||||
file_bytes: &[u8],
|
||||
) -> Option<FrameworkBinding> {
|
||||
let matches_call = super::any_callee_matches(summary, callee_is_nats);
|
||||
let matches_source = source_imports_nats(file_bytes);
|
||||
if matches_call || matches_source {
|
||||
Some(FrameworkBinding {
|
||||
adapter: ADAPTER_NAME.to_owned(),
|
||||
kind: EntryKind::MessageHandler {
|
||||
queue: extract_subject(file_bytes),
|
||||
message_schema: None,
|
||||
},
|
||||
route: None,
|
||||
request_params: Vec::new(),
|
||||
response_writer: None,
|
||||
middleware: Vec::new(),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn parse_go(src: &[u8]) -> tree_sitter::Tree {
|
||||
let mut parser = tree_sitter::Parser::new();
|
||||
let lang = tree_sitter::Language::from(tree_sitter_go::LANGUAGE);
|
||||
parser.set_language(&lang).unwrap();
|
||||
parser.parse(src, None).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fires_on_nats_subscribe() {
|
||||
let src: &[u8] = b"package entry\nimport \"github.com/nats-io/nats.go\"\n\
|
||||
func OnMessage(msg *nats.Msg) {}\n\
|
||||
var nc = nats.Connect()\n\
|
||||
var sub, _ = nc.Subscribe(\"events\", OnMessage)\n";
|
||||
let tree = parse_go(src);
|
||||
let summary = FuncSummary {
|
||||
name: "OnMessage".into(),
|
||||
..Default::default()
|
||||
};
|
||||
let binding = NatsGoAdapter
|
||||
.detect(&summary, tree.root_node(), src)
|
||||
.expect("nats.Subscribe binds");
|
||||
if let EntryKind::MessageHandler { queue, .. } = binding.kind {
|
||||
assert_eq!(queue, "events");
|
||||
}
|
||||
}
|
||||
}
|
||||
108
src/dynamic/framework/adapters/pubsub_go.rs
Normal file
108
src/dynamic/framework/adapters/pubsub_go.rs
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
//! Phase 20 (Track M.2) — Go Google Pub/Sub subscriber adapter
|
||||
//! (`cloud.google.com/go/pubsub`).
|
||||
|
||||
use crate::dynamic::framework::{FrameworkAdapter, FrameworkBinding};
|
||||
use crate::evidence::EntryKind;
|
||||
use crate::summary::FuncSummary;
|
||||
use crate::symbol::Lang;
|
||||
|
||||
pub struct PubsubGoAdapter;
|
||||
|
||||
const ADAPTER_NAME: &str = "pubsub-go";
|
||||
|
||||
fn callee_is_pubsub(name: &str) -> bool {
|
||||
let last = name.rsplit_once('.').map(|(_, s)| s).unwrap_or(name);
|
||||
matches!(
|
||||
last,
|
||||
"Receive" | "Subscription" | "Pull" | "Handle" | "OnMessage"
|
||||
)
|
||||
}
|
||||
|
||||
fn source_imports_pubsub(file_bytes: &[u8]) -> bool {
|
||||
const NEEDLES: &[&[u8]] = &[
|
||||
b"cloud.google.com/go/pubsub",
|
||||
b"pubsub.NewClient",
|
||||
b"pubsub.Message",
|
||||
];
|
||||
NEEDLES
|
||||
.iter()
|
||||
.any(|n| file_bytes.windows(n.len()).any(|w| w == *n))
|
||||
}
|
||||
|
||||
fn extract_topic(file_bytes: &[u8]) -> String {
|
||||
let text = std::str::from_utf8(file_bytes).unwrap_or("");
|
||||
for needle in [".Subscription(\"", "SubscriptionID(\"", "TopicID(\""] {
|
||||
if let Some(idx) = text.find(needle) {
|
||||
let after = &text[idx + needle.len()..];
|
||||
if let Some(end) = after.find('"') {
|
||||
return after[..end].to_owned();
|
||||
}
|
||||
}
|
||||
}
|
||||
String::new()
|
||||
}
|
||||
|
||||
impl FrameworkAdapter for PubsubGoAdapter {
|
||||
fn name(&self) -> &'static str {
|
||||
ADAPTER_NAME
|
||||
}
|
||||
|
||||
fn lang(&self) -> Lang {
|
||||
Lang::Go
|
||||
}
|
||||
|
||||
fn detect(
|
||||
&self,
|
||||
summary: &FuncSummary,
|
||||
_ast: tree_sitter::Node<'_>,
|
||||
file_bytes: &[u8],
|
||||
) -> Option<FrameworkBinding> {
|
||||
let matches_call = super::any_callee_matches(summary, callee_is_pubsub);
|
||||
let matches_source = source_imports_pubsub(file_bytes);
|
||||
if matches_call || matches_source {
|
||||
Some(FrameworkBinding {
|
||||
adapter: ADAPTER_NAME.to_owned(),
|
||||
kind: EntryKind::MessageHandler {
|
||||
queue: extract_topic(file_bytes),
|
||||
message_schema: None,
|
||||
},
|
||||
route: None,
|
||||
request_params: Vec::new(),
|
||||
response_writer: None,
|
||||
middleware: Vec::new(),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn parse_go(src: &[u8]) -> tree_sitter::Tree {
|
||||
let mut parser = tree_sitter::Parser::new();
|
||||
let lang = tree_sitter::Language::from(tree_sitter_go::LANGUAGE);
|
||||
parser.set_language(&lang).unwrap();
|
||||
parser.parse(src, None).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fires_on_pubsub_subscription() {
|
||||
let src: &[u8] = b"package entry\nimport \"cloud.google.com/go/pubsub\"\n\
|
||||
func Handle(msg *pubsub.Message) {}\n\
|
||||
var sub = pubsub.NewClient.Subscription(\"my-sub\")\n";
|
||||
let tree = parse_go(src);
|
||||
let summary = FuncSummary {
|
||||
name: "Handle".into(),
|
||||
..Default::default()
|
||||
};
|
||||
let binding = PubsubGoAdapter
|
||||
.detect(&summary, tree.root_node(), src)
|
||||
.expect("pubsub.Subscription binds");
|
||||
if let EntryKind::MessageHandler { queue, .. } = binding.kind {
|
||||
assert_eq!(queue, "my-sub");
|
||||
}
|
||||
}
|
||||
}
|
||||
115
src/dynamic/framework/adapters/pubsub_python.rs
Normal file
115
src/dynamic/framework/adapters/pubsub_python.rs
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
//! Phase 20 (Track M.2) — Python Google Pub/Sub subscriber adapter.
|
||||
|
||||
use crate::dynamic::framework::{FrameworkAdapter, FrameworkBinding};
|
||||
use crate::evidence::EntryKind;
|
||||
use crate::summary::FuncSummary;
|
||||
use crate::symbol::Lang;
|
||||
|
||||
pub struct PubsubPythonAdapter;
|
||||
|
||||
const ADAPTER_NAME: &str = "pubsub-python";
|
||||
|
||||
fn callee_is_pubsub(name: &str) -> bool {
|
||||
let last = name.rsplit_once('.').map(|(_, s)| s).unwrap_or(name);
|
||||
matches!(
|
||||
last,
|
||||
"subscribe" | "pull" | "callback" | "process_message"
|
||||
)
|
||||
}
|
||||
|
||||
fn source_imports_pubsub(file_bytes: &[u8]) -> bool {
|
||||
const NEEDLES: &[&[u8]] = &[
|
||||
b"google.cloud.pubsub",
|
||||
b"from google.cloud import pubsub",
|
||||
b"google.cloud.pubsub_v1",
|
||||
];
|
||||
NEEDLES
|
||||
.iter()
|
||||
.any(|n| file_bytes.windows(n.len()).any(|w| w == *n))
|
||||
}
|
||||
|
||||
fn extract_topic(file_bytes: &[u8]) -> String {
|
||||
let text = std::str::from_utf8(file_bytes).unwrap_or("");
|
||||
// Needles include the opening quote so we only need to find the
|
||||
// closing one — avoids picking up the next literal after a comma.
|
||||
for (needle, close) in [
|
||||
(".subscribe(\"", '"'),
|
||||
(".subscribe('", '\''),
|
||||
("subscription_path(\"", '"'),
|
||||
("subscription_path('", '\''),
|
||||
] {
|
||||
if let Some(idx) = text.find(needle) {
|
||||
let after = &text[idx + needle.len()..];
|
||||
if let Some(end) = after.find(close) {
|
||||
return after[..end].to_owned();
|
||||
}
|
||||
}
|
||||
}
|
||||
String::new()
|
||||
}
|
||||
|
||||
impl FrameworkAdapter for PubsubPythonAdapter {
|
||||
fn name(&self) -> &'static str {
|
||||
ADAPTER_NAME
|
||||
}
|
||||
|
||||
fn lang(&self) -> Lang {
|
||||
Lang::Python
|
||||
}
|
||||
|
||||
fn detect(
|
||||
&self,
|
||||
summary: &FuncSummary,
|
||||
_ast: tree_sitter::Node<'_>,
|
||||
file_bytes: &[u8],
|
||||
) -> Option<FrameworkBinding> {
|
||||
let matches_call = super::any_callee_matches(summary, callee_is_pubsub);
|
||||
let matches_source = source_imports_pubsub(file_bytes);
|
||||
if matches_call || matches_source {
|
||||
Some(FrameworkBinding {
|
||||
adapter: ADAPTER_NAME.to_owned(),
|
||||
kind: EntryKind::MessageHandler {
|
||||
queue: extract_topic(file_bytes),
|
||||
message_schema: None,
|
||||
},
|
||||
route: None,
|
||||
request_params: Vec::new(),
|
||||
response_writer: None,
|
||||
middleware: Vec::new(),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn parse_python(src: &[u8]) -> tree_sitter::Tree {
|
||||
let mut parser = tree_sitter::Parser::new();
|
||||
let lang = tree_sitter::Language::from(tree_sitter_python::LANGUAGE);
|
||||
parser.set_language(&lang).unwrap();
|
||||
parser.parse(src, None).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fires_on_pubsub_v1_subscribe() {
|
||||
let src: &[u8] = b"from google.cloud import pubsub_v1\n\
|
||||
def callback(message):\n pass\n\
|
||||
sub = pubsub_v1.SubscriberClient()\n\
|
||||
sub.subscribe(\"projects/p/subscriptions/s\", callback=callback)\n";
|
||||
let tree = parse_python(src);
|
||||
let summary = FuncSummary {
|
||||
name: "callback".into(),
|
||||
..Default::default()
|
||||
};
|
||||
let binding = PubsubPythonAdapter
|
||||
.detect(&summary, tree.root_node(), src)
|
||||
.expect("pubsub_v1 binds");
|
||||
if let EntryKind::MessageHandler { queue, .. } = binding.kind {
|
||||
assert_eq!(queue, "projects/p/subscriptions/s");
|
||||
}
|
||||
}
|
||||
}
|
||||
116
src/dynamic/framework/adapters/rabbit_java.rs
Normal file
116
src/dynamic/framework/adapters/rabbit_java.rs
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
//! Phase 20 (Track M.2) — Java RabbitMQ consumer adapter
|
||||
//! (`com.rabbitmq.client.Channel.basicConsume`, Spring AMQP
|
||||
//! `@RabbitListener`).
|
||||
|
||||
use crate::dynamic::framework::{FrameworkAdapter, FrameworkBinding};
|
||||
use crate::evidence::EntryKind;
|
||||
use crate::summary::FuncSummary;
|
||||
use crate::symbol::Lang;
|
||||
|
||||
pub struct RabbitJavaAdapter;
|
||||
|
||||
const ADAPTER_NAME: &str = "rabbit-java";
|
||||
|
||||
fn callee_is_rabbit(name: &str) -> bool {
|
||||
let last = name.rsplit_once('.').map(|(_, s)| s).unwrap_or(name);
|
||||
matches!(
|
||||
last,
|
||||
"basicConsume" | "basicGet" | "handleDelivery" | "onMessage" | "receive"
|
||||
)
|
||||
}
|
||||
|
||||
fn source_imports_rabbit(file_bytes: &[u8]) -> bool {
|
||||
const NEEDLES: &[&[u8]] = &[
|
||||
b"com.rabbitmq.client",
|
||||
b"org.springframework.amqp.rabbit",
|
||||
b"@RabbitListener",
|
||||
];
|
||||
NEEDLES
|
||||
.iter()
|
||||
.any(|n| file_bytes.windows(n.len()).any(|w| w == *n))
|
||||
}
|
||||
|
||||
fn extract_queue(file_bytes: &[u8]) -> String {
|
||||
let text = std::str::from_utf8(file_bytes).unwrap_or("");
|
||||
for needle in [
|
||||
"@RabbitListener(queues = \"",
|
||||
"@RabbitListener(queues=\"",
|
||||
"basicConsume(\"",
|
||||
"queueDeclare(\"",
|
||||
] {
|
||||
if let Some(idx) = text.find(needle) {
|
||||
let after = &text[idx + needle.len()..];
|
||||
if let Some(end) = after.find('"') {
|
||||
return after[..end].to_owned();
|
||||
}
|
||||
}
|
||||
}
|
||||
String::new()
|
||||
}
|
||||
|
||||
impl FrameworkAdapter for RabbitJavaAdapter {
|
||||
fn name(&self) -> &'static str {
|
||||
ADAPTER_NAME
|
||||
}
|
||||
|
||||
fn lang(&self) -> Lang {
|
||||
Lang::Java
|
||||
}
|
||||
|
||||
fn detect(
|
||||
&self,
|
||||
summary: &FuncSummary,
|
||||
_ast: tree_sitter::Node<'_>,
|
||||
file_bytes: &[u8],
|
||||
) -> Option<FrameworkBinding> {
|
||||
let matches_call = super::any_callee_matches(summary, callee_is_rabbit);
|
||||
let matches_source = source_imports_rabbit(file_bytes);
|
||||
if matches_call || matches_source {
|
||||
Some(FrameworkBinding {
|
||||
adapter: ADAPTER_NAME.to_owned(),
|
||||
kind: EntryKind::MessageHandler {
|
||||
queue: extract_queue(file_bytes),
|
||||
message_schema: None,
|
||||
},
|
||||
route: None,
|
||||
request_params: Vec::new(),
|
||||
response_writer: None,
|
||||
middleware: Vec::new(),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn parse_java(src: &[u8]) -> tree_sitter::Tree {
|
||||
let mut parser = tree_sitter::Parser::new();
|
||||
let lang = tree_sitter::Language::from(tree_sitter_java::LANGUAGE);
|
||||
parser.set_language(&lang).unwrap();
|
||||
parser.parse(src, None).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fires_on_rabbit_listener_annotation() {
|
||||
let src: &[u8] = b"import org.springframework.amqp.rabbit.annotation.RabbitListener;\n\
|
||||
public class Vuln {\n\
|
||||
@RabbitListener(queues = \"work\")\n\
|
||||
public void onMessage(String mid, String body) {}\n\
|
||||
}\n";
|
||||
let tree = parse_java(src);
|
||||
let summary = FuncSummary {
|
||||
name: "onMessage".into(),
|
||||
..Default::default()
|
||||
};
|
||||
let binding = RabbitJavaAdapter
|
||||
.detect(&summary, tree.root_node(), src)
|
||||
.expect("@RabbitListener binds");
|
||||
if let EntryKind::MessageHandler { queue, .. } = binding.kind {
|
||||
assert_eq!(queue, "work");
|
||||
}
|
||||
}
|
||||
}
|
||||
111
src/dynamic/framework/adapters/rabbit_python.rs
Normal file
111
src/dynamic/framework/adapters/rabbit_python.rs
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
//! Phase 20 (Track M.2) — Python RabbitMQ consumer adapter
|
||||
//! (`pika.BlockingConnection`, `aio-pika`).
|
||||
|
||||
use crate::dynamic::framework::{FrameworkAdapter, FrameworkBinding};
|
||||
use crate::evidence::EntryKind;
|
||||
use crate::summary::FuncSummary;
|
||||
use crate::symbol::Lang;
|
||||
|
||||
pub struct RabbitPythonAdapter;
|
||||
|
||||
const ADAPTER_NAME: &str = "rabbit-python";
|
||||
|
||||
fn callee_is_rabbit(name: &str) -> bool {
|
||||
let last = name.rsplit_once('.').map(|(_, s)| s).unwrap_or(name);
|
||||
matches!(
|
||||
last,
|
||||
"basic_consume" | "basic_get" | "handle" | "on_message" | "process"
|
||||
)
|
||||
}
|
||||
|
||||
fn source_imports_rabbit(file_bytes: &[u8]) -> bool {
|
||||
const NEEDLES: &[&[u8]] = &[
|
||||
b"import pika",
|
||||
b"from pika",
|
||||
b"import aio_pika",
|
||||
b"from aio_pika",
|
||||
];
|
||||
NEEDLES
|
||||
.iter()
|
||||
.any(|n| file_bytes.windows(n.len()).any(|w| w == *n))
|
||||
}
|
||||
|
||||
fn extract_queue(file_bytes: &[u8]) -> String {
|
||||
let text = std::str::from_utf8(file_bytes).unwrap_or("");
|
||||
for needle in ["queue=\"", "queue='", "queue_declare(\"", "queue_declare('"] {
|
||||
if let Some(idx) = text.find(needle) {
|
||||
let after = &text[idx + needle.len()..];
|
||||
let close = if needle.ends_with('"') { '"' } else { '\'' };
|
||||
if let Some(end) = after.find(close) {
|
||||
return after[..end].to_owned();
|
||||
}
|
||||
}
|
||||
}
|
||||
String::new()
|
||||
}
|
||||
|
||||
impl FrameworkAdapter for RabbitPythonAdapter {
|
||||
fn name(&self) -> &'static str {
|
||||
ADAPTER_NAME
|
||||
}
|
||||
|
||||
fn lang(&self) -> Lang {
|
||||
Lang::Python
|
||||
}
|
||||
|
||||
fn detect(
|
||||
&self,
|
||||
summary: &FuncSummary,
|
||||
_ast: tree_sitter::Node<'_>,
|
||||
file_bytes: &[u8],
|
||||
) -> Option<FrameworkBinding> {
|
||||
let matches_call = super::any_callee_matches(summary, callee_is_rabbit);
|
||||
let matches_source = source_imports_rabbit(file_bytes);
|
||||
if matches_call || matches_source {
|
||||
Some(FrameworkBinding {
|
||||
adapter: ADAPTER_NAME.to_owned(),
|
||||
kind: EntryKind::MessageHandler {
|
||||
queue: extract_queue(file_bytes),
|
||||
message_schema: None,
|
||||
},
|
||||
route: None,
|
||||
request_params: Vec::new(),
|
||||
response_writer: None,
|
||||
middleware: Vec::new(),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn parse_python(src: &[u8]) -> tree_sitter::Tree {
|
||||
let mut parser = tree_sitter::Parser::new();
|
||||
let lang = tree_sitter::Language::from(tree_sitter_python::LANGUAGE);
|
||||
parser.set_language(&lang).unwrap();
|
||||
parser.parse(src, None).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fires_on_pika_basic_consume() {
|
||||
let src: &[u8] = b"import pika\n\
|
||||
def on_message(ch, method, properties, body):\n pass\n\
|
||||
chan = pika.BlockingConnection().channel()\n\
|
||||
chan.basic_consume(queue=\"work\", on_message_callback=on_message)\n";
|
||||
let tree = parse_python(src);
|
||||
let summary = FuncSummary {
|
||||
name: "on_message".into(),
|
||||
..Default::default()
|
||||
};
|
||||
let binding = RabbitPythonAdapter
|
||||
.detect(&summary, tree.root_node(), src)
|
||||
.expect("pika binds");
|
||||
if let EntryKind::MessageHandler { queue, .. } = binding.kind {
|
||||
assert_eq!(queue, "work");
|
||||
}
|
||||
}
|
||||
}
|
||||
110
src/dynamic/framework/adapters/sqs_java.rs
Normal file
110
src/dynamic/framework/adapters/sqs_java.rs
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
//! Phase 20 (Track M.2) — Java SQS consumer adapter.
|
||||
|
||||
use crate::dynamic::framework::{FrameworkAdapter, FrameworkBinding};
|
||||
use crate::evidence::EntryKind;
|
||||
use crate::summary::FuncSummary;
|
||||
use crate::symbol::Lang;
|
||||
|
||||
pub struct SqsJavaAdapter;
|
||||
|
||||
const ADAPTER_NAME: &str = "sqs-java";
|
||||
|
||||
fn callee_is_sqs(name: &str) -> bool {
|
||||
let last = name.rsplit_once('.').map(|(_, s)| s).unwrap_or(name);
|
||||
matches!(
|
||||
last,
|
||||
"receiveMessage" | "deleteMessage" | "onMessage" | "handleMessage"
|
||||
)
|
||||
}
|
||||
|
||||
fn source_imports_sqs(file_bytes: &[u8]) -> bool {
|
||||
const NEEDLES: &[&[u8]] = &[
|
||||
b"software.amazon.awssdk.services.sqs",
|
||||
b"com.amazonaws.services.sqs",
|
||||
b"@SqsListener",
|
||||
b"io.awspring.cloud.sqs",
|
||||
];
|
||||
NEEDLES
|
||||
.iter()
|
||||
.any(|n| file_bytes.windows(n.len()).any(|w| w == *n))
|
||||
}
|
||||
|
||||
fn extract_queue(file_bytes: &[u8]) -> String {
|
||||
let text = std::str::from_utf8(file_bytes).unwrap_or("");
|
||||
for needle in ["@SqsListener(\"", "queueUrl(\"", "queueName(\""] {
|
||||
if let Some(idx) = text.find(needle) {
|
||||
let after = &text[idx + needle.len()..];
|
||||
if let Some(end) = after.find('"') {
|
||||
return after[..end].to_owned();
|
||||
}
|
||||
}
|
||||
}
|
||||
String::new()
|
||||
}
|
||||
|
||||
impl FrameworkAdapter for SqsJavaAdapter {
|
||||
fn name(&self) -> &'static str {
|
||||
ADAPTER_NAME
|
||||
}
|
||||
|
||||
fn lang(&self) -> Lang {
|
||||
Lang::Java
|
||||
}
|
||||
|
||||
fn detect(
|
||||
&self,
|
||||
summary: &FuncSummary,
|
||||
_ast: tree_sitter::Node<'_>,
|
||||
file_bytes: &[u8],
|
||||
) -> Option<FrameworkBinding> {
|
||||
let matches_call = super::any_callee_matches(summary, callee_is_sqs);
|
||||
let matches_source = source_imports_sqs(file_bytes);
|
||||
if matches_call || matches_source {
|
||||
Some(FrameworkBinding {
|
||||
adapter: ADAPTER_NAME.to_owned(),
|
||||
kind: EntryKind::MessageHandler {
|
||||
queue: extract_queue(file_bytes),
|
||||
message_schema: None,
|
||||
},
|
||||
route: None,
|
||||
request_params: Vec::new(),
|
||||
response_writer: None,
|
||||
middleware: Vec::new(),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn parse_java(src: &[u8]) -> tree_sitter::Tree {
|
||||
let mut parser = tree_sitter::Parser::new();
|
||||
let lang = tree_sitter::Language::from(tree_sitter_java::LANGUAGE);
|
||||
parser.set_language(&lang).unwrap();
|
||||
parser.parse(src, None).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fires_on_sqs_listener_annotation() {
|
||||
let src: &[u8] = b"import io.awspring.cloud.sqs.annotation.SqsListener;\n\
|
||||
public class Vuln {\n\
|
||||
@SqsListener(\"jobs\")\n\
|
||||
public void handleMessage(java.util.Map<String,String> env) {}\n\
|
||||
}\n";
|
||||
let tree = parse_java(src);
|
||||
let summary = FuncSummary {
|
||||
name: "handleMessage".into(),
|
||||
..Default::default()
|
||||
};
|
||||
let binding = SqsJavaAdapter
|
||||
.detect(&summary, tree.root_node(), src)
|
||||
.expect("@SqsListener binds");
|
||||
if let EntryKind::MessageHandler { queue, .. } = binding.kind {
|
||||
assert_eq!(queue, "jobs");
|
||||
}
|
||||
}
|
||||
}
|
||||
112
src/dynamic/framework/adapters/sqs_node.rs
Normal file
112
src/dynamic/framework/adapters/sqs_node.rs
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
//! Phase 20 (Track M.2) — Node SQS consumer adapter (`@aws-sdk/client-sqs`,
|
||||
//! `aws-sdk`, `sqs-consumer`).
|
||||
|
||||
use crate::dynamic::framework::{FrameworkAdapter, FrameworkBinding};
|
||||
use crate::evidence::EntryKind;
|
||||
use crate::summary::FuncSummary;
|
||||
use crate::symbol::Lang;
|
||||
|
||||
pub struct SqsNodeAdapter;
|
||||
|
||||
const ADAPTER_NAME: &str = "sqs-node";
|
||||
|
||||
fn callee_is_sqs(name: &str) -> bool {
|
||||
let last = name.rsplit_once('.').map(|(_, s)| s).unwrap_or(name);
|
||||
matches!(
|
||||
last,
|
||||
"receiveMessage" | "deleteMessage" | "handleMessage" | "send" | "Consumer"
|
||||
)
|
||||
}
|
||||
|
||||
fn source_imports_sqs(file_bytes: &[u8]) -> bool {
|
||||
const NEEDLES: &[&[u8]] = &[
|
||||
b"@aws-sdk/client-sqs",
|
||||
b"aws-sdk/clients/sqs",
|
||||
b"require('sqs-consumer')",
|
||||
b"require(\"sqs-consumer\")",
|
||||
b"from 'sqs-consumer'",
|
||||
b"from \"sqs-consumer\"",
|
||||
];
|
||||
NEEDLES
|
||||
.iter()
|
||||
.any(|n| file_bytes.windows(n.len()).any(|w| w == *n))
|
||||
}
|
||||
|
||||
fn extract_queue(file_bytes: &[u8]) -> String {
|
||||
let text = std::str::from_utf8(file_bytes).unwrap_or("");
|
||||
for needle in ["QueueUrl: \"", "QueueUrl: '", "queueUrl: \"", "queueUrl: '"] {
|
||||
if let Some(idx) = text.find(needle) {
|
||||
let after = &text[idx + needle.len()..];
|
||||
let close = if needle.ends_with('"') { '"' } else { '\'' };
|
||||
if let Some(end) = after.find(close) {
|
||||
return after[..end].to_owned();
|
||||
}
|
||||
}
|
||||
}
|
||||
String::new()
|
||||
}
|
||||
|
||||
impl FrameworkAdapter for SqsNodeAdapter {
|
||||
fn name(&self) -> &'static str {
|
||||
ADAPTER_NAME
|
||||
}
|
||||
|
||||
fn lang(&self) -> Lang {
|
||||
Lang::JavaScript
|
||||
}
|
||||
|
||||
fn detect(
|
||||
&self,
|
||||
summary: &FuncSummary,
|
||||
_ast: tree_sitter::Node<'_>,
|
||||
file_bytes: &[u8],
|
||||
) -> Option<FrameworkBinding> {
|
||||
let matches_call = super::any_callee_matches(summary, callee_is_sqs);
|
||||
let matches_source = source_imports_sqs(file_bytes);
|
||||
if matches_call || matches_source {
|
||||
Some(FrameworkBinding {
|
||||
adapter: ADAPTER_NAME.to_owned(),
|
||||
kind: EntryKind::MessageHandler {
|
||||
queue: extract_queue(file_bytes),
|
||||
message_schema: None,
|
||||
},
|
||||
route: None,
|
||||
request_params: Vec::new(),
|
||||
response_writer: None,
|
||||
middleware: Vec::new(),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn parse_js(src: &[u8]) -> tree_sitter::Tree {
|
||||
let mut parser = tree_sitter::Parser::new();
|
||||
let lang = tree_sitter::Language::from(tree_sitter_javascript::LANGUAGE);
|
||||
parser.set_language(&lang).unwrap();
|
||||
parser.parse(src, None).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fires_on_sqs_consumer() {
|
||||
let src: &[u8] = b"const { Consumer } = require('sqs-consumer');\n\
|
||||
module.exports.handler = function(env) {};\n\
|
||||
const c = Consumer.create({ queueUrl: 'http://localhost/q', handleMessage: handler });\n";
|
||||
let tree = parse_js(src);
|
||||
let summary = FuncSummary {
|
||||
name: "handler".into(),
|
||||
..Default::default()
|
||||
};
|
||||
let binding = SqsNodeAdapter
|
||||
.detect(&summary, tree.root_node(), src)
|
||||
.expect("sqs-consumer binds");
|
||||
if let EntryKind::MessageHandler { queue, .. } = binding.kind {
|
||||
assert_eq!(queue, "http://localhost/q");
|
||||
}
|
||||
}
|
||||
}
|
||||
112
src/dynamic/framework/adapters/sqs_python.rs
Normal file
112
src/dynamic/framework/adapters/sqs_python.rs
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
//! Phase 20 (Track M.2) — Python SQS consumer adapter.
|
||||
|
||||
use crate::dynamic::framework::{FrameworkAdapter, FrameworkBinding};
|
||||
use crate::evidence::EntryKind;
|
||||
use crate::summary::FuncSummary;
|
||||
use crate::symbol::Lang;
|
||||
|
||||
pub struct SqsPythonAdapter;
|
||||
|
||||
const ADAPTER_NAME: &str = "sqs-python";
|
||||
|
||||
fn callee_is_sqs(name: &str) -> bool {
|
||||
let last = name.rsplit_once('.').map(|(_, s)| s).unwrap_or(name);
|
||||
matches!(
|
||||
last,
|
||||
"receive_message" | "delete_message" | "process_message" | "handler"
|
||||
)
|
||||
}
|
||||
|
||||
fn source_imports_sqs(file_bytes: &[u8]) -> bool {
|
||||
const NEEDLES: &[&[u8]] = &[
|
||||
b"boto3.client('sqs'",
|
||||
b"boto3.client(\"sqs\"",
|
||||
b"boto3.resource('sqs'",
|
||||
b"boto3.resource(\"sqs\"",
|
||||
b"@sqs_listener",
|
||||
b"from aws_lambda_powertools.utilities.batch import sqs_batch_processor",
|
||||
];
|
||||
NEEDLES
|
||||
.iter()
|
||||
.any(|n| file_bytes.windows(n.len()).any(|w| w == *n))
|
||||
}
|
||||
|
||||
fn extract_queue(file_bytes: &[u8]) -> String {
|
||||
let text = std::str::from_utf8(file_bytes).unwrap_or("");
|
||||
for needle in ["QueueUrl=\"", "QueueUrl='", "QueueName=\"", "QueueName='"] {
|
||||
if let Some(idx) = text.find(needle) {
|
||||
let after = &text[idx + needle.len()..];
|
||||
let close = if needle.ends_with('"') { '"' } else { '\'' };
|
||||
if let Some(end) = after.find(close) {
|
||||
return after[..end].to_owned();
|
||||
}
|
||||
}
|
||||
}
|
||||
String::new()
|
||||
}
|
||||
|
||||
impl FrameworkAdapter for SqsPythonAdapter {
|
||||
fn name(&self) -> &'static str {
|
||||
ADAPTER_NAME
|
||||
}
|
||||
|
||||
fn lang(&self) -> Lang {
|
||||
Lang::Python
|
||||
}
|
||||
|
||||
fn detect(
|
||||
&self,
|
||||
summary: &FuncSummary,
|
||||
_ast: tree_sitter::Node<'_>,
|
||||
file_bytes: &[u8],
|
||||
) -> Option<FrameworkBinding> {
|
||||
let matches_call = super::any_callee_matches(summary, callee_is_sqs);
|
||||
let matches_source = source_imports_sqs(file_bytes);
|
||||
if matches_call || matches_source {
|
||||
Some(FrameworkBinding {
|
||||
adapter: ADAPTER_NAME.to_owned(),
|
||||
kind: EntryKind::MessageHandler {
|
||||
queue: extract_queue(file_bytes),
|
||||
message_schema: None,
|
||||
},
|
||||
route: None,
|
||||
request_params: Vec::new(),
|
||||
response_writer: None,
|
||||
middleware: Vec::new(),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn parse_python(src: &[u8]) -> tree_sitter::Tree {
|
||||
let mut parser = tree_sitter::Parser::new();
|
||||
let lang = tree_sitter::Language::from(tree_sitter_python::LANGUAGE);
|
||||
parser.set_language(&lang).unwrap();
|
||||
parser.parse(src, None).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fires_on_boto3_sqs_receive() {
|
||||
let src: &[u8] = b"import boto3\n\
|
||||
sqs = boto3.client('sqs')\n\
|
||||
def handler(envelope):\n pass\n\
|
||||
sqs.receive_message(QueueUrl=\"jobs\")\n";
|
||||
let tree = parse_python(src);
|
||||
let summary = FuncSummary {
|
||||
name: "handler".into(),
|
||||
..Default::default()
|
||||
};
|
||||
let binding = SqsPythonAdapter
|
||||
.detect(&summary, tree.root_node(), src)
|
||||
.expect("boto3 sqs binds");
|
||||
if let EntryKind::MessageHandler { queue, .. } = binding.kind {
|
||||
assert_eq!(queue, "jobs");
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue