mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-09 19:45:13 +02:00
[pitboss/grind] deferred session-0017 (20260522T163126Z-7d60)
This commit is contained in:
parent
5cb8056250
commit
1e5f27f56d
8 changed files with 1231 additions and 8 deletions
247
src/dynamic/framework/adapters/crypto_go.rs
Normal file
247
src/dynamic/framework/adapters/crypto_go.rs
Normal file
|
|
@ -0,0 +1,247 @@
|
|||
//! Go [`super::super::FrameworkAdapter`] matching weak-crypto sink
|
||||
//! constructions (`math/rand.Int*` non-CSPRNG randomness used for
|
||||
//! key material, `crypto/md5.Sum` / `crypto/sha1.Sum` /
|
||||
//! `crypto/des.NewCipher` / `crypto/rc4.NewCipher`).
|
||||
//!
|
||||
//! Phase 11 (Track L.9). Fires when the function body invokes one
|
||||
//! of the canonical Go weak-crypto entry points and the surrounding
|
||||
//! source imports the matching stdlib module.
|
||||
//!
|
||||
//! See sibling adapters [`super::crypto_python::CryptoPythonAdapter`],
|
||||
//! [`super::crypto_java::CryptoJavaAdapter`], and
|
||||
//! [`super::crypto_ruby::CryptoRubyAdapter`] for the same shape on
|
||||
//! other languages.
|
||||
|
||||
use crate::dynamic::framework::{FrameworkAdapter, FrameworkBinding};
|
||||
use crate::evidence::EntryKind;
|
||||
use crate::summary::FuncSummary;
|
||||
use crate::symbol::Lang;
|
||||
|
||||
pub struct CryptoGoAdapter;
|
||||
|
||||
const ADAPTER_NAME: &str = "crypto-go";
|
||||
|
||||
fn callee_is_weak_crypto(name: &str) -> bool {
|
||||
let last = name.rsplit_once('.').map(|(_, s)| s).unwrap_or(name);
|
||||
matches!(
|
||||
last,
|
||||
"Int"
|
||||
| "Intn"
|
||||
| "Int31"
|
||||
| "Int31n"
|
||||
| "Int63"
|
||||
| "Int63n"
|
||||
| "Uint32"
|
||||
| "Uint64"
|
||||
| "Float32"
|
||||
| "Float64"
|
||||
| "Read"
|
||||
| "Sum"
|
||||
| "New"
|
||||
| "NewCipher"
|
||||
) || matches!(
|
||||
name,
|
||||
"rand.Int"
|
||||
| "rand.Intn"
|
||||
| "rand.Int31"
|
||||
| "rand.Int31n"
|
||||
| "rand.Int63"
|
||||
| "rand.Int63n"
|
||||
| "rand.Uint32"
|
||||
| "rand.Uint64"
|
||||
| "rand.Float32"
|
||||
| "rand.Float64"
|
||||
| "rand.Read"
|
||||
| "md5.Sum"
|
||||
| "md5.New"
|
||||
| "sha1.Sum"
|
||||
| "sha1.New"
|
||||
| "des.NewCipher"
|
||||
| "des.NewTripleDESCipher"
|
||||
| "rc4.NewCipher"
|
||||
)
|
||||
}
|
||||
|
||||
fn source_imports_go_crypto(file_bytes: &[u8]) -> bool {
|
||||
const NEEDLES: &[&[u8]] = &[
|
||||
b"\"math/rand\"",
|
||||
b"math/rand\"",
|
||||
b"\"crypto/md5\"",
|
||||
b"crypto/md5\"",
|
||||
b"\"crypto/sha1\"",
|
||||
b"crypto/sha1\"",
|
||||
b"\"crypto/des\"",
|
||||
b"crypto/des\"",
|
||||
b"\"crypto/rc4\"",
|
||||
b"crypto/rc4\"",
|
||||
];
|
||||
NEEDLES
|
||||
.iter()
|
||||
.any(|n| file_bytes.windows(n.len()).any(|w| w == *n))
|
||||
}
|
||||
|
||||
/// Returns `true` when the surrounding source visibly routes the
|
||||
/// crypto call through a hardened path (`crypto/rand` CSPRNG,
|
||||
/// `crypto/sha256` or stronger, `crypto/aes` paired with `GCM`,
|
||||
/// `golang.org/x/crypto/chacha20poly1305`).
|
||||
fn source_routed_through_strong_path(file_bytes: &[u8]) -> bool {
|
||||
const NEEDLES: &[&[u8]] = &[
|
||||
b"\"crypto/rand\"",
|
||||
b"crypto/rand\"",
|
||||
b"\"crypto/sha256\"",
|
||||
b"crypto/sha256\"",
|
||||
b"\"crypto/sha512\"",
|
||||
b"crypto/sha512\"",
|
||||
b"sha3.New",
|
||||
b"chacha20poly1305",
|
||||
b"cipher.NewGCM",
|
||||
b"argon2.Key",
|
||||
b"argon2.IDKey",
|
||||
b"bcrypt.GenerateFromPassword",
|
||||
];
|
||||
NEEDLES
|
||||
.iter()
|
||||
.any(|n| file_bytes.windows(n.len()).any(|w| w == *n))
|
||||
}
|
||||
|
||||
impl FrameworkAdapter for CryptoGoAdapter {
|
||||
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> {
|
||||
if source_routed_through_strong_path(file_bytes) {
|
||||
return None;
|
||||
}
|
||||
let matches_call = super::any_callee_matches(summary, callee_is_weak_crypto);
|
||||
let matches_source = source_imports_go_crypto(file_bytes);
|
||||
if matches_call && matches_source {
|
||||
Some(FrameworkBinding {
|
||||
adapter: ADAPTER_NAME.to_owned(),
|
||||
kind: EntryKind::Function,
|
||||
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_math_rand_intn() {
|
||||
let src: &[u8] = b"package vuln\nimport \"math/rand\"\nfunc Run() int {\n return rand.Intn(1000)\n}\n";
|
||||
let tree = parse_go(src);
|
||||
let summary = FuncSummary {
|
||||
name: "Run".into(),
|
||||
callees: vec![crate::summary::CalleeSite::bare("rand.Intn")],
|
||||
..Default::default()
|
||||
};
|
||||
assert!(
|
||||
CryptoGoAdapter
|
||||
.detect(&summary, tree.root_node(), src)
|
||||
.is_some()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fires_on_md5_sum() {
|
||||
let src: &[u8] = b"package vuln\nimport \"crypto/md5\"\nfunc Sign(b []byte) [16]byte {\n return md5.Sum(b)\n}\n";
|
||||
let tree = parse_go(src);
|
||||
let summary = FuncSummary {
|
||||
name: "Sign".into(),
|
||||
callees: vec![crate::summary::CalleeSite::bare("md5.Sum")],
|
||||
..Default::default()
|
||||
};
|
||||
assert!(
|
||||
CryptoGoAdapter
|
||||
.detect(&summary, tree.root_node(), src)
|
||||
.is_some()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fires_on_des_newcipher() {
|
||||
let src: &[u8] = b"package vuln\nimport \"crypto/des\"\nimport \"crypto/cipher\"\nfunc Enc(key []byte) (cipher.Block, error) {\n return des.NewCipher(key)\n}\n";
|
||||
let tree = parse_go(src);
|
||||
let summary = FuncSummary {
|
||||
name: "Enc".into(),
|
||||
callees: vec![crate::summary::CalleeSite::bare("des.NewCipher")],
|
||||
..Default::default()
|
||||
};
|
||||
assert!(
|
||||
CryptoGoAdapter
|
||||
.detect(&summary, tree.root_node(), src)
|
||||
.is_some()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skips_when_source_routes_through_crypto_rand() {
|
||||
let src: &[u8] = b"package vuln\nimport \"math/rand\"\nimport \"crypto/rand\"\nfunc Run() ([]byte, error) {\n key := make([]byte, 32)\n if _, err := rand.Read(key); err != nil { return nil, err }\n return key, nil\n}\n";
|
||||
let tree = parse_go(src);
|
||||
let summary = FuncSummary {
|
||||
name: "Run".into(),
|
||||
callees: vec![crate::summary::CalleeSite::bare("rand.Read")],
|
||||
..Default::default()
|
||||
};
|
||||
assert!(
|
||||
CryptoGoAdapter
|
||||
.detect(&summary, tree.root_node(), src)
|
||||
.is_none()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skips_when_sha256_in_source() {
|
||||
let src: &[u8] = b"package vuln\nimport \"crypto/sha256\"\nfunc Sign(b []byte) [32]byte {\n return sha256.Sum256(b)\n}\n";
|
||||
let tree = parse_go(src);
|
||||
let summary = FuncSummary {
|
||||
name: "Sign".into(),
|
||||
callees: vec![crate::summary::CalleeSite::bare("sha256.Sum256")],
|
||||
..Default::default()
|
||||
};
|
||||
assert!(
|
||||
CryptoGoAdapter
|
||||
.detect(&summary, tree.root_node(), src)
|
||||
.is_none()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skips_plain_function() {
|
||||
let src: &[u8] = b"package vuln\nfunc Add(a, b int) int { return a + b }\n";
|
||||
let tree = parse_go(src);
|
||||
let summary = FuncSummary {
|
||||
name: "Add".into(),
|
||||
..Default::default()
|
||||
};
|
||||
assert!(
|
||||
CryptoGoAdapter
|
||||
.detect(&summary, tree.root_node(), src)
|
||||
.is_none()
|
||||
);
|
||||
}
|
||||
}
|
||||
254
src/dynamic/framework/adapters/crypto_rust.rs
Normal file
254
src/dynamic/framework/adapters/crypto_rust.rs
Normal file
|
|
@ -0,0 +1,254 @@
|
|||
//! Rust [`super::super::FrameworkAdapter`] matching weak-crypto sink
|
||||
//! constructions (`md5::compute` / `Md5::digest`, `sha1::Sha1::digest`,
|
||||
//! `rand::random` / non-CSPRNG `rand::Rng::gen_*`, `crypto::des` DES /
|
||||
//! `crypto::rc4` RC4 ciphers).
|
||||
//!
|
||||
//! Phase 11 (Track L.9). Fires when the function body invokes one
|
||||
//! of the canonical Rust weak-crypto entry points and the surrounding
|
||||
//! source imports the matching crate.
|
||||
//!
|
||||
//! See sibling adapters [`super::crypto_python::CryptoPythonAdapter`],
|
||||
//! [`super::crypto_java::CryptoJavaAdapter`],
|
||||
//! [`super::crypto_ruby::CryptoRubyAdapter`], and
|
||||
//! [`super::crypto_go::CryptoGoAdapter`] for the same shape on other
|
||||
//! languages.
|
||||
|
||||
use crate::dynamic::framework::{FrameworkAdapter, FrameworkBinding};
|
||||
use crate::evidence::EntryKind;
|
||||
use crate::summary::FuncSummary;
|
||||
use crate::symbol::Lang;
|
||||
|
||||
pub struct CryptoRustAdapter;
|
||||
|
||||
const ADAPTER_NAME: &str = "crypto-rust";
|
||||
|
||||
fn callee_is_weak_crypto(name: &str) -> bool {
|
||||
let last = name.rsplit_once("::").map(|(_, s)| s).unwrap_or(name);
|
||||
let last = last.rsplit_once('.').map(|(_, s)| s).unwrap_or(last);
|
||||
matches!(
|
||||
last,
|
||||
"compute"
|
||||
| "digest"
|
||||
| "finalize"
|
||||
| "random"
|
||||
| "gen"
|
||||
| "gen_range"
|
||||
| "gen_bool"
|
||||
| "thread_rng"
|
||||
| "new_unkeyed"
|
||||
) || matches!(
|
||||
name,
|
||||
"md5::compute"
|
||||
| "Md5::digest"
|
||||
| "Md5::new"
|
||||
| "md_5::Md5::digest"
|
||||
| "md_5::Md5::new"
|
||||
| "sha1::Sha1::digest"
|
||||
| "sha1::Sha1::new"
|
||||
| "Sha1::digest"
|
||||
| "Sha1::new"
|
||||
| "rand::random"
|
||||
| "rand::thread_rng"
|
||||
| "rand::Rng::gen"
|
||||
| "rand::Rng::gen_range"
|
||||
| "rand::rngs::ThreadRng::gen"
|
||||
| "Des::new"
|
||||
| "TdesEde3::new"
|
||||
| "Rc4::new"
|
||||
)
|
||||
}
|
||||
|
||||
fn source_imports_rust_crypto(file_bytes: &[u8]) -> bool {
|
||||
const NEEDLES: &[&[u8]] = &[
|
||||
b"use md5",
|
||||
b"use md_5",
|
||||
b"use sha1",
|
||||
b"use sha_1",
|
||||
b"use rand",
|
||||
b"md5::",
|
||||
b"md_5::Md5",
|
||||
b"sha1::Sha1",
|
||||
b"sha_1::Sha1",
|
||||
b"rand::random",
|
||||
b"rand::thread_rng",
|
||||
b"rand::Rng",
|
||||
b"des::Des",
|
||||
b"rc4::Rc4",
|
||||
];
|
||||
NEEDLES
|
||||
.iter()
|
||||
.any(|n| file_bytes.windows(n.len()).any(|w| w == *n))
|
||||
}
|
||||
|
||||
/// Returns `true` when the surrounding source visibly routes the
|
||||
/// crypto call through a hardened path (CSPRNG via `getrandom` /
|
||||
/// `OsRng`, SHA-256+ digests, AES-GCM / ChaCha20-Poly1305 / Argon2
|
||||
/// authenticated encryption + KDF, `ring` constants).
|
||||
fn source_routed_through_strong_path(file_bytes: &[u8]) -> bool {
|
||||
const NEEDLES: &[&[u8]] = &[
|
||||
b"getrandom::getrandom",
|
||||
b"rand::rngs::OsRng",
|
||||
b"OsRng",
|
||||
b"sha2::Sha256",
|
||||
b"sha2::Sha384",
|
||||
b"sha2::Sha512",
|
||||
b"sha3::Sha3_256",
|
||||
b"sha3::Sha3_512",
|
||||
b"ring::digest::SHA256",
|
||||
b"ring::digest::SHA384",
|
||||
b"ring::digest::SHA512",
|
||||
b"aes_gcm",
|
||||
b"AesGcm",
|
||||
b"chacha20poly1305",
|
||||
b"ChaCha20Poly1305",
|
||||
b"argon2::Argon2",
|
||||
b"argon2::PasswordHash",
|
||||
b"bcrypt::hash",
|
||||
b"ed25519_dalek",
|
||||
];
|
||||
NEEDLES
|
||||
.iter()
|
||||
.any(|n| file_bytes.windows(n.len()).any(|w| w == *n))
|
||||
}
|
||||
|
||||
impl FrameworkAdapter for CryptoRustAdapter {
|
||||
fn name(&self) -> &'static str {
|
||||
ADAPTER_NAME
|
||||
}
|
||||
|
||||
fn lang(&self) -> Lang {
|
||||
Lang::Rust
|
||||
}
|
||||
|
||||
fn detect(
|
||||
&self,
|
||||
summary: &FuncSummary,
|
||||
_ast: tree_sitter::Node<'_>,
|
||||
file_bytes: &[u8],
|
||||
) -> Option<FrameworkBinding> {
|
||||
if source_routed_through_strong_path(file_bytes) {
|
||||
return None;
|
||||
}
|
||||
let matches_call = super::any_callee_matches(summary, callee_is_weak_crypto);
|
||||
let matches_source = source_imports_rust_crypto(file_bytes);
|
||||
if matches_call && matches_source {
|
||||
Some(FrameworkBinding {
|
||||
adapter: ADAPTER_NAME.to_owned(),
|
||||
kind: EntryKind::Function,
|
||||
route: None,
|
||||
request_params: Vec::new(),
|
||||
response_writer: None,
|
||||
middleware: Vec::new(),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn parse_rust(src: &[u8]) -> tree_sitter::Tree {
|
||||
let mut parser = tree_sitter::Parser::new();
|
||||
let lang = tree_sitter::Language::from(tree_sitter_rust::LANGUAGE);
|
||||
parser.set_language(&lang).unwrap();
|
||||
parser.parse(src, None).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fires_on_md5_compute() {
|
||||
let src: &[u8] = b"use md5;\npub fn sign(value: &[u8]) -> md5::Digest {\n md5::compute(value)\n}\n";
|
||||
let tree = parse_rust(src);
|
||||
let summary = FuncSummary {
|
||||
name: "sign".into(),
|
||||
callees: vec![crate::summary::CalleeSite::bare("md5::compute")],
|
||||
..Default::default()
|
||||
};
|
||||
assert!(
|
||||
CryptoRustAdapter
|
||||
.detect(&summary, tree.root_node(), src)
|
||||
.is_some()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fires_on_sha1_digest() {
|
||||
let src: &[u8] = b"use sha1::Sha1;\nuse sha1::Digest;\npub fn sign(value: &[u8]) -> Vec<u8> {\n let mut h = Sha1::new();\n h.update(value);\n h.finalize().to_vec()\n}\n";
|
||||
let tree = parse_rust(src);
|
||||
let summary = FuncSummary {
|
||||
name: "sign".into(),
|
||||
callees: vec![crate::summary::CalleeSite::bare("Sha1::new")],
|
||||
..Default::default()
|
||||
};
|
||||
assert!(
|
||||
CryptoRustAdapter
|
||||
.detect(&summary, tree.root_node(), src)
|
||||
.is_some()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fires_on_rand_random() {
|
||||
let src: &[u8] = b"use rand;\npub fn token() -> u64 {\n rand::random::<u64>()\n}\n";
|
||||
let tree = parse_rust(src);
|
||||
let summary = FuncSummary {
|
||||
name: "token".into(),
|
||||
callees: vec![crate::summary::CalleeSite::bare("rand::random")],
|
||||
..Default::default()
|
||||
};
|
||||
assert!(
|
||||
CryptoRustAdapter
|
||||
.detect(&summary, tree.root_node(), src)
|
||||
.is_some()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skips_when_source_routes_through_osrng() {
|
||||
let src: &[u8] = b"use rand;\nuse rand::rngs::OsRng;\nuse rand::RngCore;\npub fn token() -> [u8; 32] {\n let mut buf = [0u8; 32];\n OsRng.fill_bytes(&mut buf);\n buf\n}\n";
|
||||
let tree = parse_rust(src);
|
||||
let summary = FuncSummary {
|
||||
name: "token".into(),
|
||||
callees: vec![crate::summary::CalleeSite::bare("rand::random")],
|
||||
..Default::default()
|
||||
};
|
||||
assert!(
|
||||
CryptoRustAdapter
|
||||
.detect(&summary, tree.root_node(), src)
|
||||
.is_none()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skips_when_sha256_in_source() {
|
||||
let src: &[u8] = b"use sha2::Sha256;\nuse sha2::Digest;\npub fn sign(value: &[u8]) -> Vec<u8> {\n let mut h = Sha256::new();\n h.update(value);\n h.finalize().to_vec()\n}\n";
|
||||
let tree = parse_rust(src);
|
||||
let summary = FuncSummary {
|
||||
name: "sign".into(),
|
||||
callees: vec![crate::summary::CalleeSite::bare("Sha256::new")],
|
||||
..Default::default()
|
||||
};
|
||||
assert!(
|
||||
CryptoRustAdapter
|
||||
.detect(&summary, tree.root_node(), src)
|
||||
.is_none()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skips_plain_function() {
|
||||
let src: &[u8] = b"pub fn add(a: i64, b: i64) -> i64 { a + b }\n";
|
||||
let tree = parse_rust(src);
|
||||
let summary = FuncSummary {
|
||||
name: "add".into(),
|
||||
..Default::default()
|
||||
};
|
||||
assert!(
|
||||
CryptoRustAdapter
|
||||
.detect(&summary, tree.root_node(), src)
|
||||
.is_none()
|
||||
);
|
||||
}
|
||||
}
|
||||
228
src/dynamic/framework/adapters/data_exfil_java.rs
Normal file
228
src/dynamic/framework/adapters/data_exfil_java.rs
Normal file
|
|
@ -0,0 +1,228 @@
|
|||
//! Java [`super::super::FrameworkAdapter`] matching outbound-HTTP
|
||||
//! sink constructions (`java.net.HttpURLConnection`, the modern
|
||||
//! `java.net.http.HttpClient`, OkHttp, Apache HttpClient).
|
||||
//!
|
||||
//! Phase 11 (Track L.9). Fires when the function body invokes one
|
||||
//! of the canonical Java HTTP-client entry points and the surrounding
|
||||
//! source imports the matching stdlib / third-party module.
|
||||
//!
|
||||
//! See sibling adapters
|
||||
//! [`super::data_exfil_python::DataExfilPythonAdapter`],
|
||||
//! [`super::data_exfil_js::DataExfilJsAdapter`],
|
||||
//! [`super::data_exfil_go::DataExfilGoAdapter`], and
|
||||
//! [`super::data_exfil_ruby::DataExfilRubyAdapter`] for the same
|
||||
//! shape on other languages.
|
||||
|
||||
use crate::dynamic::framework::{FrameworkAdapter, FrameworkBinding};
|
||||
use crate::evidence::EntryKind;
|
||||
use crate::summary::FuncSummary;
|
||||
use crate::symbol::Lang;
|
||||
|
||||
pub struct DataExfilJavaAdapter;
|
||||
|
||||
const ADAPTER_NAME: &str = "data-exfil-java";
|
||||
|
||||
fn callee_is_outbound_http(name: &str) -> bool {
|
||||
let last = name.rsplit_once('.').map(|(_, s)| s).unwrap_or(name);
|
||||
matches!(
|
||||
last,
|
||||
"openConnection"
|
||||
| "openStream"
|
||||
| "send"
|
||||
| "sendAsync"
|
||||
| "execute"
|
||||
| "newCall"
|
||||
| "newBuilder"
|
||||
| "build"
|
||||
| "connect"
|
||||
) || matches!(
|
||||
name,
|
||||
"java.net.URL.openConnection"
|
||||
| "java.net.URL.openStream"
|
||||
| "URL.openConnection"
|
||||
| "URL.openStream"
|
||||
| "HttpClient.send"
|
||||
| "HttpClient.sendAsync"
|
||||
| "HttpClient.newHttpClient"
|
||||
| "HttpRequest.newBuilder"
|
||||
| "OkHttpClient.newCall"
|
||||
| "Call.execute"
|
||||
| "HttpClients.createDefault"
|
||||
| "CloseableHttpClient.execute"
|
||||
| "Request.Builder.url"
|
||||
)
|
||||
}
|
||||
|
||||
fn source_imports_java_http_client(file_bytes: &[u8]) -> bool {
|
||||
const NEEDLES: &[&[u8]] = &[
|
||||
b"java.net.HttpURLConnection",
|
||||
b"java.net.URL",
|
||||
b"java.net.http.HttpClient",
|
||||
b"java.net.http.HttpRequest",
|
||||
b"okhttp3.OkHttpClient",
|
||||
b"okhttp3.Request",
|
||||
b"okhttp3.Call",
|
||||
b"org.apache.http.client.HttpClient",
|
||||
b"org.apache.http.impl.client.HttpClients",
|
||||
b"org.apache.http.impl.client.CloseableHttpClient",
|
||||
b"org.apache.hc.client5.http",
|
||||
];
|
||||
NEEDLES
|
||||
.iter()
|
||||
.any(|n| file_bytes.windows(n.len()).any(|w| w == *n))
|
||||
}
|
||||
|
||||
/// Returns `true` when the surrounding source visibly routes the
|
||||
/// outbound URL through a host-allowlist / network-policy gate.
|
||||
fn host_routed_through_allowlist(file_bytes: &[u8]) -> bool {
|
||||
const NEEDLES: &[&[u8]] = &[
|
||||
b"ALLOWLIST",
|
||||
b"ALLOWED_HOSTS",
|
||||
b"allowedHosts",
|
||||
b"allowlist",
|
||||
b"\"127.0.0.1\"",
|
||||
b"\"localhost\"",
|
||||
b".equals(\"localhost\")",
|
||||
b".contains(host)",
|
||||
b".containsKey(host)",
|
||||
];
|
||||
NEEDLES
|
||||
.iter()
|
||||
.any(|n| file_bytes.windows(n.len()).any(|w| w == *n))
|
||||
}
|
||||
|
||||
impl FrameworkAdapter for DataExfilJavaAdapter {
|
||||
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> {
|
||||
if host_routed_through_allowlist(file_bytes) {
|
||||
return None;
|
||||
}
|
||||
let matches_call = super::any_callee_matches(summary, callee_is_outbound_http);
|
||||
let matches_source = source_imports_java_http_client(file_bytes);
|
||||
if matches_call && matches_source {
|
||||
Some(FrameworkBinding {
|
||||
adapter: ADAPTER_NAME.to_owned(),
|
||||
kind: EntryKind::Function,
|
||||
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_url_open_connection() {
|
||||
let src: &[u8] = b"import java.net.HttpURLConnection;\nimport java.net.URL;\n\
|
||||
public class Vuln {\n public static void run(String host) throws Exception {\n URL u = new URL(\"http://\" + host + \"/exfil\");\n HttpURLConnection conn = (HttpURLConnection) u.openConnection();\n conn.connect();\n }\n}\n";
|
||||
let tree = parse_java(src);
|
||||
let summary = FuncSummary {
|
||||
name: "run".into(),
|
||||
callees: vec![crate::summary::CalleeSite::bare("URL.openConnection")],
|
||||
..Default::default()
|
||||
};
|
||||
assert!(
|
||||
DataExfilJavaAdapter
|
||||
.detect(&summary, tree.root_node(), src)
|
||||
.is_some()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fires_on_httpclient_send() {
|
||||
let src: &[u8] = b"import java.net.http.HttpClient;\nimport java.net.http.HttpRequest;\nimport java.net.URI;\n\
|
||||
public class Vuln {\n public static void run(String host) throws Exception {\n HttpClient c = HttpClient.newHttpClient();\n HttpRequest r = HttpRequest.newBuilder(URI.create(\"http://\" + host)).build();\n c.send(r, java.net.http.HttpResponse.BodyHandlers.discarding());\n }\n}\n";
|
||||
let tree = parse_java(src);
|
||||
let summary = FuncSummary {
|
||||
name: "run".into(),
|
||||
callees: vec![
|
||||
crate::summary::CalleeSite::bare("HttpRequest.newBuilder"),
|
||||
crate::summary::CalleeSite::bare("HttpClient.send"),
|
||||
],
|
||||
..Default::default()
|
||||
};
|
||||
assert!(
|
||||
DataExfilJavaAdapter
|
||||
.detect(&summary, tree.root_node(), src)
|
||||
.is_some()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fires_on_okhttp_newcall_execute() {
|
||||
let src: &[u8] = b"import okhttp3.OkHttpClient;\nimport okhttp3.Request;\n\
|
||||
public class Vuln {\n public static void run(String host) throws Exception {\n OkHttpClient c = new OkHttpClient();\n Request r = new Request.Builder().url(\"http://\" + host).build();\n c.newCall(r).execute();\n }\n}\n";
|
||||
let tree = parse_java(src);
|
||||
let summary = FuncSummary {
|
||||
name: "run".into(),
|
||||
callees: vec![
|
||||
crate::summary::CalleeSite::bare("OkHttpClient.newCall"),
|
||||
crate::summary::CalleeSite::bare("Call.execute"),
|
||||
],
|
||||
..Default::default()
|
||||
};
|
||||
assert!(
|
||||
DataExfilJavaAdapter
|
||||
.detect(&summary, tree.root_node(), src)
|
||||
.is_some()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skips_when_host_in_allowlist_literal() {
|
||||
let src: &[u8] = b"import java.net.HttpURLConnection;\nimport java.net.URL;\n\
|
||||
public class Vuln {\n public static void run(String host) throws Exception {\n if (!host.equals(\"127.0.0.1\")) { return; }\n URL u = new URL(\"http://\" + host + \"/exfil\");\n ((HttpURLConnection) u.openConnection()).connect();\n }\n}\n";
|
||||
let tree = parse_java(src);
|
||||
let summary = FuncSummary {
|
||||
name: "run".into(),
|
||||
callees: vec![crate::summary::CalleeSite::bare("URL.openConnection")],
|
||||
..Default::default()
|
||||
};
|
||||
assert!(
|
||||
DataExfilJavaAdapter
|
||||
.detect(&summary, tree.root_node(), src)
|
||||
.is_none()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skips_plain_method() {
|
||||
let src: &[u8] = b"public class Plain { public static int add(int a, int b) { return a + b; } }\n";
|
||||
let tree = parse_java(src);
|
||||
let summary = FuncSummary {
|
||||
name: "add".into(),
|
||||
..Default::default()
|
||||
};
|
||||
assert!(
|
||||
DataExfilJavaAdapter
|
||||
.detect(&summary, tree.root_node(), src)
|
||||
.is_none()
|
||||
);
|
||||
}
|
||||
}
|
||||
234
src/dynamic/framework/adapters/data_exfil_php.rs
Normal file
234
src/dynamic/framework/adapters/data_exfil_php.rs
Normal file
|
|
@ -0,0 +1,234 @@
|
|||
//! PHP [`super::super::FrameworkAdapter`] matching outbound-HTTP
|
||||
//! sink constructions (`curl_init` / `curl_exec`, `file_get_contents`
|
||||
//! against a remote URL, `fopen`/`fsockopen`/`stream_socket_client`,
|
||||
//! Guzzle).
|
||||
//!
|
||||
//! Phase 11 (Track L.9). Fires when the function body invokes one
|
||||
//! of the canonical PHP HTTP-client entry points and the surrounding
|
||||
//! source carries the `<?php` script-tag opener (PHP files always
|
||||
//! open with a literal `<?php` tag in production code, mirroring the
|
||||
//! [`super::crypto_php::CryptoPhpAdapter`] source-signal pattern).
|
||||
//!
|
||||
//! See sibling adapters
|
||||
//! [`super::data_exfil_python::DataExfilPythonAdapter`],
|
||||
//! [`super::data_exfil_js::DataExfilJsAdapter`],
|
||||
//! [`super::data_exfil_go::DataExfilGoAdapter`],
|
||||
//! [`super::data_exfil_ruby::DataExfilRubyAdapter`], and
|
||||
//! [`super::data_exfil_java::DataExfilJavaAdapter`] for the same
|
||||
//! shape on other languages.
|
||||
|
||||
use crate::dynamic::framework::{FrameworkAdapter, FrameworkBinding};
|
||||
use crate::evidence::EntryKind;
|
||||
use crate::summary::FuncSummary;
|
||||
use crate::symbol::Lang;
|
||||
|
||||
pub struct DataExfilPhpAdapter;
|
||||
|
||||
const ADAPTER_NAME: &str = "data-exfil-php";
|
||||
|
||||
fn callee_is_outbound_http(name: &str) -> bool {
|
||||
let last = name.rsplit_once('\\').map(|(_, s)| s).unwrap_or(name);
|
||||
let last = last.rsplit_once("::").map(|(_, s)| s).unwrap_or(last);
|
||||
let last = last.rsplit_once("->").map(|(_, s)| s).unwrap_or(last);
|
||||
matches!(
|
||||
last,
|
||||
"curl_init"
|
||||
| "curl_exec"
|
||||
| "curl_setopt"
|
||||
| "curl_multi_exec"
|
||||
| "file_get_contents"
|
||||
| "fopen"
|
||||
| "fsockopen"
|
||||
| "stream_socket_client"
|
||||
| "stream_context_create"
|
||||
| "get"
|
||||
| "post"
|
||||
| "put"
|
||||
| "delete"
|
||||
| "request"
|
||||
| "sendRequest"
|
||||
| "send"
|
||||
) || matches!(
|
||||
name,
|
||||
"curl_init"
|
||||
| "curl_exec"
|
||||
| "file_get_contents"
|
||||
| "fopen"
|
||||
| "fsockopen"
|
||||
| "stream_socket_client"
|
||||
| "GuzzleHttp\\Client.get"
|
||||
| "GuzzleHttp\\Client.post"
|
||||
| "GuzzleHttp\\Client.request"
|
||||
| "GuzzleHttp\\Client.send"
|
||||
| "Symfony\\Component\\HttpClient\\HttpClient.create"
|
||||
| "Symfony\\Contracts\\HttpClient\\HttpClientInterface.request"
|
||||
)
|
||||
}
|
||||
|
||||
fn source_imports_php_http_client(file_bytes: &[u8]) -> bool {
|
||||
const NEEDLES: &[&[u8]] = &[
|
||||
b"<?php",
|
||||
b"<?=",
|
||||
b"GuzzleHttp\\Client",
|
||||
b"GuzzleHttp\\Psr7",
|
||||
b"Symfony\\Component\\HttpClient",
|
||||
b"Symfony\\Contracts\\HttpClient",
|
||||
b"curl_init(",
|
||||
b"curl_exec(",
|
||||
b"file_get_contents(",
|
||||
b"fsockopen(",
|
||||
b"stream_socket_client(",
|
||||
];
|
||||
NEEDLES
|
||||
.iter()
|
||||
.any(|n| file_bytes.windows(n.len()).any(|w| w == *n))
|
||||
}
|
||||
|
||||
/// Returns `true` when the surrounding source visibly routes the
|
||||
/// outbound URL through a host-allowlist / network-policy gate.
|
||||
fn host_routed_through_allowlist(file_bytes: &[u8]) -> bool {
|
||||
const NEEDLES: &[&[u8]] = &[
|
||||
b"ALLOWLIST",
|
||||
b"allowlist",
|
||||
b"ALLOWED_HOSTS",
|
||||
b"allowed_hosts",
|
||||
b"'127.0.0.1'",
|
||||
b"\"127.0.0.1\"",
|
||||
b"'localhost'",
|
||||
b"\"localhost\"",
|
||||
b"in_array($host",
|
||||
b"isset($allow",
|
||||
];
|
||||
NEEDLES
|
||||
.iter()
|
||||
.any(|n| file_bytes.windows(n.len()).any(|w| w == *n))
|
||||
}
|
||||
|
||||
impl FrameworkAdapter for DataExfilPhpAdapter {
|
||||
fn name(&self) -> &'static str {
|
||||
ADAPTER_NAME
|
||||
}
|
||||
|
||||
fn lang(&self) -> Lang {
|
||||
Lang::Php
|
||||
}
|
||||
|
||||
fn detect(
|
||||
&self,
|
||||
summary: &FuncSummary,
|
||||
_ast: tree_sitter::Node<'_>,
|
||||
file_bytes: &[u8],
|
||||
) -> Option<FrameworkBinding> {
|
||||
if host_routed_through_allowlist(file_bytes) {
|
||||
return None;
|
||||
}
|
||||
let matches_call = super::any_callee_matches(summary, callee_is_outbound_http);
|
||||
let matches_source = source_imports_php_http_client(file_bytes);
|
||||
if matches_call && matches_source {
|
||||
Some(FrameworkBinding {
|
||||
adapter: ADAPTER_NAME.to_owned(),
|
||||
kind: EntryKind::Function,
|
||||
route: None,
|
||||
request_params: Vec::new(),
|
||||
response_writer: None,
|
||||
middleware: Vec::new(),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn parse_php(src: &[u8]) -> tree_sitter::Tree {
|
||||
let mut parser = tree_sitter::Parser::new();
|
||||
let lang = tree_sitter::Language::from(tree_sitter_php::LANGUAGE_PHP);
|
||||
parser.set_language(&lang).unwrap();
|
||||
parser.parse(src, None).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fires_on_curl_init_exec() {
|
||||
let src: &[u8] = b"<?php\nfunction run($host) {\n $ch = curl_init('http://' . $host . '/exfil');\n curl_exec($ch);\n}\n";
|
||||
let tree = parse_php(src);
|
||||
let summary = FuncSummary {
|
||||
name: "run".into(),
|
||||
callees: vec![
|
||||
crate::summary::CalleeSite::bare("curl_init"),
|
||||
crate::summary::CalleeSite::bare("curl_exec"),
|
||||
],
|
||||
..Default::default()
|
||||
};
|
||||
assert!(
|
||||
DataExfilPhpAdapter
|
||||
.detect(&summary, tree.root_node(), src)
|
||||
.is_some()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fires_on_file_get_contents_remote() {
|
||||
let src: &[u8] = b"<?php\nfunction run($host) {\n return file_get_contents('http://' . $host . '/exfil');\n}\n";
|
||||
let tree = parse_php(src);
|
||||
let summary = FuncSummary {
|
||||
name: "run".into(),
|
||||
callees: vec![crate::summary::CalleeSite::bare("file_get_contents")],
|
||||
..Default::default()
|
||||
};
|
||||
assert!(
|
||||
DataExfilPhpAdapter
|
||||
.detect(&summary, tree.root_node(), src)
|
||||
.is_some()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fires_on_guzzle_request() {
|
||||
let src: &[u8] = b"<?php\nuse GuzzleHttp\\Client;\nfunction run($host) {\n $c = new Client();\n $c->request('GET', 'http://' . $host);\n}\n";
|
||||
let tree = parse_php(src);
|
||||
let summary = FuncSummary {
|
||||
name: "run".into(),
|
||||
callees: vec![crate::summary::CalleeSite::bare("request")],
|
||||
..Default::default()
|
||||
};
|
||||
assert!(
|
||||
DataExfilPhpAdapter
|
||||
.detect(&summary, tree.root_node(), src)
|
||||
.is_some()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skips_when_host_in_allowlist_literal() {
|
||||
let src: &[u8] = b"<?php\nfunction run($host) {\n if ($host !== '127.0.0.1') { return; }\n $ch = curl_init('http://' . $host);\n curl_exec($ch);\n}\n";
|
||||
let tree = parse_php(src);
|
||||
let summary = FuncSummary {
|
||||
name: "run".into(),
|
||||
callees: vec![crate::summary::CalleeSite::bare("curl_init")],
|
||||
..Default::default()
|
||||
};
|
||||
assert!(
|
||||
DataExfilPhpAdapter
|
||||
.detect(&summary, tree.root_node(), src)
|
||||
.is_none()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skips_plain_function() {
|
||||
let src: &[u8] = b"<?php\nfunction add($a, $b) { return $a + $b; }\n";
|
||||
let tree = parse_php(src);
|
||||
let summary = FuncSummary {
|
||||
name: "add".into(),
|
||||
..Default::default()
|
||||
};
|
||||
assert!(
|
||||
DataExfilPhpAdapter
|
||||
.detect(&summary, tree.root_node(), src)
|
||||
.is_none()
|
||||
);
|
||||
}
|
||||
}
|
||||
239
src/dynamic/framework/adapters/data_exfil_rust.rs
Normal file
239
src/dynamic/framework/adapters/data_exfil_rust.rs
Normal file
|
|
@ -0,0 +1,239 @@
|
|||
//! Rust [`super::super::FrameworkAdapter`] matching outbound-HTTP
|
||||
//! sink constructions (`reqwest::get`, `reqwest::blocking::get`,
|
||||
//! `reqwest::Client::*`, `hyper::Client::request`, `ureq::get`,
|
||||
//! `surf::get`, `isahc::get`).
|
||||
//!
|
||||
//! Phase 11 (Track L.9). Fires when the function body invokes one
|
||||
//! of the canonical Rust HTTP-client entry points and the surrounding
|
||||
//! source imports the matching crate.
|
||||
//!
|
||||
//! See sibling adapters
|
||||
//! [`super::data_exfil_python::DataExfilPythonAdapter`],
|
||||
//! [`super::data_exfil_js::DataExfilJsAdapter`],
|
||||
//! [`super::data_exfil_go::DataExfilGoAdapter`],
|
||||
//! [`super::data_exfil_ruby::DataExfilRubyAdapter`],
|
||||
//! [`super::data_exfil_java::DataExfilJavaAdapter`], and
|
||||
//! [`super::data_exfil_php::DataExfilPhpAdapter`] for the same shape
|
||||
//! on other languages.
|
||||
|
||||
use crate::dynamic::framework::{FrameworkAdapter, FrameworkBinding};
|
||||
use crate::evidence::EntryKind;
|
||||
use crate::summary::FuncSummary;
|
||||
use crate::symbol::Lang;
|
||||
|
||||
pub struct DataExfilRustAdapter;
|
||||
|
||||
const ADAPTER_NAME: &str = "data-exfil-rust";
|
||||
|
||||
fn callee_is_outbound_http(name: &str) -> bool {
|
||||
let last = name.rsplit_once("::").map(|(_, s)| s).unwrap_or(name);
|
||||
let last = last.rsplit_once('.').map(|(_, s)| s).unwrap_or(last);
|
||||
matches!(
|
||||
last,
|
||||
"get"
|
||||
| "post"
|
||||
| "put"
|
||||
| "patch"
|
||||
| "delete"
|
||||
| "head"
|
||||
| "send"
|
||||
| "send_async"
|
||||
| "execute"
|
||||
| "fetch"
|
||||
| "request"
|
||||
| "call"
|
||||
) || matches!(
|
||||
name,
|
||||
"reqwest::get"
|
||||
| "reqwest::blocking::get"
|
||||
| "reqwest::Client::get"
|
||||
| "reqwest::Client::post"
|
||||
| "reqwest::Client::execute"
|
||||
| "reqwest::blocking::Client::get"
|
||||
| "reqwest::blocking::Client::post"
|
||||
| "reqwest::blocking::Client::execute"
|
||||
| "reqwest::RequestBuilder::send"
|
||||
| "reqwest::blocking::RequestBuilder::send"
|
||||
| "hyper::Client::request"
|
||||
| "hyper::Client::get"
|
||||
| "ureq::get"
|
||||
| "ureq::post"
|
||||
| "ureq::request"
|
||||
| "surf::get"
|
||||
| "surf::post"
|
||||
| "isahc::get"
|
||||
| "isahc::post"
|
||||
| "isahc::send"
|
||||
)
|
||||
}
|
||||
|
||||
fn source_imports_rust_http_client(file_bytes: &[u8]) -> bool {
|
||||
const NEEDLES: &[&[u8]] = &[
|
||||
b"use reqwest",
|
||||
b"reqwest::",
|
||||
b"use hyper",
|
||||
b"hyper::Client",
|
||||
b"use ureq",
|
||||
b"ureq::",
|
||||
b"use surf",
|
||||
b"surf::",
|
||||
b"use isahc",
|
||||
b"isahc::",
|
||||
b"use awc",
|
||||
b"awc::Client",
|
||||
];
|
||||
NEEDLES
|
||||
.iter()
|
||||
.any(|n| file_bytes.windows(n.len()).any(|w| w == *n))
|
||||
}
|
||||
|
||||
/// Returns `true` when the surrounding source visibly routes the
|
||||
/// outbound URL through a host-allowlist / network-policy gate.
|
||||
fn host_routed_through_allowlist(file_bytes: &[u8]) -> bool {
|
||||
const NEEDLES: &[&[u8]] = &[
|
||||
b"ALLOWLIST",
|
||||
b"allowlist",
|
||||
b"ALLOWED_HOSTS",
|
||||
b"allowed_hosts",
|
||||
b"\"127.0.0.1\"",
|
||||
b"\"localhost\"",
|
||||
b".contains(host)",
|
||||
b".contains(&host)",
|
||||
b".contains(\"localhost\")",
|
||||
];
|
||||
NEEDLES
|
||||
.iter()
|
||||
.any(|n| file_bytes.windows(n.len()).any(|w| w == *n))
|
||||
}
|
||||
|
||||
impl FrameworkAdapter for DataExfilRustAdapter {
|
||||
fn name(&self) -> &'static str {
|
||||
ADAPTER_NAME
|
||||
}
|
||||
|
||||
fn lang(&self) -> Lang {
|
||||
Lang::Rust
|
||||
}
|
||||
|
||||
fn detect(
|
||||
&self,
|
||||
summary: &FuncSummary,
|
||||
_ast: tree_sitter::Node<'_>,
|
||||
file_bytes: &[u8],
|
||||
) -> Option<FrameworkBinding> {
|
||||
if host_routed_through_allowlist(file_bytes) {
|
||||
return None;
|
||||
}
|
||||
let matches_call = super::any_callee_matches(summary, callee_is_outbound_http);
|
||||
let matches_source = source_imports_rust_http_client(file_bytes);
|
||||
if matches_call && matches_source {
|
||||
Some(FrameworkBinding {
|
||||
adapter: ADAPTER_NAME.to_owned(),
|
||||
kind: EntryKind::Function,
|
||||
route: None,
|
||||
request_params: Vec::new(),
|
||||
response_writer: None,
|
||||
middleware: Vec::new(),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn parse_rust(src: &[u8]) -> tree_sitter::Tree {
|
||||
let mut parser = tree_sitter::Parser::new();
|
||||
let lang = tree_sitter::Language::from(tree_sitter_rust::LANGUAGE);
|
||||
parser.set_language(&lang).unwrap();
|
||||
parser.parse(src, None).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fires_on_reqwest_blocking_get() {
|
||||
let src: &[u8] = b"use reqwest;\npub fn run(host: &str) -> Result<(), Box<dyn std::error::Error>> {\n let url = format!(\"http://{}/exfil\", host);\n let _ = reqwest::blocking::get(&url)?;\n Ok(())\n}\n";
|
||||
let tree = parse_rust(src);
|
||||
let summary = FuncSummary {
|
||||
name: "run".into(),
|
||||
callees: vec![crate::summary::CalleeSite::bare("reqwest::blocking::get")],
|
||||
..Default::default()
|
||||
};
|
||||
assert!(
|
||||
DataExfilRustAdapter
|
||||
.detect(&summary, tree.root_node(), src)
|
||||
.is_some()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fires_on_reqwest_client_post() {
|
||||
let src: &[u8] = b"use reqwest::Client;\npub async fn run(host: &str) -> Result<(), Box<dyn std::error::Error>> {\n let c = Client::new();\n let _ = c.post(format!(\"http://{}/exfil\", host)).send().await?;\n Ok(())\n}\n";
|
||||
let tree = parse_rust(src);
|
||||
let summary = FuncSummary {
|
||||
name: "run".into(),
|
||||
callees: vec![
|
||||
crate::summary::CalleeSite::bare("reqwest::Client::post"),
|
||||
crate::summary::CalleeSite::bare("reqwest::RequestBuilder::send"),
|
||||
],
|
||||
..Default::default()
|
||||
};
|
||||
assert!(
|
||||
DataExfilRustAdapter
|
||||
.detect(&summary, tree.root_node(), src)
|
||||
.is_some()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fires_on_ureq_get() {
|
||||
let src: &[u8] = b"use ureq;\npub fn run(host: &str) -> Result<(), ureq::Error> {\n let _ = ureq::get(&format!(\"http://{}/exfil\", host)).call()?;\n Ok(())\n}\n";
|
||||
let tree = parse_rust(src);
|
||||
let summary = FuncSummary {
|
||||
name: "run".into(),
|
||||
callees: vec![
|
||||
crate::summary::CalleeSite::bare("ureq::get"),
|
||||
crate::summary::CalleeSite::bare("call"),
|
||||
],
|
||||
..Default::default()
|
||||
};
|
||||
assert!(
|
||||
DataExfilRustAdapter
|
||||
.detect(&summary, tree.root_node(), src)
|
||||
.is_some()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skips_when_host_in_allowlist_literal() {
|
||||
let src: &[u8] = b"use reqwest;\npub fn run(host: &str) -> Result<(), Box<dyn std::error::Error>> {\n if host != \"127.0.0.1\" { return Ok(()); }\n let _ = reqwest::blocking::get(format!(\"http://{}/\", host))?;\n Ok(())\n}\n";
|
||||
let tree = parse_rust(src);
|
||||
let summary = FuncSummary {
|
||||
name: "run".into(),
|
||||
callees: vec![crate::summary::CalleeSite::bare("reqwest::blocking::get")],
|
||||
..Default::default()
|
||||
};
|
||||
assert!(
|
||||
DataExfilRustAdapter
|
||||
.detect(&summary, tree.root_node(), src)
|
||||
.is_none()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skips_plain_function() {
|
||||
let src: &[u8] = b"pub fn add(a: i64, b: i64) -> i64 { a + b }\n";
|
||||
let tree = parse_rust(src);
|
||||
let summary = FuncSummary {
|
||||
name: "add".into(),
|
||||
..Default::default()
|
||||
};
|
||||
assert!(
|
||||
DataExfilRustAdapter
|
||||
.detect(&summary, tree.root_node(), src)
|
||||
.is_none()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -11,15 +11,20 @@
|
|||
//! the route / framework adapters; the per-cap sink adapters live
|
||||
//! here so the per-language verticals can ship independently.
|
||||
|
||||
pub mod crypto_go;
|
||||
pub mod crypto_java;
|
||||
pub mod crypto_js;
|
||||
pub mod crypto_php;
|
||||
pub mod crypto_python;
|
||||
pub mod crypto_ruby;
|
||||
pub mod crypto_rust;
|
||||
pub mod data_exfil_go;
|
||||
pub mod data_exfil_java;
|
||||
pub mod data_exfil_js;
|
||||
pub mod data_exfil_php;
|
||||
pub mod data_exfil_python;
|
||||
pub mod data_exfil_ruby;
|
||||
pub mod data_exfil_rust;
|
||||
pub mod go_chi;
|
||||
pub mod go_echo;
|
||||
pub mod go_fiber;
|
||||
|
|
@ -131,15 +136,20 @@ pub mod xxe_php;
|
|||
pub mod xxe_python;
|
||||
pub mod xxe_ruby;
|
||||
|
||||
pub use crypto_go::CryptoGoAdapter;
|
||||
pub use crypto_java::CryptoJavaAdapter;
|
||||
pub use crypto_js::CryptoJsAdapter;
|
||||
pub use crypto_php::CryptoPhpAdapter;
|
||||
pub use crypto_python::CryptoPythonAdapter;
|
||||
pub use crypto_ruby::CryptoRubyAdapter;
|
||||
pub use crypto_rust::CryptoRustAdapter;
|
||||
pub use data_exfil_go::DataExfilGoAdapter;
|
||||
pub use data_exfil_java::DataExfilJavaAdapter;
|
||||
pub use data_exfil_js::DataExfilJsAdapter;
|
||||
pub use data_exfil_php::DataExfilPhpAdapter;
|
||||
pub use data_exfil_python::DataExfilPythonAdapter;
|
||||
pub use data_exfil_ruby::DataExfilRubyAdapter;
|
||||
pub use data_exfil_rust::DataExfilRustAdapter;
|
||||
pub use go_chi::GoChiAdapter;
|
||||
pub use go_echo::GoEchoAdapter;
|
||||
pub use go_fiber::GoFiberAdapter;
|
||||
|
|
|
|||
|
|
@ -291,11 +291,17 @@ mod tests {
|
|||
// CRYPTO × {Php, Ruby} + DATA_EXFIL × Ruby.
|
||||
// Php: +1 (CryptoPhp) 12 → 13
|
||||
// Ruby: +2 (CryptoRuby, DataExfilRuby) 12 → 14
|
||||
// Track L.9 closing slice (session-0017 of run 7d60):
|
||||
// CRYPTO × {Go, Rust} + DATA_EXFIL × {Java, Php, Rust}.
|
||||
// Go: +1 (CryptoGo) 12 → 13
|
||||
// Java: +1 (DataExfilJava) 19 → 20
|
||||
// Php: +1 (DataExfilPhp) 13 → 14
|
||||
// Rust: +2 (CryptoRust, DataExfilRust) 8 → 10
|
||||
let java_registered = registry::adapters_for(Lang::Java);
|
||||
assert_eq!(
|
||||
java_registered.len(),
|
||||
19,
|
||||
"Java must have Phase 21 baseline (18) + Track L.9 CryptoJava (1)",
|
||||
20,
|
||||
"Java must have Phase 21 baseline (18) + Track L.9 (CryptoJava, DataExfilJava)",
|
||||
);
|
||||
for adapter in java_registered {
|
||||
assert_eq!(adapter.lang(), Lang::Java);
|
||||
|
|
@ -303,8 +309,8 @@ mod tests {
|
|||
let php_registered = registry::adapters_for(Lang::Php);
|
||||
assert_eq!(
|
||||
php_registered.len(),
|
||||
13,
|
||||
"Php must have Phase 20 baseline (10) + M.3 Laravel middleware+migration (2) + Track L.9 CryptoPhp (1)",
|
||||
14,
|
||||
"Php must have Phase 20 baseline (10) + M.3 Laravel middleware+migration (2) + Track L.9 (CryptoPhp, DataExfilPhp)",
|
||||
);
|
||||
for adapter in php_registered {
|
||||
assert_eq!(adapter.lang(), Lang::Php);
|
||||
|
|
@ -348,8 +354,8 @@ mod tests {
|
|||
let go_registered = registry::adapters_for(Lang::Go);
|
||||
assert_eq!(
|
||||
go_registered.len(),
|
||||
12,
|
||||
"Go must have Phase 21 baseline (11) + Track L.9 DataExfilGo (1)",
|
||||
13,
|
||||
"Go must have Phase 21 baseline (11) + Track L.9 (CryptoGo, DataExfilGo)",
|
||||
);
|
||||
for adapter in go_registered {
|
||||
assert_eq!(adapter.lang(), Lang::Go);
|
||||
|
|
@ -357,8 +363,8 @@ mod tests {
|
|||
let rust_registered = registry::adapters_for(Lang::Rust);
|
||||
assert_eq!(
|
||||
rust_registered.len(),
|
||||
8,
|
||||
"Rust must have Phase 20 baseline (6) + M.3 juniper (1) + refinery (1)",
|
||||
10,
|
||||
"Rust must have Phase 20 baseline (6) + M.3 juniper (1) + refinery (1) + Track L.9 (CryptoRust, DataExfilRust)",
|
||||
);
|
||||
for adapter in rust_registered {
|
||||
assert_eq!(adapter.lang(), Lang::Rust);
|
||||
|
|
|
|||
|
|
@ -45,6 +45,8 @@ pub fn adapters_for(lang: Lang) -> &'static [&'static dyn FrameworkAdapter] {
|
|||
// later phase that appends a new adapter cannot silently re-order
|
||||
// the existing first-match.
|
||||
static RUST: &[&dyn FrameworkAdapter] = &[
|
||||
&super::adapters::CryptoRustAdapter,
|
||||
&super::adapters::DataExfilRustAdapter,
|
||||
&super::adapters::GraphqlJuniperAdapter,
|
||||
&super::adapters::HeaderRustAdapter,
|
||||
&super::adapters::MigrationRefineryAdapter,
|
||||
|
|
@ -58,6 +60,7 @@ static C: &[&dyn FrameworkAdapter] = &[];
|
|||
static CPP: &[&dyn FrameworkAdapter] = &[];
|
||||
static JAVA: &[&dyn FrameworkAdapter] = &[
|
||||
&super::adapters::CryptoJavaAdapter,
|
||||
&super::adapters::DataExfilJavaAdapter,
|
||||
&super::adapters::HeaderJavaAdapter,
|
||||
&super::adapters::JavaDeserializeAdapter,
|
||||
&super::adapters::JavaMicronautAdapter,
|
||||
|
|
@ -78,6 +81,7 @@ static JAVA: &[&dyn FrameworkAdapter] = &[
|
|||
&super::adapters::XxeJavaAdapter,
|
||||
];
|
||||
static GO: &[&dyn FrameworkAdapter] = &[
|
||||
&super::adapters::CryptoGoAdapter,
|
||||
&super::adapters::DataExfilGoAdapter,
|
||||
&super::adapters::GoChiAdapter,
|
||||
&super::adapters::GoEchoAdapter,
|
||||
|
|
@ -93,6 +97,7 @@ static GO: &[&dyn FrameworkAdapter] = &[
|
|||
];
|
||||
static PHP: &[&dyn FrameworkAdapter] = &[
|
||||
&super::adapters::CryptoPhpAdapter,
|
||||
&super::adapters::DataExfilPhpAdapter,
|
||||
&super::adapters::HeaderPhpAdapter,
|
||||
&super::adapters::LdapPhpAdapter,
|
||||
&super::adapters::MiddlewareLaravelAdapter,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue