cargo fmt

This commit is contained in:
elipeter 2026-05-21 14:35:42 -05:00
parent bec7bbf96c
commit 3a35cd6c8f
294 changed files with 6809 additions and 3911 deletions

View file

@ -29,9 +29,9 @@ use crate::summary::GlobalSummaries;
use crate::surface::{
SurfaceMap, dangerous, datastore, external,
lang::{
go_gin, go_http, java_quarkus, java_servlet, java_spring, js_express, js_koa,
php_laravel, php_slim, python_django, python_fastapi, python_flask,
ruby_rails, ruby_sinatra, rust_actix, rust_axum, ts_next,
go_gin, go_http, java_quarkus, java_servlet, java_spring, js_express, js_koa, php_laravel,
php_slim, python_django, python_fastapi, python_flask, ruby_rails, ruby_sinatra,
rust_actix, rust_axum, ts_next,
},
reachability,
};
@ -63,12 +63,8 @@ pub fn build_surface_map(inputs: &SurfaceBuildInputs<'_>) -> SurfaceMap {
.as_mut()
.and_then(|p| p.parse(&bytes, None))
.map(|tree| {
let mut all = python_flask::detect_flask_routes(
&tree,
&bytes,
path,
inputs.scan_root,
);
let mut all =
python_flask::detect_flask_routes(&tree, &bytes, path, inputs.scan_root);
all.extend(python_fastapi::detect_fastapi_routes(
&tree,
&bytes,
@ -165,12 +161,8 @@ pub fn build_surface_map(inputs: &SurfaceBuildInputs<'_>) -> SurfaceMap {
.as_mut()
.and_then(|p| p.parse(&bytes, None))
.map(|tree| {
let mut all = php_laravel::detect_laravel_routes(
&tree,
&bytes,
path,
inputs.scan_root,
);
let mut all =
php_laravel::detect_laravel_routes(&tree, &bytes, path, inputs.scan_root);
all.extend(php_slim::detect_slim_routes(
&tree,
&bytes,
@ -185,12 +177,8 @@ pub fn build_surface_map(inputs: &SurfaceBuildInputs<'_>) -> SurfaceMap {
.as_mut()
.and_then(|p| p.parse(&bytes, None))
.map(|tree| {
let mut all = ruby_sinatra::detect_sinatra_routes(
&tree,
&bytes,
path,
inputs.scan_root,
);
let mut all =
ruby_sinatra::detect_sinatra_routes(&tree, &bytes, path, inputs.scan_root);
all.extend(ruby_rails::detect_rails_routes(
&tree,
&bytes,
@ -435,13 +423,15 @@ def evaluator():
let files = vec![py];
let inputs = empty_inputs(&files, Some(dir.path()), &gs, &cg, &cfg);
let map = build_surface_map(&inputs);
assert!(map
.nodes
.iter()
.any(|n| matches!(n, SurfaceNode::DangerousLocal(_))));
assert!(map
.edges
.iter()
.any(|e| matches!(e.kind, crate::surface::EdgeKind::Reaches)));
assert!(
map.nodes
.iter()
.any(|n| matches!(n, SurfaceNode::DangerousLocal(_)))
);
assert!(
map.edges
.iter()
.any(|e| matches!(e.kind, crate::surface::EdgeKind::Reaches))
);
}
}

View file

@ -28,86 +28,314 @@ struct DriverRule {
const DRIVER_RULES: &[DriverRule] = &[
// Python — relational
DriverRule { leaf: "psycopg2.connect", kind: DataStoreKind::Sql, label: "PostgreSQL (psycopg2)" },
DriverRule { leaf: "psycopg.connect", kind: DataStoreKind::Sql, label: "PostgreSQL (psycopg3)" },
DriverRule { leaf: "mysql.connector.connect", kind: DataStoreKind::Sql, label: "MySQL (mysql.connector)" },
DriverRule { leaf: "MySQLdb.connect", kind: DataStoreKind::Sql, label: "MySQL (MySQLdb)" },
DriverRule { leaf: "pymysql.connect", kind: DataStoreKind::Sql, label: "MySQL (PyMySQL)" },
DriverRule { leaf: "sqlite3.connect", kind: DataStoreKind::Sql, label: "SQLite (sqlite3)" },
DriverRule { leaf: "sqlalchemy.create_engine", kind: DataStoreKind::Sql, label: "SQLAlchemy" },
DriverRule { leaf: "django.db.connection", kind: DataStoreKind::Sql, label: "Django ORM" },
DriverRule {
leaf: "psycopg2.connect",
kind: DataStoreKind::Sql,
label: "PostgreSQL (psycopg2)",
},
DriverRule {
leaf: "psycopg.connect",
kind: DataStoreKind::Sql,
label: "PostgreSQL (psycopg3)",
},
DriverRule {
leaf: "mysql.connector.connect",
kind: DataStoreKind::Sql,
label: "MySQL (mysql.connector)",
},
DriverRule {
leaf: "MySQLdb.connect",
kind: DataStoreKind::Sql,
label: "MySQL (MySQLdb)",
},
DriverRule {
leaf: "pymysql.connect",
kind: DataStoreKind::Sql,
label: "MySQL (PyMySQL)",
},
DriverRule {
leaf: "sqlite3.connect",
kind: DataStoreKind::Sql,
label: "SQLite (sqlite3)",
},
DriverRule {
leaf: "sqlalchemy.create_engine",
kind: DataStoreKind::Sql,
label: "SQLAlchemy",
},
DriverRule {
leaf: "django.db.connection",
kind: DataStoreKind::Sql,
label: "Django ORM",
},
// Python — kv / doc
DriverRule { leaf: "redis.Redis", kind: DataStoreKind::KeyValue, label: "Redis" },
DriverRule { leaf: "redis.from_url", kind: DataStoreKind::KeyValue, label: "Redis" },
DriverRule { leaf: "pymongo.MongoClient", kind: DataStoreKind::Document, label: "MongoDB" },
DriverRule { leaf: "boto3.client", kind: DataStoreKind::BlobStore, label: "AWS (boto3)" },
DriverRule { leaf: "boto3.resource", kind: DataStoreKind::BlobStore, label: "AWS (boto3)" },
DriverRule {
leaf: "redis.Redis",
kind: DataStoreKind::KeyValue,
label: "Redis",
},
DriverRule {
leaf: "redis.from_url",
kind: DataStoreKind::KeyValue,
label: "Redis",
},
DriverRule {
leaf: "pymongo.MongoClient",
kind: DataStoreKind::Document,
label: "MongoDB",
},
DriverRule {
leaf: "boto3.client",
kind: DataStoreKind::BlobStore,
label: "AWS (boto3)",
},
DriverRule {
leaf: "boto3.resource",
kind: DataStoreKind::BlobStore,
label: "AWS (boto3)",
},
// JavaScript / TypeScript — relational
DriverRule { leaf: "knex", kind: DataStoreKind::Sql, label: "Knex.js" },
DriverRule { leaf: "createConnection", kind: DataStoreKind::Sql, label: "MySQL/Postgres (mysql/pg)" },
DriverRule { leaf: "Sequelize", kind: DataStoreKind::Sql, label: "Sequelize" },
DriverRule { leaf: "TypeORM.createConnection", kind: DataStoreKind::Sql, label: "TypeORM" },
DriverRule { leaf: "PrismaClient", kind: DataStoreKind::Sql, label: "Prisma" },
DriverRule { leaf: "pool.query", kind: DataStoreKind::Sql, label: "pg/mysql pool" },
DriverRule { leaf: "client.query", kind: DataStoreKind::Sql, label: "pg client" },
DriverRule { leaf: "db.query", kind: DataStoreKind::Sql, label: "Generic SQL driver" },
DriverRule {
leaf: "knex",
kind: DataStoreKind::Sql,
label: "Knex.js",
},
DriverRule {
leaf: "createConnection",
kind: DataStoreKind::Sql,
label: "MySQL/Postgres (mysql/pg)",
},
DriverRule {
leaf: "Sequelize",
kind: DataStoreKind::Sql,
label: "Sequelize",
},
DriverRule {
leaf: "TypeORM.createConnection",
kind: DataStoreKind::Sql,
label: "TypeORM",
},
DriverRule {
leaf: "PrismaClient",
kind: DataStoreKind::Sql,
label: "Prisma",
},
DriverRule {
leaf: "pool.query",
kind: DataStoreKind::Sql,
label: "pg/mysql pool",
},
DriverRule {
leaf: "client.query",
kind: DataStoreKind::Sql,
label: "pg client",
},
DriverRule {
leaf: "db.query",
kind: DataStoreKind::Sql,
label: "Generic SQL driver",
},
// JS — kv / doc
DriverRule { leaf: "redis.createClient", kind: DataStoreKind::KeyValue, label: "Redis (node-redis)" },
DriverRule { leaf: "ioredis", kind: DataStoreKind::KeyValue, label: "ioredis" },
DriverRule { leaf: "MongoClient.connect", kind: DataStoreKind::Document, label: "MongoDB (node)" },
DriverRule { leaf: "AWS.S3", kind: DataStoreKind::BlobStore, label: "AWS S3" },
DriverRule {
leaf: "redis.createClient",
kind: DataStoreKind::KeyValue,
label: "Redis (node-redis)",
},
DriverRule {
leaf: "ioredis",
kind: DataStoreKind::KeyValue,
label: "ioredis",
},
DriverRule {
leaf: "MongoClient.connect",
kind: DataStoreKind::Document,
label: "MongoDB (node)",
},
DriverRule {
leaf: "AWS.S3",
kind: DataStoreKind::BlobStore,
label: "AWS S3",
},
// Java — JDBC / Hibernate
DriverRule { leaf: "DriverManager.getConnection", kind: DataStoreKind::Sql, label: "JDBC" },
DriverRule { leaf: "JdbcTemplate", kind: DataStoreKind::Sql, label: "Spring JdbcTemplate" },
DriverRule { leaf: "EntityManager", kind: DataStoreKind::Sql, label: "JPA EntityManager" },
DriverRule { leaf: "SessionFactory.openSession", kind: DataStoreKind::Sql, label: "Hibernate" },
DriverRule { leaf: "Jedis", kind: DataStoreKind::KeyValue, label: "Jedis (Redis)" },
DriverRule { leaf: "MongoClients.create", kind: DataStoreKind::Document, label: "MongoDB (java-driver)" },
DriverRule {
leaf: "DriverManager.getConnection",
kind: DataStoreKind::Sql,
label: "JDBC",
},
DriverRule {
leaf: "JdbcTemplate",
kind: DataStoreKind::Sql,
label: "Spring JdbcTemplate",
},
DriverRule {
leaf: "EntityManager",
kind: DataStoreKind::Sql,
label: "JPA EntityManager",
},
DriverRule {
leaf: "SessionFactory.openSession",
kind: DataStoreKind::Sql,
label: "Hibernate",
},
DriverRule {
leaf: "Jedis",
kind: DataStoreKind::KeyValue,
label: "Jedis (Redis)",
},
DriverRule {
leaf: "MongoClients.create",
kind: DataStoreKind::Document,
label: "MongoDB (java-driver)",
},
// Go — sql + ORM
DriverRule { leaf: "sql.Open", kind: DataStoreKind::Sql, label: "database/sql" },
DriverRule { leaf: "gorm.Open", kind: DataStoreKind::Sql, label: "GORM" },
DriverRule { leaf: "sqlx.Connect", kind: DataStoreKind::Sql, label: "sqlx" },
DriverRule { leaf: "sqlx.Open", kind: DataStoreKind::Sql, label: "sqlx" },
DriverRule { leaf: "redis.NewClient", kind: DataStoreKind::KeyValue, label: "go-redis" },
DriverRule { leaf: "mongo.Connect", kind: DataStoreKind::Document, label: "MongoDB (go-driver)" },
DriverRule {
leaf: "sql.Open",
kind: DataStoreKind::Sql,
label: "database/sql",
},
DriverRule {
leaf: "gorm.Open",
kind: DataStoreKind::Sql,
label: "GORM",
},
DriverRule {
leaf: "sqlx.Connect",
kind: DataStoreKind::Sql,
label: "sqlx",
},
DriverRule {
leaf: "sqlx.Open",
kind: DataStoreKind::Sql,
label: "sqlx",
},
DriverRule {
leaf: "redis.NewClient",
kind: DataStoreKind::KeyValue,
label: "go-redis",
},
DriverRule {
leaf: "mongo.Connect",
kind: DataStoreKind::Document,
label: "MongoDB (go-driver)",
},
// PHP — Eloquent / PDO
DriverRule { leaf: "PDO", kind: DataStoreKind::Sql, label: "PDO" },
DriverRule { leaf: "Eloquent::find", kind: DataStoreKind::Sql, label: "Laravel Eloquent" },
DriverRule { leaf: "Eloquent::where", kind: DataStoreKind::Sql, label: "Laravel Eloquent" },
DriverRule { leaf: "DB::connection", kind: DataStoreKind::Sql, label: "Laravel DB" },
DriverRule { leaf: "Doctrine", kind: DataStoreKind::Sql, label: "Doctrine ORM" },
DriverRule {
leaf: "PDO",
kind: DataStoreKind::Sql,
label: "PDO",
},
DriverRule {
leaf: "Eloquent::find",
kind: DataStoreKind::Sql,
label: "Laravel Eloquent",
},
DriverRule {
leaf: "Eloquent::where",
kind: DataStoreKind::Sql,
label: "Laravel Eloquent",
},
DriverRule {
leaf: "DB::connection",
kind: DataStoreKind::Sql,
label: "Laravel DB",
},
DriverRule {
leaf: "Doctrine",
kind: DataStoreKind::Sql,
label: "Doctrine ORM",
},
// Ruby — ActiveRecord
DriverRule { leaf: "ActiveRecord::Base.connection", kind: DataStoreKind::Sql, label: "ActiveRecord" },
DriverRule { leaf: "ActiveRecord::Base.find", kind: DataStoreKind::Sql, label: "ActiveRecord" },
DriverRule { leaf: ".find_by_sql", kind: DataStoreKind::Sql, label: "ActiveRecord raw SQL" },
DriverRule {
leaf: "ActiveRecord::Base.connection",
kind: DataStoreKind::Sql,
label: "ActiveRecord",
},
DriverRule {
leaf: "ActiveRecord::Base.find",
kind: DataStoreKind::Sql,
label: "ActiveRecord",
},
DriverRule {
leaf: ".find_by_sql",
kind: DataStoreKind::Sql,
label: "ActiveRecord raw SQL",
},
// Rust — sqlx / diesel
DriverRule { leaf: "sqlx::query", kind: DataStoreKind::Sql, label: "sqlx" },
DriverRule { leaf: "sqlx::query_as", kind: DataStoreKind::Sql, label: "sqlx" },
DriverRule { leaf: "diesel::sql_query", kind: DataStoreKind::Sql, label: "Diesel" },
DriverRule { leaf: "PgConnection::establish", kind: DataStoreKind::Sql, label: "Diesel" },
DriverRule {
leaf: "sqlx::query",
kind: DataStoreKind::Sql,
label: "sqlx",
},
DriverRule {
leaf: "sqlx::query_as",
kind: DataStoreKind::Sql,
label: "sqlx",
},
DriverRule {
leaf: "diesel::sql_query",
kind: DataStoreKind::Sql,
label: "Diesel",
},
DriverRule {
leaf: "PgConnection::establish",
kind: DataStoreKind::Sql,
label: "Diesel",
},
// Type-qualified — fires when the SSA type-fact engine resolves a
// receiver to `TypeKind::DatabaseConnection` regardless of the bare
// callee name (e.g. `conn = psycopg2.connect(); conn.cursor()` →
// typed_call_receivers maps the `.cursor` ordinal to "DatabaseConnection").
DriverRule { leaf: "DatabaseConnection.cursor", kind: DataStoreKind::Sql, label: "Database connection" },
DriverRule { leaf: "DatabaseConnection.execute", kind: DataStoreKind::Sql, label: "Database connection" },
DriverRule { leaf: "DatabaseConnection.query", kind: DataStoreKind::Sql, label: "Database connection" },
DriverRule { leaf: "DatabaseConnection.exec", kind: DataStoreKind::Sql, label: "Database connection" },
DriverRule { leaf: "DatabaseConnection.prepare", kind: DataStoreKind::Sql, label: "Database connection" },
DriverRule { leaf: "DatabaseConnection.commit", kind: DataStoreKind::Sql, label: "Database connection" },
DriverRule { leaf: "FileHandle.read", kind: DataStoreKind::Filesystem, label: "Filesystem" },
DriverRule { leaf: "FileHandle.write", kind: DataStoreKind::Filesystem, label: "Filesystem" },
DriverRule { leaf: "FileHandle.close", kind: DataStoreKind::Filesystem, label: "Filesystem" },
DriverRule {
leaf: "DatabaseConnection.cursor",
kind: DataStoreKind::Sql,
label: "Database connection",
},
DriverRule {
leaf: "DatabaseConnection.execute",
kind: DataStoreKind::Sql,
label: "Database connection",
},
DriverRule {
leaf: "DatabaseConnection.query",
kind: DataStoreKind::Sql,
label: "Database connection",
},
DriverRule {
leaf: "DatabaseConnection.exec",
kind: DataStoreKind::Sql,
label: "Database connection",
},
DriverRule {
leaf: "DatabaseConnection.prepare",
kind: DataStoreKind::Sql,
label: "Database connection",
},
DriverRule {
leaf: "DatabaseConnection.commit",
kind: DataStoreKind::Sql,
label: "Database connection",
},
DriverRule {
leaf: "FileHandle.read",
kind: DataStoreKind::Filesystem,
label: "Filesystem",
},
DriverRule {
leaf: "FileHandle.write",
kind: DataStoreKind::Filesystem,
label: "Filesystem",
},
DriverRule {
leaf: "FileHandle.close",
kind: DataStoreKind::Filesystem,
label: "Filesystem",
},
// Filesystem (best-effort: language-agnostic open()-family)
DriverRule { leaf: "open", kind: DataStoreKind::Filesystem, label: "Filesystem" },
DriverRule {
leaf: "open",
kind: DataStoreKind::Filesystem,
label: "Filesystem",
},
];
/// Walk every function summary's callee list and emit one
@ -127,7 +355,9 @@ pub fn detect_data_stores(summaries: &GlobalSummaries) -> Vec<SurfaceNode> {
let mut seen: std::collections::HashSet<(String, u32, String)> =
std::collections::HashSet::new();
for (key, summary) in summaries.iter() {
let typed = summaries.get_ssa(key).map(|s| s.typed_call_receivers.as_slice());
let typed = summaries
.get_ssa(key)
.map(|s| s.typed_call_receivers.as_slice());
for callee in &summary.callees {
let rule = match_rule(&callee.name).or_else(|| {
typed
@ -136,11 +366,7 @@ pub fn detect_data_stores(summaries: &GlobalSummaries) -> Vec<SurfaceNode> {
});
let Some(rule) = rule else { continue };
let location = call_site_location(summary, callee);
let dedup = (
location.file.clone(),
location.line,
rule.label.to_string(),
);
let dedup = (location.file.clone(), location.line, rule.label.to_string());
if !seen.insert(dedup) {
continue;
}
@ -170,7 +396,10 @@ fn qualify(container: &str, callee_name: &str) -> String {
/// `Vec<(ordinal, container)>` per function. Typical lengths are 0 to a
/// few dozen; a HashMap-per-summary would be wasteful.
fn container_for_ordinal(typed: &[(u32, String)], ordinal: u32) -> Option<&str> {
typed.iter().find(|(o, _)| *o == ordinal).map(|(_, c)| c.as_str())
typed
.iter()
.find(|(o, _)| *o == ordinal)
.map(|(_, c)| c.as_str())
}
fn match_rule(callee: &str) -> Option<&'static DriverRule> {
@ -285,11 +514,8 @@ mod tests {
#[test]
fn dedup_collapses_repeats_in_same_file() {
let mut gs = GlobalSummaries::new();
let (k, s) = summary_with_callees(
"init",
"app.py",
&["psycopg2.connect", "psycopg2.connect"],
);
let (k, s) =
summary_with_callees("init", "app.py", &["psycopg2.connect", "psycopg2.connect"]);
gs.insert(k, s);
let nodes = detect_data_stores(&gs);
assert_eq!(nodes.len(), 1);
@ -352,14 +578,12 @@ mod tests {
file_path: "app.py".into(),
lang: "python".into(),
param_count: 0,
callees: vec![
{
let mut c = CalleeSite::bare("conn.cursor");
c.ordinal = 7;
c.span = Some((4, 8));
c
},
],
callees: vec![{
let mut c = CalleeSite::bare("conn.cursor");
c.ordinal = 7;
c.span = Some((4, 8));
c
}],
..Default::default()
};
gs.insert(key.clone(), summary);

View file

@ -19,81 +19,307 @@ struct ClientRule {
const CLIENT_RULES: &[ClientRule] = &[
// HTTP
ClientRule { leaf: "requests.get", kind: ExternalServiceKind::HttpApi, label: "requests (Python)" },
ClientRule { leaf: "requests.post", kind: ExternalServiceKind::HttpApi, label: "requests (Python)" },
ClientRule { leaf: "httpx.get", kind: ExternalServiceKind::HttpApi, label: "httpx (Python)" },
ClientRule { leaf: "httpx.post", kind: ExternalServiceKind::HttpApi, label: "httpx (Python)" },
ClientRule { leaf: "urllib.request.urlopen", kind: ExternalServiceKind::HttpApi, label: "urllib" },
ClientRule { leaf: "fetch", kind: ExternalServiceKind::HttpApi, label: "fetch (JS)" },
ClientRule { leaf: "axios.get", kind: ExternalServiceKind::HttpApi, label: "axios" },
ClientRule { leaf: "axios.post", kind: ExternalServiceKind::HttpApi, label: "axios" },
ClientRule { leaf: "http.request", kind: ExternalServiceKind::HttpApi, label: "node http" },
ClientRule { leaf: "got", kind: ExternalServiceKind::HttpApi, label: "got (JS)" },
ClientRule { leaf: "HttpClient.send", kind: ExternalServiceKind::HttpApi, label: "Java HttpClient" },
ClientRule { leaf: "HttpClient.execute", kind: ExternalServiceKind::HttpApi, label: "Java HttpClient" },
ClientRule { leaf: "RestTemplate.exchange", kind: ExternalServiceKind::HttpApi, label: "Spring RestTemplate" },
ClientRule { leaf: "RestTemplate.getForObject", kind: ExternalServiceKind::HttpApi, label: "Spring RestTemplate" },
ClientRule { leaf: "OkHttpClient.newCall", kind: ExternalServiceKind::HttpApi, label: "OkHttp" },
ClientRule { leaf: "http.Get", kind: ExternalServiceKind::HttpApi, label: "net/http (Go)" },
ClientRule { leaf: "http.Post", kind: ExternalServiceKind::HttpApi, label: "net/http (Go)" },
ClientRule { leaf: "http.NewRequest", kind: ExternalServiceKind::HttpApi, label: "net/http (Go)" },
ClientRule { leaf: "client.Do", kind: ExternalServiceKind::HttpApi, label: "go http client" },
ClientRule { leaf: "reqwest::get", kind: ExternalServiceKind::HttpApi, label: "reqwest (Rust)" },
ClientRule { leaf: "reqwest::Client", kind: ExternalServiceKind::HttpApi, label: "reqwest (Rust)" },
ClientRule { leaf: "Net::HTTP", kind: ExternalServiceKind::HttpApi, label: "Net::HTTP (Ruby)" },
ClientRule { leaf: "HTTParty.get", kind: ExternalServiceKind::HttpApi, label: "HTTParty" },
ClientRule { leaf: "Faraday", kind: ExternalServiceKind::HttpApi, label: "Faraday (Ruby)" },
ClientRule { leaf: "curl_exec", kind: ExternalServiceKind::HttpApi, label: "PHP curl" },
ClientRule { leaf: "file_get_contents", kind: ExternalServiceKind::HttpApi, label: "PHP file_get_contents" },
ClientRule { leaf: "Guzzle", kind: ExternalServiceKind::HttpApi, label: "Guzzle (PHP)" },
ClientRule {
leaf: "requests.get",
kind: ExternalServiceKind::HttpApi,
label: "requests (Python)",
},
ClientRule {
leaf: "requests.post",
kind: ExternalServiceKind::HttpApi,
label: "requests (Python)",
},
ClientRule {
leaf: "httpx.get",
kind: ExternalServiceKind::HttpApi,
label: "httpx (Python)",
},
ClientRule {
leaf: "httpx.post",
kind: ExternalServiceKind::HttpApi,
label: "httpx (Python)",
},
ClientRule {
leaf: "urllib.request.urlopen",
kind: ExternalServiceKind::HttpApi,
label: "urllib",
},
ClientRule {
leaf: "fetch",
kind: ExternalServiceKind::HttpApi,
label: "fetch (JS)",
},
ClientRule {
leaf: "axios.get",
kind: ExternalServiceKind::HttpApi,
label: "axios",
},
ClientRule {
leaf: "axios.post",
kind: ExternalServiceKind::HttpApi,
label: "axios",
},
ClientRule {
leaf: "http.request",
kind: ExternalServiceKind::HttpApi,
label: "node http",
},
ClientRule {
leaf: "got",
kind: ExternalServiceKind::HttpApi,
label: "got (JS)",
},
ClientRule {
leaf: "HttpClient.send",
kind: ExternalServiceKind::HttpApi,
label: "Java HttpClient",
},
ClientRule {
leaf: "HttpClient.execute",
kind: ExternalServiceKind::HttpApi,
label: "Java HttpClient",
},
ClientRule {
leaf: "RestTemplate.exchange",
kind: ExternalServiceKind::HttpApi,
label: "Spring RestTemplate",
},
ClientRule {
leaf: "RestTemplate.getForObject",
kind: ExternalServiceKind::HttpApi,
label: "Spring RestTemplate",
},
ClientRule {
leaf: "OkHttpClient.newCall",
kind: ExternalServiceKind::HttpApi,
label: "OkHttp",
},
ClientRule {
leaf: "http.Get",
kind: ExternalServiceKind::HttpApi,
label: "net/http (Go)",
},
ClientRule {
leaf: "http.Post",
kind: ExternalServiceKind::HttpApi,
label: "net/http (Go)",
},
ClientRule {
leaf: "http.NewRequest",
kind: ExternalServiceKind::HttpApi,
label: "net/http (Go)",
},
ClientRule {
leaf: "client.Do",
kind: ExternalServiceKind::HttpApi,
label: "go http client",
},
ClientRule {
leaf: "reqwest::get",
kind: ExternalServiceKind::HttpApi,
label: "reqwest (Rust)",
},
ClientRule {
leaf: "reqwest::Client",
kind: ExternalServiceKind::HttpApi,
label: "reqwest (Rust)",
},
ClientRule {
leaf: "Net::HTTP",
kind: ExternalServiceKind::HttpApi,
label: "Net::HTTP (Ruby)",
},
ClientRule {
leaf: "HTTParty.get",
kind: ExternalServiceKind::HttpApi,
label: "HTTParty",
},
ClientRule {
leaf: "Faraday",
kind: ExternalServiceKind::HttpApi,
label: "Faraday (Ruby)",
},
ClientRule {
leaf: "curl_exec",
kind: ExternalServiceKind::HttpApi,
label: "PHP curl",
},
ClientRule {
leaf: "file_get_contents",
kind: ExternalServiceKind::HttpApi,
label: "PHP file_get_contents",
},
ClientRule {
leaf: "Guzzle",
kind: ExternalServiceKind::HttpApi,
label: "Guzzle (PHP)",
},
// Message brokers
ClientRule { leaf: "kafka.send", kind: ExternalServiceKind::MessageBroker, label: "Kafka" },
ClientRule { leaf: "KafkaProducer.send", kind: ExternalServiceKind::MessageBroker, label: "Kafka" },
ClientRule { leaf: "rabbitmq.publish", kind: ExternalServiceKind::MessageBroker, label: "RabbitMQ" },
ClientRule { leaf: "amqp.publish", kind: ExternalServiceKind::MessageBroker, label: "AMQP" },
ClientRule { leaf: "sqs.send_message", kind: ExternalServiceKind::MessageBroker, label: "AWS SQS" },
ClientRule { leaf: "sns.publish", kind: ExternalServiceKind::MessageBroker, label: "AWS SNS" },
ClientRule {
leaf: "kafka.send",
kind: ExternalServiceKind::MessageBroker,
label: "Kafka",
},
ClientRule {
leaf: "KafkaProducer.send",
kind: ExternalServiceKind::MessageBroker,
label: "Kafka",
},
ClientRule {
leaf: "rabbitmq.publish",
kind: ExternalServiceKind::MessageBroker,
label: "RabbitMQ",
},
ClientRule {
leaf: "amqp.publish",
kind: ExternalServiceKind::MessageBroker,
label: "AMQP",
},
ClientRule {
leaf: "sqs.send_message",
kind: ExternalServiceKind::MessageBroker,
label: "AWS SQS",
},
ClientRule {
leaf: "sns.publish",
kind: ExternalServiceKind::MessageBroker,
label: "AWS SNS",
},
// Search indices
ClientRule { leaf: "Elasticsearch", kind: ExternalServiceKind::SearchIndex, label: "Elasticsearch" },
ClientRule { leaf: "elasticsearch.search", kind: ExternalServiceKind::SearchIndex, label: "Elasticsearch" },
ClientRule { leaf: "OpenSearch", kind: ExternalServiceKind::SearchIndex, label: "OpenSearch" },
ClientRule { leaf: "Algolia", kind: ExternalServiceKind::SearchIndex, label: "Algolia" },
ClientRule {
leaf: "Elasticsearch",
kind: ExternalServiceKind::SearchIndex,
label: "Elasticsearch",
},
ClientRule {
leaf: "elasticsearch.search",
kind: ExternalServiceKind::SearchIndex,
label: "Elasticsearch",
},
ClientRule {
leaf: "OpenSearch",
kind: ExternalServiceKind::SearchIndex,
label: "OpenSearch",
},
ClientRule {
leaf: "Algolia",
kind: ExternalServiceKind::SearchIndex,
label: "Algolia",
},
// Auth providers
ClientRule { leaf: "auth0", kind: ExternalServiceKind::AuthProvider, label: "Auth0" },
ClientRule { leaf: "passport.authenticate", kind: ExternalServiceKind::AuthProvider, label: "Passport.js" },
ClientRule { leaf: "OAuth2Client", kind: ExternalServiceKind::AuthProvider, label: "OAuth2 client" },
ClientRule { leaf: "google.oauth2", kind: ExternalServiceKind::AuthProvider, label: "Google OAuth2" },
ClientRule {
leaf: "auth0",
kind: ExternalServiceKind::AuthProvider,
label: "Auth0",
},
ClientRule {
leaf: "passport.authenticate",
kind: ExternalServiceKind::AuthProvider,
label: "Passport.js",
},
ClientRule {
leaf: "OAuth2Client",
kind: ExternalServiceKind::AuthProvider,
label: "OAuth2 client",
},
ClientRule {
leaf: "google.oauth2",
kind: ExternalServiceKind::AuthProvider,
label: "Google OAuth2",
},
// SMTP
ClientRule { leaf: "smtplib.SMTP", kind: ExternalServiceKind::HttpApi, label: "SMTP (Python)" },
ClientRule { leaf: "Mail::send", kind: ExternalServiceKind::HttpApi, label: "Laravel Mail" },
ClientRule { leaf: "ActionMailer", kind: ExternalServiceKind::HttpApi, label: "Rails ActionMailer" },
ClientRule {
leaf: "smtplib.SMTP",
kind: ExternalServiceKind::HttpApi,
label: "SMTP (Python)",
},
ClientRule {
leaf: "Mail::send",
kind: ExternalServiceKind::HttpApi,
label: "Laravel Mail",
},
ClientRule {
leaf: "ActionMailer",
kind: ExternalServiceKind::HttpApi,
label: "Rails ActionMailer",
},
// DNS
ClientRule { leaf: "socket.gethostbyname", kind: ExternalServiceKind::HttpApi, label: "DNS resolver" },
ClientRule { leaf: "dns.lookup", kind: ExternalServiceKind::HttpApi, label: "DNS resolver" },
ClientRule { leaf: "net.LookupIP", kind: ExternalServiceKind::HttpApi, label: "DNS resolver" },
ClientRule {
leaf: "socket.gethostbyname",
kind: ExternalServiceKind::HttpApi,
label: "DNS resolver",
},
ClientRule {
leaf: "dns.lookup",
kind: ExternalServiceKind::HttpApi,
label: "DNS resolver",
},
ClientRule {
leaf: "net.LookupIP",
kind: ExternalServiceKind::HttpApi,
label: "DNS resolver",
},
// Type-qualified — fires when the SSA type-fact engine resolves a
// receiver to `TypeKind::HttpClient` regardless of the bare callee
// name (`session = requests.Session(); session.get(url)` →
// typed_call_receivers maps the `.get` ordinal to "HttpClient", so
// the bound-receiver call surfaces as an outbound HTTP node even
// though `requests.get` is the only direct-import rule above).
ClientRule { leaf: "HttpClient.get", kind: ExternalServiceKind::HttpApi, label: "HTTP client" },
ClientRule { leaf: "HttpClient.post", kind: ExternalServiceKind::HttpApi, label: "HTTP client" },
ClientRule { leaf: "HttpClient.put", kind: ExternalServiceKind::HttpApi, label: "HTTP client" },
ClientRule { leaf: "HttpClient.delete", kind: ExternalServiceKind::HttpApi, label: "HTTP client" },
ClientRule { leaf: "HttpClient.patch", kind: ExternalServiceKind::HttpApi, label: "HTTP client" },
ClientRule { leaf: "HttpClient.request", kind: ExternalServiceKind::HttpApi, label: "HTTP client" },
ClientRule { leaf: "HttpClient.head", kind: ExternalServiceKind::HttpApi, label: "HTTP client" },
ClientRule { leaf: "HttpClient.options", kind: ExternalServiceKind::HttpApi, label: "HTTP client" },
ClientRule { leaf: "RequestBuilder.send", kind: ExternalServiceKind::HttpApi, label: "HTTP request builder" },
ClientRule { leaf: "URL.openConnection", kind: ExternalServiceKind::HttpApi, label: "URL connection" },
ClientRule { leaf: "URL.openStream", kind: ExternalServiceKind::HttpApi, label: "URL connection" },
ClientRule {
leaf: "HttpClient.get",
kind: ExternalServiceKind::HttpApi,
label: "HTTP client",
},
ClientRule {
leaf: "HttpClient.post",
kind: ExternalServiceKind::HttpApi,
label: "HTTP client",
},
ClientRule {
leaf: "HttpClient.put",
kind: ExternalServiceKind::HttpApi,
label: "HTTP client",
},
ClientRule {
leaf: "HttpClient.delete",
kind: ExternalServiceKind::HttpApi,
label: "HTTP client",
},
ClientRule {
leaf: "HttpClient.patch",
kind: ExternalServiceKind::HttpApi,
label: "HTTP client",
},
ClientRule {
leaf: "HttpClient.request",
kind: ExternalServiceKind::HttpApi,
label: "HTTP client",
},
ClientRule {
leaf: "HttpClient.head",
kind: ExternalServiceKind::HttpApi,
label: "HTTP client",
},
ClientRule {
leaf: "HttpClient.options",
kind: ExternalServiceKind::HttpApi,
label: "HTTP client",
},
ClientRule {
leaf: "RequestBuilder.send",
kind: ExternalServiceKind::HttpApi,
label: "HTTP request builder",
},
ClientRule {
leaf: "URL.openConnection",
kind: ExternalServiceKind::HttpApi,
label: "URL connection",
},
ClientRule {
leaf: "URL.openStream",
kind: ExternalServiceKind::HttpApi,
label: "URL connection",
},
];
/// Walk every function summary's callee list and emit one
@ -109,10 +335,11 @@ const CLIENT_RULES: &[ClientRule] = &[
/// client.get(url)`) that the name-only matcher misses.
pub fn detect_external_services(summaries: &GlobalSummaries) -> Vec<SurfaceNode> {
let mut out: Vec<SurfaceNode> = Vec::new();
let mut seen: std::collections::HashSet<(String, String)> =
std::collections::HashSet::new();
let mut seen: std::collections::HashSet<(String, String)> = std::collections::HashSet::new();
for (key, summary) in summaries.iter() {
let typed = summaries.get_ssa(key).map(|s| s.typed_call_receivers.as_slice());
let typed = summaries
.get_ssa(key)
.map(|s| s.typed_call_receivers.as_slice());
for callee in &summary.callees {
let rule = match_rule(&callee.name).or_else(|| {
typed
@ -161,7 +388,10 @@ fn qualify(container: &str, callee_name: &str) -> String {
}
fn container_for_ordinal(typed: &[(u32, String)], ordinal: u32) -> Option<&str> {
typed.iter().find(|(o, _)| *o == ordinal).map(|(_, c)| c.as_str())
typed
.iter()
.find(|(o, _)| *o == ordinal)
.map(|(_, c)| c.as_str())
}
fn match_rule(callee: &str) -> Option<&'static ClientRule> {

View file

@ -106,10 +106,7 @@ pub fn python_imports_any(bytes: &[u8], modules: &[&str]) -> bool {
let pkg = if let Some(rest) = line.strip_prefix("from ") {
rest.split_whitespace().next().unwrap_or("")
} else if let Some(rest) = line.strip_prefix("import ") {
rest.split([',', ' ', ';'])
.next()
.unwrap_or("")
.trim()
rest.split([',', ' ', ';']).next().unwrap_or("").trim()
} else {
continue;
};
@ -237,7 +234,10 @@ mod tests {
#[test]
fn leaf_matches_handles_dot_and_colon_paths() {
assert!(leaf_matches("flask_login.login_required", &["login_required"]));
assert!(leaf_matches(
"flask_login.login_required",
&["login_required"]
));
assert!(leaf_matches("Auth::JwtRequired", &["JwtRequired"]));
assert!(!leaf_matches("OtherDecorator", &["login_required"]));
}
@ -246,7 +246,10 @@ mod tests {
fn python_imports_any_matches_actual_imports() {
assert!(python_imports_any(b"from flask import Flask\n", &["flask"]));
assert!(python_imports_any(b"import flask\n", &["flask"]));
assert!(python_imports_any(b"from flask.app import Flask\n", &["flask"]));
assert!(python_imports_any(
b"from flask.app import Flask\n",
&["flask"]
));
assert!(python_imports_any(b"import django.urls\n", &["django"]));
// Comment-only mention must not match.
assert!(!python_imports_any(b"# flask is great\n", &["flask"]));
@ -260,10 +263,7 @@ mod tests {
fn rust_uses_any_matches_use_statements() {
assert!(rust_uses_any(b"use actix_web::web;\n", &["actix_web"]));
assert!(rust_uses_any(b"use actix_web;\n", &["actix_web"]));
assert!(rust_uses_any(
b"pub use axum::Router;\n",
&["axum"]
));
assert!(rust_uses_any(b"pub use axum::Router;\n", &["axum"]));
assert!(rust_uses_any(
b"pub(crate) use axum::extract::Path;\n",
&["axum"]

View file

@ -21,8 +21,8 @@ use tree_sitter::{Node, Tree};
pub use crate::auth_analysis::auth_markers::GIN_MIDDLEWARES as AUTH_MIDDLEWARES;
const VERBS: &[&str] = &[
"GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD", "Any",
"Get", "Post", "Put", "Delete", "Patch", "Options", "Head",
"GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD", "Any", "Get", "Post", "Put",
"Delete", "Patch", "Options", "Head",
];
pub fn detect_gin_routes(
@ -73,7 +73,9 @@ fn match_gin_call(call: Node, bytes: &[u8], file_rel: &str) -> Option<SurfaceNod
.children(&mut cursor)
.filter(|n| !matches!(n.kind(), "(" | ")" | ","))
.collect();
let route = positional.first().and_then(|n| string_node_value(*n, bytes))?;
let route = positional
.first()
.and_then(|n| string_node_value(*n, bytes))?;
let handler_node = positional.iter().rev().find(|n| {
matches!(
n.kind(),

View file

@ -141,7 +141,10 @@ fn class_has_auth_annotation(class: Node, bytes: &[u8]) -> bool {
}
if let Some(name) = annotation_name(ann, bytes) {
let leaf = name.rsplit('.').next().unwrap_or(&name);
if AUTH_ANNOTATIONS.iter().any(|a| leaf.eq_ignore_ascii_case(a)) {
if AUTH_ANNOTATIONS
.iter()
.any(|a| leaf.eq_ignore_ascii_case(a))
{
return true;
}
}
@ -149,7 +152,11 @@ fn class_has_auth_annotation(class: Node, bytes: &[u8]) -> bool {
false
}
fn method_mapping(method: Node, bytes: &[u8], class_path: &str) -> Option<(HttpMethod, String, bool)> {
fn method_mapping(
method: Node,
bytes: &[u8],
class_path: &str,
) -> Option<(HttpMethod, String, bool)> {
let modifiers = crate::surface::lang::common::child_or_named(method, "modifiers")?;
let mut cursor = modifiers.walk();
let mut verb: Option<HttpMethod> = None;
@ -163,7 +170,10 @@ fn method_mapping(method: Node, bytes: &[u8], class_path: &str) -> Option<(HttpM
continue;
};
let leaf = name.rsplit('.').next().unwrap_or(&name);
if let Some((_, m)) = JAXRS_VERBS.iter().find(|(n, _)| n.eq_ignore_ascii_case(leaf)) {
if let Some((_, m)) = JAXRS_VERBS
.iter()
.find(|(n, _)| n.eq_ignore_ascii_case(leaf))
{
verb = Some(*m);
}
if leaf == "Path"
@ -171,7 +181,10 @@ fn method_mapping(method: Node, bytes: &[u8], class_path: &str) -> Option<(HttpM
{
method_path = p;
}
if AUTH_ANNOTATIONS.iter().any(|a| leaf.eq_ignore_ascii_case(a)) {
if AUTH_ANNOTATIONS
.iter()
.any(|a| leaf.eq_ignore_ascii_case(a))
{
auth = true;
}
}
@ -181,7 +194,11 @@ fn method_mapping(method: Node, bytes: &[u8], class_path: &str) -> Option<(HttpM
} else if method_path.is_empty() {
class_path.to_string()
} else {
format!("{}/{}", class_path.trim_end_matches('/'), method_path.trim_start_matches('/'))
format!(
"{}/{}",
class_path.trim_end_matches('/'),
method_path.trim_start_matches('/')
)
};
Some((v, combined, auth))
}
@ -258,7 +275,8 @@ public class GreetResource {
}
"#;
let (tree, bytes) = parse(src);
let nodes = detect_quarkus_routes(&tree, &bytes, &PathBuf::from("GreetResource.java"), None);
let nodes =
detect_quarkus_routes(&tree, &bytes, &PathBuf::from("GreetResource.java"), None);
assert_eq!(nodes.len(), 1);
let SurfaceNode::EntryPoint(ep) = &nodes[0] else {
panic!()

View file

@ -139,7 +139,10 @@ fn class_has_auth_annotation(class: Node, bytes: &[u8]) -> bool {
}
if let Some(name) = annotation_name(ann, bytes)
&& AUTH_ANNOTATIONS.iter().any(|a| {
name.rsplit('.').next().unwrap_or(&name).eq_ignore_ascii_case(a)
name.rsplit('.')
.next()
.unwrap_or(&name)
.eq_ignore_ascii_case(a)
})
{
return true;
@ -148,7 +151,11 @@ fn class_has_auth_annotation(class: Node, bytes: &[u8]) -> bool {
false
}
fn jaxrs_method_mapping(method: Node, bytes: &[u8], class_path: &str) -> Option<(HttpMethod, String, bool)> {
fn jaxrs_method_mapping(
method: Node,
bytes: &[u8],
class_path: &str,
) -> Option<(HttpMethod, String, bool)> {
let modifiers = crate::surface::lang::common::child_or_named(method, "modifiers")?;
let mut cursor = modifiers.walk();
let mut verb: Option<HttpMethod> = None;
@ -162,7 +169,10 @@ fn jaxrs_method_mapping(method: Node, bytes: &[u8], class_path: &str) -> Option<
continue;
};
let leaf = name.rsplit('.').next().unwrap_or(&name);
if let Some((_, m)) = JAXRS_VERBS.iter().find(|(n, _)| n.eq_ignore_ascii_case(leaf)) {
if let Some((_, m)) = JAXRS_VERBS
.iter()
.find(|(n, _)| n.eq_ignore_ascii_case(leaf))
{
verb = Some(*m);
}
if leaf == "Path"
@ -183,7 +193,11 @@ fn jaxrs_method_mapping(method: Node, bytes: &[u8], class_path: &str) -> Option<
} else if method_path.is_empty() {
class_path.to_string()
} else {
format!("{}/{}", class_path.trim_end_matches('/'), method_path.trim_start_matches('/'))
format!(
"{}/{}",
class_path.trim_end_matches('/'),
method_path.trim_start_matches('/')
)
};
Some((v, combined, auth))
}
@ -255,7 +269,8 @@ public class UsersResource {
}
"#;
let (tree, bytes) = parse(src);
let nodes = detect_servlet_routes(&tree, &bytes, &PathBuf::from("UsersResource.java"), None);
let nodes =
detect_servlet_routes(&tree, &bytes, &PathBuf::from("UsersResource.java"), None);
assert!(!nodes.is_empty());
let SurfaceNode::EntryPoint(ep) = &nodes[0] else {
panic!()

View file

@ -46,9 +46,7 @@ pub fn detect_spring_routes(
if member.kind() != "method_declaration" {
continue;
}
if let Some((method, route_path, auth)) =
method_mapping(member, bytes, &class_path)
{
if let Some((method, route_path, auth)) = method_mapping(member, bytes, &class_path) {
let auth_required = class_auth || auth;
let handler_name = method_name(member, bytes).unwrap_or_default();
out.push(SurfaceNode::EntryPoint(EntryPoint {
@ -114,9 +112,7 @@ fn class_has_auth_annotation(class: Node, bytes: &[u8]) -> bool {
continue;
}
if let Some((name, _)) = annotation_name_and_args(ann, bytes)
&& AUTH_ANNOTATIONS
.iter()
.any(|a| leaf_matches(&name, &[a]))
&& AUTH_ANNOTATIONS.iter().any(|a| leaf_matches(&name, &[a]))
{
return true;
}
@ -140,10 +136,7 @@ fn method_mapping(
let Some((name, args_text)) = annotation_name_and_args(ann, bytes) else {
continue;
};
if AUTH_ANNOTATIONS
.iter()
.any(|a| leaf_matches(&name, &[a]))
{
if AUTH_ANNOTATIONS.iter().any(|a| leaf_matches(&name, &[a])) {
auth = true;
}
if found.is_some() {
@ -156,7 +149,11 @@ fn method_mapping(
// Class-only mapping; method has no path.
method_route = class_path.to_string();
} else if !class_path.is_empty() {
method_route = format!("{}/{}", class_path.trim_end_matches('/'), method_route.trim_start_matches('/'));
method_route = format!(
"{}/{}",
class_path.trim_end_matches('/'),
method_route.trim_start_matches('/')
);
}
let method = default_method
.or_else(|| extract_request_method_from_args(&args_text))
@ -171,10 +168,7 @@ fn method_mapping(
}
fn is_annotation(node: Node) -> bool {
matches!(
node.kind(),
"annotation" | "marker_annotation"
)
matches!(node.kind(), "annotation" | "marker_annotation")
}
/// Returns `(annotation_name, raw_args_text)` for an annotation node.
@ -253,7 +247,8 @@ public class UserController {
}
"#;
let (tree, bytes) = parse(src);
let nodes = detect_spring_routes(&tree, &bytes, &PathBuf::from("UserController.java"), None);
let nodes =
detect_spring_routes(&tree, &bytes, &PathBuf::from("UserController.java"), None);
assert_eq!(nodes.len(), 1);
let SurfaceNode::EntryPoint(ep) = &nodes[0] else {
panic!()

View file

@ -153,10 +153,7 @@ fn arg_is_auth_marker(node: Node, bytes: &[u8]) -> bool {
fn receiver_is_express(object: Node, bytes: &[u8], has_express_witness: bool) -> bool {
fn name_matches_strong(text: &str) -> bool {
let lower = text.to_ascii_lowercase();
lower == "app"
|| lower == "server"
|| lower.ends_with("_app")
|| lower.ends_with("api")
lower == "app" || lower == "server" || lower.ends_with("_app") || lower.ends_with("api")
}
fn name_matches_router(text: &str) -> bool {
let lower = text.to_ascii_lowercase();
@ -239,7 +236,10 @@ mod tests {
let src = "const Router = require('@koa/router');\nconst router = new Router();\nrouter.get('/users', async ctx => {});\n";
let (tree, bytes) = parse(src);
let nodes = detect_express_routes(&tree, &bytes, &PathBuf::from("server.js"), None);
assert!(nodes.is_empty(), "express probe FP'd on koa-only file: {nodes:?}");
assert!(
nodes.is_empty(),
"express probe FP'd on koa-only file: {nodes:?}"
);
}
#[test]

View file

@ -12,26 +12,26 @@
pub mod common;
pub mod python_flask;
pub mod python_fastapi;
pub mod python_django;
pub mod python_fastapi;
pub mod python_flask;
pub mod js_express;
pub mod js_koa;
pub mod ts_next;
pub mod java_spring;
pub mod java_servlet;
pub mod java_quarkus;
pub mod java_servlet;
pub mod java_spring;
pub mod go_http;
pub mod go_gin;
pub mod go_http;
pub mod php_laravel;
pub mod php_slim;
pub mod ruby_sinatra;
pub mod ruby_rails;
pub mod ruby_sinatra;
pub mod rust_actix;
pub mod rust_axum;

View file

@ -119,7 +119,9 @@ fn check_chained_middleware(call: Node, bytes: &[u8]) -> bool {
&& name_text == "middleware"
&& let Some(args) = p.child_by_field_name("arguments")
&& let Ok(args_text) = args.utf8_text(bytes)
&& (args_text.contains("auth") || args_text.contains("jwt") || args_text.contains("authenticated"))
&& (args_text.contains("auth")
|| args_text.contains("jwt")
|| args_text.contains("authenticated"))
{
return true;
}

View file

@ -60,7 +60,13 @@ pub fn detect_django_routes(
let file_rel = rel_file(path, scan_root);
let mut out = Vec::new();
let function_index = collect_function_definitions(tree.root_node(), bytes);
detect_url_dispatch(tree.root_node(), bytes, &file_rel, &function_index, &mut out);
detect_url_dispatch(
tree.root_node(),
bytes,
&file_rel,
&function_index,
&mut out,
);
detect_class_based_views(tree.root_node(), bytes, &file_rel, &mut out);
out
}
@ -178,16 +184,9 @@ fn parse_url_call(call: Node, bytes: &[u8]) -> Option<(String, String)> {
Some((route?, handler?))
}
fn detect_class_based_views(
root: Node,
bytes: &[u8],
file_rel: &str,
out: &mut Vec<SurfaceNode>,
) {
fn detect_class_based_views(root: Node, bytes: &[u8], file_rel: &str, out: &mut Vec<SurfaceNode>) {
fn recurse(node: Node, bytes: &[u8], file_rel: &str, out: &mut Vec<SurfaceNode>) {
if node.kind() == "class_definition"
&& class_is_django_view(node, bytes)
{
if node.kind() == "class_definition" && class_is_django_view(node, bytes) {
let class_auth = class_has_auth_permission(node, bytes);
// Walk the body for HTTP-named methods.
if let Some(body) = node.child_by_field_name("body") {

View file

@ -17,9 +17,7 @@
use crate::entry_points::HttpMethod;
use crate::surface::lang::common::python_imports_any;
use crate::surface::{
EntryPoint, Framework, SourceLocation, SurfaceNode, relative_path_string,
};
use crate::surface::{EntryPoint, Framework, SourceLocation, SurfaceNode, relative_path_string};
use std::path::Path;
use tree_sitter::{Node, Tree};
@ -273,9 +271,7 @@ fn decorator_is_auth_marker(decorator: Node, bytes: &[u8]) -> bool {
return false;
};
let leaf = text.rsplit('.').next().unwrap_or(text).trim();
AUTH_DECORATORS
.iter()
.any(|d| leaf.eq_ignore_ascii_case(d))
AUTH_DECORATORS.iter().any(|d| leaf.eq_ignore_ascii_case(d))
}
/// Read the function name from a `function_definition` node.

View file

@ -42,37 +42,35 @@ fn detect_routes_dsl(root: Node, bytes: &[u8], file_rel: &str, out: &mut Vec<Sur
fn recurse(node: Node, bytes: &[u8], file_rel: &str, out: &mut Vec<SurfaceNode>) {
if matches!(node.kind(), "call" | "method_call")
&& let Some(method_node) = node.child_by_field_name("method")
&& let Ok(method_text) = method_node.utf8_text(bytes)
&& let Some((_, method)) = VERBS.iter().find(|(v, _)| *v == method_text)
{
let args_opt = node
.child_by_field_name("arguments")
.or_else(|| {
let mut c = node.walk();
node.children(&mut c).find(|n| n.kind() == "argument_list")
});
if let Some(args) = args_opt {
let mut cursor = args.walk();
let positional: Vec<Node> = args.named_children(&mut cursor).collect();
if let Some(route_node) = positional.first()
&& let Some(route) = string_node_value(*route_node, bytes)
{
let handler_name = positional
.iter()
.find_map(|n| extract_to_handler(*n, bytes))
.unwrap_or_default();
out.push(SurfaceNode::EntryPoint(EntryPoint {
location: loc_for(node, file_rel),
framework: Framework::Rails,
method: *method,
route,
handler_name,
handler_location: loc_for(node, file_rel),
auth_required: false,
}));
}
&& let Ok(method_text) = method_node.utf8_text(bytes)
&& let Some((_, method)) = VERBS.iter().find(|(v, _)| *v == method_text)
{
let args_opt = node.child_by_field_name("arguments").or_else(|| {
let mut c = node.walk();
node.children(&mut c).find(|n| n.kind() == "argument_list")
});
if let Some(args) = args_opt {
let mut cursor = args.walk();
let positional: Vec<Node> = args.named_children(&mut cursor).collect();
if let Some(route_node) = positional.first()
&& let Some(route) = string_node_value(*route_node, bytes)
{
let handler_name = positional
.iter()
.find_map(|n| extract_to_handler(*n, bytes))
.unwrap_or_default();
out.push(SurfaceNode::EntryPoint(EntryPoint {
location: loc_for(node, file_rel),
framework: Framework::Rails,
method: *method,
route,
handler_name,
handler_location: loc_for(node, file_rel),
auth_required: false,
}));
}
}
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
recurse(child, bytes, file_rel, out);
@ -109,9 +107,7 @@ fn extract_to_handler(node: Node, bytes: &[u8]) -> Option<String> {
fn detect_controllers(root: Node, bytes: &[u8], file_rel: &str, out: &mut Vec<SurfaceNode>) {
fn recurse(node: Node, bytes: &[u8], file_rel: &str, out: &mut Vec<SurfaceNode>) {
if node.kind() == "class"
&& class_is_controller(node, bytes)
{
if node.kind() == "class" && class_is_controller(node, bytes) {
let class_auth = class_has_before_authenticate(node, bytes);
walk_methods(node, bytes, &mut |method_node, name| {
out.push(SurfaceNode::EntryPoint(EntryPoint {

View file

@ -50,24 +50,18 @@ fn walk_calls<'tree, F: FnMut(Node<'tree>)>(node: Node<'tree>, visit: &mut F) {
fn match_sinatra_call(call: Node, bytes: &[u8], file_rel: &str) -> Option<SurfaceNode> {
let method_name_node = call.child_by_field_name("method")?;
let method_text = method_name_node.utf8_text(bytes).ok()?;
let (_, method) = VERBS
.iter()
.find(|(v, _)| *v == method_text)?;
let (_, method) = VERBS.iter().find(|(v, _)| *v == method_text)?;
// Must have a block to be a Sinatra route.
let block = call
.child_by_field_name("block")
.or_else(|| {
let mut c = call.walk();
call.children(&mut c)
.find(|n| matches!(n.kind(), "do_block" | "block"))
})?;
let block = call.child_by_field_name("block").or_else(|| {
let mut c = call.walk();
call.children(&mut c)
.find(|n| matches!(n.kind(), "do_block" | "block"))
})?;
// Args: Sinatra accepts a string literal as the first positional arg.
let args = call
.child_by_field_name("arguments")
.or_else(|| {
let mut c = call.walk();
call.children(&mut c).find(|n| n.kind() == "argument_list")
})?;
let args = call.child_by_field_name("arguments").or_else(|| {
let mut c = call.walk();
call.children(&mut c).find(|n| n.kind() == "argument_list")
})?;
let mut cursor = args.walk();
let route_node = args.named_children(&mut cursor).next()?;
let route = string_node_value(route_node, bytes)?;

View file

@ -68,9 +68,7 @@ fn match_actix_function(func: Node, bytes: &[u8], file_rel: &str) -> Option<Surf
let mut route_path = String::new();
for attr in attrs {
let raw = attr.utf8_text(bytes).ok()?;
let inner = raw
.trim_start_matches(['#', '!'])
.trim_matches(['[', ']']);
let inner = raw.trim_start_matches(['#', '!']).trim_matches(['[', ']']);
for (name, default_method) in ROUTE_MACROS {
let prefix = format!("{}(", name);
if inner.starts_with(&prefix) {

View file

@ -299,12 +299,7 @@ mod tests {
fn detects_app_router_get() {
let src = "export async function GET(req: Request) { return new Response('ok'); }\n";
let (tree, bytes) = parse(src);
let nodes = detect_next_routes(
&tree,
&bytes,
&PathBuf::from("app/users/route.ts"),
None,
);
let nodes = detect_next_routes(&tree, &bytes, &PathBuf::from("app/users/route.ts"), None);
assert_eq!(nodes.len(), 1);
let SurfaceNode::EntryPoint(ep) = &nodes[0] else {
panic!()

View file

@ -342,9 +342,10 @@ impl SurfaceMap {
/// scan root or when path stripping fails.
pub fn relative_path_string(path: &Path, scan_root: Option<&Path>) -> String {
if let Some(root) = scan_root
&& let Ok(rel) = path.strip_prefix(root) {
return rel.to_string_lossy().replace('\\', "/");
}
&& let Ok(rel) = path.strip_prefix(root)
{
return rel.to_string_lossy().replace('\\', "/");
}
path.to_string_lossy().replace('\\', "/")
}

View file

@ -77,9 +77,7 @@ pub fn populate_reaches_edges(
.index
.iter()
.filter(|(k, _)| k.name == ep.handler_name)
.filter(|(k, _)| {
file_part_of_namespace(&k.namespace) == ep.handler_location.file
})
.filter(|(k, _)| file_part_of_namespace(&k.namespace) == ep.handler_location.file)
.map(|(_, idx)| *idx)
.collect::<Vec<_>>();
@ -217,9 +215,6 @@ mod tests {
"src/file.ts"
);
// Last `::` wins, matching `namespace_with_package`'s shape.
assert_eq!(
file_part_of_namespace("@a/b::@c/d::lib/x.ts"),
"lib/x.ts"
);
assert_eq!(file_part_of_namespace("@a/b::@c/d::lib/x.ts"), "lib/x.ts");
}
}