use crate::flow::FlowContext;
use crate::rules::{Rule, create_finding_with_confidence};
use rma_common::{Confidence, Finding, Language, Severity};
use rma_parser::ParsedFile;
use tree_sitter::Node;
#[inline]
fn contains_ignore_case(haystack: &str, needle: &str) -> bool {
haystack
.as_bytes()
.windows(needle.len())
.any(|window| window.eq_ignore_ascii_case(needle.as_bytes()))
}
pub struct CommandExecutionRule;
impl Rule for CommandExecutionRule {
fn id(&self) -> &str {
"java/command-injection"
}
fn description(&self) -> &str {
"Detects command injection patterns (shell mode with dynamic arguments)"
}
fn applies_to(&self, lang: Language) -> bool {
lang == Language::Java
}
fn check(&self, parsed: &ParsedFile) -> Vec<Finding> {
let mut findings = Vec::new();
let mut cursor = parsed.tree.walk();
find_nodes_by_kind(&mut cursor, "method_invocation", |node: Node| {
if let Ok(text) = node.utf8_text(parsed.content.as_bytes()) {
if text.contains("Runtime") && text.contains("getRuntime") {
let has_concat = text.contains(" + ") || text.contains("\" +");
if has_concat {
findings.push(create_finding_with_confidence(
self.id(),
&node,
&parsed.path,
&parsed.content,
Severity::Critical,
"Command injection: Runtime.exec with string concatenation - use ProcessBuilder with array args",
Language::Java,
Confidence::High,
));
} else {
findings.push(create_finding_with_confidence(
self.id(),
&node,
&parsed.path,
&parsed.content,
Severity::Warning,
"Runtime.exec detected - prefer ProcessBuilder with explicit arguments",
Language::Java,
Confidence::Medium,
));
}
}
}
});
cursor = parsed.tree.walk();
find_nodes_by_kind(&mut cursor, "object_creation_expression", |node: Node| {
if let Ok(text) = node.utf8_text(parsed.content.as_bytes())
&& text.contains("ProcessBuilder")
{
let is_shell = text.contains("\"sh\"")
|| text.contains("\"bash\"")
|| text.contains("\"cmd\"")
|| text.contains("\"/bin/sh\"")
|| text.contains("\"cmd.exe\"");
let has_shell_mode =
text.contains("\"-c\"") || text.contains("\"/c\"") || text.contains("\"/C\"");
let has_concat = text.contains(" + ") || text.contains("\" +");
if is_shell && has_shell_mode && has_concat {
findings.push(create_finding_with_confidence(
self.id(),
&node,
&parsed.path,
&parsed.content,
Severity::Critical,
"Command injection: ProcessBuilder with shell mode and string concatenation",
Language::Java,
Confidence::High,
));
} else if is_shell && has_shell_mode {
findings.push(create_finding_with_confidence(
self.id(),
&node,
&parsed.path,
&parsed.content,
Severity::Warning,
"ProcessBuilder with shell mode - ensure arguments are not from untrusted input",
Language::Java,
Confidence::Medium,
));
}
}
});
findings
}
}
pub struct SqlInjectionRule;
impl Rule for SqlInjectionRule {
fn id(&self) -> &str {
"java/sql-injection"
}
fn description(&self) -> &str {
"Detects SQL queries built with string concatenation that may allow injection"
}
fn applies_to(&self, lang: Language) -> bool {
lang == Language::Java
}
fn check(&self, parsed: &ParsedFile) -> Vec<Finding> {
let mut findings = Vec::new();
if !parsed.content.contains("java.sql")
&& !parsed.content.contains("executeQuery")
&& !parsed.content.contains("executeUpdate")
{
return findings;
}
let mut cursor = parsed.tree.walk();
find_nodes_by_kind(&mut cursor, "method_invocation", |node: Node| {
if let Ok(text) = node.utf8_text(parsed.content.as_bytes()) {
if (text.contains("executeQuery") || text.contains("executeUpdate"))
&& (text.contains(" + ") || text.contains("\" +"))
{
if contains_ignore_case(text, "select ")
|| contains_ignore_case(text, "insert ")
|| contains_ignore_case(text, "update ")
|| contains_ignore_case(text, "delete ")
{
findings.push(create_finding_with_confidence(
self.id(),
&node,
&parsed.path,
&parsed.content,
Severity::Critical,
"SQL query with string concatenation - use PreparedStatement instead",
Language::Java,
Confidence::High,
));
}
}
}
});
findings
}
}
pub struct InsecureDeserializationRule;
impl Rule for InsecureDeserializationRule {
fn id(&self) -> &str {
"java/insecure-deserialization"
}
fn description(&self) -> &str {
"Detects ObjectInputStream usage which can lead to remote code execution"
}
fn applies_to(&self, lang: Language) -> bool {
lang == Language::Java
}
fn check(&self, parsed: &ParsedFile) -> Vec<Finding> {
let mut findings = Vec::new();
let mut cursor = parsed.tree.walk();
find_nodes_by_kind(&mut cursor, "object_creation_expression", |node: Node| {
if let Ok(text) = node.utf8_text(parsed.content.as_bytes())
&& text.contains("ObjectInputStream")
{
findings.push(create_finding_with_confidence(
self.id(),
&node,
&parsed.path,
&parsed.content,
Severity::Critical,
"ObjectInputStream can lead to RCE - use safe alternatives like JSON",
Language::Java,
Confidence::High,
));
}
});
cursor = parsed.tree.walk();
find_nodes_by_kind(&mut cursor, "method_invocation", |node: Node| {
if let Ok(text) = node.utf8_text(parsed.content.as_bytes())
&& text.contains(".readObject(")
{
findings.push(create_finding_with_confidence(
self.id(),
&node,
&parsed.path,
&parsed.content,
Severity::Warning,
"readObject() on untrusted data can lead to RCE - validate input source",
Language::Java,
Confidence::High,
));
}
});
findings
}
}
pub struct XxeVulnerabilityRule;
impl Rule for XxeVulnerabilityRule {
fn id(&self) -> &str {
"java/xxe-vulnerability"
}
fn description(&self) -> &str {
"Detects XML parsers that may be vulnerable to XXE attacks"
}
fn applies_to(&self, lang: Language) -> bool {
lang == Language::Java
}
fn check(&self, parsed: &ParsedFile) -> Vec<Finding> {
let mut findings = Vec::new();
if !parsed.content.contains("XMLInputFactory")
&& !parsed.content.contains("DocumentBuilder")
&& !parsed.content.contains("SAXParser")
{
return findings;
}
let has_secure_config = parsed.content.contains("FEATURE_SECURE_PROCESSING")
|| parsed.content.contains("setFeature")
|| parsed.content.contains("disallow-doctype-decl");
if !has_secure_config {
let mut cursor = parsed.tree.walk();
find_nodes_by_kind(&mut cursor, "object_creation_expression", |node: Node| {
if let Ok(text) = node.utf8_text(parsed.content.as_bytes())
&& (text.contains("DocumentBuilder")
|| text.contains("SAXParser")
|| text.contains("XMLInputFactory"))
{
findings.push(create_finding_with_confidence(
self.id(),
&node,
&parsed.path,
&parsed.content,
Severity::Error,
"XML parser without secure configuration - vulnerable to XXE attacks",
Language::Java,
Confidence::High,
));
}
});
}
findings
}
}
pub struct PathTraversalRule;
impl Rule for PathTraversalRule {
fn id(&self) -> &str {
"java/path-traversal"
}
fn description(&self) -> &str {
"Detects file operations with dynamic paths that may allow directory traversal"
}
fn applies_to(&self, lang: Language) -> bool {
lang == Language::Java
}
fn check(&self, parsed: &ParsedFile) -> Vec<Finding> {
let mut findings = Vec::new();
let mut cursor = parsed.tree.walk();
find_nodes_by_kind(&mut cursor, "object_creation_expression", |node: Node| {
if let Ok(text) = node.utf8_text(parsed.content.as_bytes()) {
if text.starts_with("new File(") && (text.contains(" + ") || text.contains("\" +"))
{
findings.push(create_finding_with_confidence(
self.id(),
&node,
&parsed.path,
&parsed.content,
Severity::Warning,
"File path with concatenation - validate to prevent directory traversal",
Language::Java,
Confidence::High,
));
}
}
});
findings
}
}
pub struct StringConcatInLoopRule;
impl Rule for StringConcatInLoopRule {
fn id(&self) -> &str {
"java/string-concat-in-loop"
}
fn description(&self) -> &str {
"Detects string concatenation in loops - use StringBuilder instead"
}
fn applies_to(&self, lang: Language) -> bool {
lang == Language::Java
}
fn uses_flow(&self) -> bool {
true
}
fn check(&self, _parsed: &ParsedFile) -> Vec<Finding> {
Vec::new()
}
fn check_with_flow(&self, parsed: &ParsedFile, flow: &FlowContext) -> Vec<Finding> {
let mut findings = Vec::new();
let mut cursor = parsed.tree.walk();
find_nodes_by_kind(&mut cursor, "assignment_expression", |node: Node| {
if flow.is_in_loop(node.id()) {
if let Ok(text) = node.utf8_text(parsed.content.as_bytes()) {
if text.contains("+=") && (text.contains("\"") || text.contains("String")) {
findings.push(create_finding_with_confidence(
self.id(),
&node,
&parsed.path,
&parsed.content,
Severity::Warning,
"String concatenation in loop - use StringBuilder for better performance",
Language::Java,
Confidence::High,
));
} else if text.contains(" + ") && text.contains("\"") {
findings.push(create_finding_with_confidence(
self.id(),
&node,
&parsed.path,
&parsed.content,
Severity::Warning,
"String concatenation in loop - use StringBuilder for better performance",
Language::Java,
Confidence::High,
));
}
}
}
});
cursor = parsed.tree.walk();
find_nodes_by_kind(&mut cursor, "binary_expression", |node: Node| {
if flow.loop_depth(node.id()) > 0 {
if let Ok(text) = node.utf8_text(parsed.content.as_bytes()) {
if text.contains(" + \"") || text.contains("\" + ") {
if let Some(parent) = node.parent() {
if parent.kind() != "assignment_expression" {
findings.push(create_finding_with_confidence(
self.id(),
&node,
&parsed.path,
&parsed.content,
Severity::Info,
"String concatenation in loop - consider StringBuilder if this runs many iterations",
Language::Java,
Confidence::Medium,
));
}
}
}
}
}
});
findings
}
}
pub struct NpePronePatternsRule;
impl Rule for NpePronePatternsRule {
fn id(&self) -> &str {
"java/potential-npe"
}
fn description(&self) -> &str {
"Detects patterns that may lead to NullPointerException"
}
fn applies_to(&self, lang: Language) -> bool {
lang == Language::Java
}
fn check(&self, parsed: &ParsedFile) -> Vec<Finding> {
let mut findings = Vec::new();
let mut cursor = parsed.tree.walk();
find_nodes_by_kind(&mut cursor, "method_invocation", |node: Node| {
if let Ok(text) = node.utf8_text(parsed.content.as_bytes()) {
let chain_depth = text.matches('.').count();
if chain_depth >= 3 {
let is_safe_chain = text.contains("StringBuilder")
|| text.contains(".stream()")
|| text.contains(".filter(")
|| text.contains(".map(")
|| text.contains(".collect(")
|| text.contains("Optional.")
|| text.contains(".orElse(")
|| text.contains(".toString()");
if !is_safe_chain {
findings.push(create_finding_with_confidence(
self.id(),
&node,
&parsed.path,
&parsed.content,
Severity::Warning,
"Deeply chained method calls may cause NPE - consider null checks or Optional",
Language::Java,
Confidence::Low,
));
}
}
}
});
cursor = parsed.tree.walk();
find_nodes_by_kind(&mut cursor, "if_statement", |node: Node| {
if let Some(condition) = node.child_by_field_name("condition") {
if let Ok(cond_text) = condition.utf8_text(parsed.content.as_bytes()) {
if cond_text.contains("!= null") || cond_text.contains("== null") {
let var_name = cond_text
.split(|c: char| !c.is_alphanumeric() && c != '_')
.find(|s| !s.is_empty() && *s != "null");
if let Some(var) = var_name {
if let Some(parent) = node.parent() {
let mut prev_sibling = node.prev_sibling();
while let Some(sibling) = prev_sibling {
if let Ok(sibling_text) =
sibling.utf8_text(parsed.content.as_bytes())
{
let dereference_pattern = format!("{}.", var);
if sibling_text.contains(&dereference_pattern) {
findings.push(create_finding_with_confidence(
self.id(),
&node,
&parsed.path,
&parsed.content,
Severity::Warning,
&format!(
"Null check for '{}' after dereference - check may be too late",
var
),
Language::Java,
Confidence::Low,
));
break;
}
}
prev_sibling = sibling.prev_sibling();
}
let _ = parent; }
}
}
}
}
});
findings
}
}
pub struct UnclosedResourceRule;
impl UnclosedResourceRule {
const CLOSEABLE_RESOURCES: &'static [&'static str] = &[
"FileInputStream",
"FileOutputStream",
"FileReader",
"FileWriter",
"BufferedReader",
"BufferedWriter",
"BufferedInputStream",
"BufferedOutputStream",
"InputStreamReader",
"OutputStreamWriter",
"PrintWriter",
"PrintStream",
"ObjectInputStream",
"ObjectOutputStream",
"DataInputStream",
"DataOutputStream",
"RandomAccessFile",
"Socket",
"ServerSocket",
"Connection",
"Statement",
"PreparedStatement",
"CallableStatement",
"ResultSet",
];
fn is_in_try_with_resources(node: &Node) -> bool {
let mut current = node.parent();
while let Some(parent) = current {
if parent.kind() == "try_with_resources_statement" {
return true;
}
if parent.kind() == "try_statement" {
if parent.child_by_field_name("resources").is_some() {
return true;
}
}
current = parent.parent();
}
false
}
fn is_field_assignment(node: &Node, content: &str) -> bool {
if let Some(parent) = node.parent() {
if parent.kind() == "assignment_expression" {
if let Some(left) = parent.child_by_field_name("left") {
if let Ok(left_text) = left.utf8_text(content.as_bytes()) {
return left_text.starts_with("this.");
}
}
}
}
false
}
}
impl Rule for UnclosedResourceRule {
fn id(&self) -> &str {
"java/unclosed-resource"
}
fn description(&self) -> &str {
"Detects resources that may not be properly closed"
}
fn applies_to(&self, lang: Language) -> bool {
lang == Language::Java
}
fn uses_flow(&self) -> bool {
true }
fn check(&self, parsed: &ParsedFile) -> Vec<Finding> {
let mut findings = Vec::new();
let mut cursor = parsed.tree.walk();
find_nodes_by_kind(&mut cursor, "object_creation_expression", |node: Node| {
if let Ok(text) = node.utf8_text(parsed.content.as_bytes()) {
for resource in Self::CLOSEABLE_RESOURCES {
if text.contains(&format!("new {}(", resource)) {
if Self::is_in_try_with_resources(&node) {
return;
}
if Self::is_field_assignment(&node, &parsed.content) {
return;
}
findings.push(create_finding_with_confidence(
self.id(),
&node,
&parsed.path,
&parsed.content,
Severity::Warning,
&format!(
"{} should be in try-with-resources or explicitly closed in finally block",
resource
),
Language::Java,
Confidence::Medium,
));
break;
}
}
}
});
findings
}
fn check_with_flow(&self, parsed: &ParsedFile, flow: &FlowContext) -> Vec<Finding> {
let mut findings = Vec::new();
let mut cursor = parsed.tree.walk();
find_nodes_by_kind(&mut cursor, "object_creation_expression", |node: Node| {
if let Ok(text) = node.utf8_text(parsed.content.as_bytes()) {
for resource in Self::CLOSEABLE_RESOURCES {
if text.contains(&format!("new {}(", resource)) {
if Self::is_in_try_with_resources(&node) {
return;
}
if Self::is_field_assignment(&node, &parsed.content) {
return;
}
let block_id = flow.cfg.node_to_block.get(&node.id()).copied();
let is_properly_managed = if let Some(bid) = block_id {
flow.is_finally_block(bid)
} else {
false
};
if !is_properly_managed {
findings.push(create_finding_with_confidence(
self.id(),
&node,
&parsed.path,
&parsed.content,
Severity::Warning,
&format!(
"{} should be in try-with-resources or explicitly closed in finally block",
resource
),
Language::Java,
Confidence::Medium,
));
}
break;
}
}
}
});
findings
}
}
pub struct LogInjectionRule;
impl LogInjectionRule {
const LOG_METHODS: &'static [&'static str] =
&["info", "warn", "error", "debug", "trace", "fatal", "log"];
const USER_INPUT_PATTERNS: &'static [&'static str] = &[
"getParameter",
"getHeader",
"getCookie",
"getQueryString",
"getInputStream",
"getReader",
"request.",
"req.",
"params.",
"body.",
"query.",
];
}
impl Rule for LogInjectionRule {
fn id(&self) -> &str {
"java/log-injection"
}
fn description(&self) -> &str {
"Detects log injection vulnerabilities from user input"
}
fn applies_to(&self, lang: Language) -> bool {
lang == Language::Java
}
fn check(&self, parsed: &ParsedFile) -> Vec<Finding> {
let mut findings = Vec::new();
let content_lower = parsed.content.to_lowercase();
if !content_lower.contains("logger")
&& !content_lower.contains("log.")
&& !content_lower.contains("logging")
{
return findings;
}
let mut cursor = parsed.tree.walk();
find_nodes_by_kind(&mut cursor, "method_invocation", |node: Node| {
if let Ok(text) = node.utf8_text(parsed.content.as_bytes()) {
let text_lower = text.to_lowercase();
let is_log_call = Self::LOG_METHODS
.iter()
.any(|method| text_lower.contains(&format!(".{}(", method)));
if is_log_call {
let has_concat = text.contains(" + ") || text.contains("\" +");
let has_user_input = Self::USER_INPUT_PATTERNS
.iter()
.any(|pattern| text.contains(pattern));
if has_concat && has_user_input {
findings.push(create_finding_with_confidence(
self.id(),
&node,
&parsed.path,
&parsed.content,
Severity::Warning,
"Log statement with user input concatenation - potential log injection. Use parameterized logging instead.",
Language::Java,
Confidence::Medium,
));
} else if has_user_input {
findings.push(create_finding_with_confidence(
self.id(),
&node,
&parsed.path,
&parsed.content,
Severity::Info,
"Log statement includes user input - ensure proper sanitization to prevent log injection",
Language::Java,
Confidence::Low,
));
}
}
}
});
findings
}
}
pub struct SpringSecurityMisconfigRule;
impl Rule for SpringSecurityMisconfigRule {
fn id(&self) -> &str {
"java/spring-security-misconfig"
}
fn description(&self) -> &str {
"Detects Spring Security misconfigurations"
}
fn applies_to(&self, lang: Language) -> bool {
lang == Language::Java
}
fn check(&self, parsed: &ParsedFile) -> Vec<Finding> {
let mut findings = Vec::new();
if !parsed.content.contains("Security")
&& !parsed.content.contains("@CrossOrigin")
&& !parsed.content.contains("csrf")
{
return findings;
}
let mut cursor = parsed.tree.walk();
find_nodes_by_kind(&mut cursor, "method_invocation", |node: Node| {
if let Ok(text) = node.utf8_text(parsed.content.as_bytes()) {
if text.contains(".csrf(") && text.contains(".disable()") {
findings.push(create_finding_with_confidence(
self.id(),
&node,
&parsed.path,
&parsed.content,
Severity::Warning,
"CSRF protection is disabled - this may expose the application to cross-site request forgery attacks",
Language::Java,
Confidence::High,
));
}
if (text.contains(".authorizeRequests(")
|| text.contains(".authorizeHttpRequests("))
&& text.contains(".anyRequest()")
&& text.contains(".permitAll()")
{
let has_authenticated = text.contains(".authenticated()");
let has_specific_matchers = text.contains(".antMatchers(")
|| text.contains(".requestMatchers(")
|| text.contains(".mvcMatchers(");
if !has_authenticated && !has_specific_matchers {
findings.push(create_finding_with_confidence(
self.id(),
&node,
&parsed.path,
&parsed.content,
Severity::Warning,
"All requests permitted without authentication - review security configuration",
Language::Java,
Confidence::High,
));
}
}
if text.contains(".httpBasic(") && text.contains(".disable()") {
findings.push(create_finding_with_confidence(
self.id(),
&node,
&parsed.path,
&parsed.content,
Severity::Info,
"HTTP Basic authentication disabled - ensure alternative authentication is configured",
Language::Java,
Confidence::Medium,
));
}
if text.contains(".sessionManagement(")
&& text.contains("SessionCreationPolicy.STATELESS")
{
}
}
});
cursor = parsed.tree.walk();
find_nodes_by_kind(&mut cursor, "annotation", |node: Node| {
if let Ok(text) = node.utf8_text(parsed.content.as_bytes()) {
if text.contains("@CrossOrigin") {
if text.contains("origins = \"*\"")
|| text.contains("origins=\"*\"")
|| text.contains("value = \"*\"")
|| text.contains("value=\"*\"")
{
findings.push(create_finding_with_confidence(
self.id(),
&node,
&parsed.path,
&parsed.content,
Severity::Warning,
"@CrossOrigin with wildcard origin (*) allows any domain - restrict to specific origins",
Language::Java,
Confidence::High,
));
}
if text.contains("allowCredentials")
&& (text.contains("\"*\"") || text.contains("= true"))
{
if text.contains("origins = \"*\"") || text.contains("origins=\"*\"") {
findings.push(create_finding_with_confidence(
self.id(),
&node,
&parsed.path,
&parsed.content,
Severity::Error,
"@CrossOrigin with wildcard origin and allowCredentials is a security risk",
Language::Java,
Confidence::High,
));
}
}
}
}
});
cursor = parsed.tree.walk();
find_nodes_by_kind(&mut cursor, "marker_annotation", |node: Node| {
if let Ok(text) = node.utf8_text(parsed.content.as_bytes()) {
if text.contains("@EnableWebSecurity") {
if parsed.content.contains("debug = true") {
findings.push(create_finding_with_confidence(
self.id(),
&node,
&parsed.path,
&parsed.content,
Severity::Warning,
"Spring Security debug mode enabled - disable in production",
Language::Java,
Confidence::High,
));
}
}
}
});
findings
}
}
pub struct GenericExceptionHint;
impl Rule for GenericExceptionHint {
fn id(&self) -> &str {
"java/generic-exception-hint"
}
fn description(&self) -> &str {
"Review hint: catching generic Exception may hide bugs"
}
fn applies_to(&self, lang: Language) -> bool {
lang == Language::Java
}
fn check(&self, parsed: &ParsedFile) -> Vec<Finding> {
let mut findings = Vec::new();
let mut cursor = parsed.tree.walk();
find_nodes_by_kind(&mut cursor, "catch_clause", |node: Node| {
if let Ok(text) = node.utf8_text(parsed.content.as_bytes()) {
if text.contains("Exception e)") || text.contains("Throwable") {
if parsed.content.contains("public static void main") {
return;
}
findings.push(create_finding_with_confidence(
self.id(),
&node,
&parsed.path,
&parsed.content,
Severity::Info,
"Catching generic Exception - consider catching specific exceptions",
Language::Java,
Confidence::Low,
));
}
}
});
findings
}
}
pub struct SystemOutHint;
impl Rule for SystemOutHint {
fn id(&self) -> &str {
"java/system-out-hint"
}
fn description(&self) -> &str {
"Review hint: System.out.println should use proper logging in production"
}
fn applies_to(&self, lang: Language) -> bool {
lang == Language::Java
}
fn check(&self, parsed: &ParsedFile) -> Vec<Finding> {
let mut findings = Vec::new();
let mut cursor = parsed.tree.walk();
find_nodes_by_kind(&mut cursor, "method_invocation", |node: Node| {
if let Ok(text) = node.utf8_text(parsed.content.as_bytes())
&& (text.contains("System.out.print") || text.contains("System.err.print"))
{
findings.push(create_finding_with_confidence(
self.id(),
&node,
&parsed.path,
&parsed.content,
Severity::Info,
"System.out detected - consider using a logging framework",
Language::Java,
Confidence::Low,
));
}
});
findings
}
}
fn find_nodes_by_kind<F>(cursor: &mut tree_sitter::TreeCursor, kind: &str, mut callback: F)
where
F: FnMut(Node),
{
loop {
let node = cursor.node();
if node.kind() == kind {
callback(node);
}
if cursor.goto_first_child() {
continue;
}
loop {
if cursor.goto_next_sibling() {
break;
}
if !cursor.goto_parent() {
return;
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use rma_common::RmaConfig;
use rma_parser::ParserEngine;
use std::path::Path;
fn parse_java(content: &str) -> ParsedFile {
let config = RmaConfig::default();
let parser = ParserEngine::new(config);
parser.parse_file(Path::new("Test.java"), content).unwrap()
}
#[test]
fn test_deserialization_detection() {
let content = r#"
import java.io.ObjectInputStream;
public class Danger {
public Object deserialize(InputStream is) {
ObjectInputStream ois = new ObjectInputStream(is);
return ois.readObject();
}
}
"#;
let parsed = parse_java(content);
let rule = InsecureDeserializationRule;
let findings = rule.check(&parsed);
assert!(!findings.is_empty());
}
#[test]
fn test_npe_chained_method_calls_flagged() {
let content = r#"
public class Test {
public void process() {
String result = service.getUser().getAddress().getCity().toUpperCase();
}
}
"#;
let parsed = parse_java(content);
let rule = NpePronePatternsRule;
let findings = rule.check(&parsed);
assert!(
!findings.is_empty(),
"Deeply chained calls should be flagged"
);
assert!(findings[0].message.contains("chained"));
assert_eq!(findings[0].severity, Severity::Warning);
assert_eq!(findings[0].confidence, Confidence::Low);
}
#[test]
fn test_npe_safe_chains_not_flagged() {
let content = r#"
public class Test {
public void process() {
// StringBuilder chains are safe
String s = new StringBuilder().append("a").append("b").append("c").toString();
// Stream API chains are safe
List<String> result = list.stream().filter(x -> x != null).map(String::toUpperCase).collect(Collectors.toList());
// Optional chains are safe
String value = Optional.ofNullable(user).map(User::getName).orElse("default");
}
}
"#;
let parsed = parse_java(content);
let rule = NpePronePatternsRule;
let findings = rule.check(&parsed);
let chain_findings: Vec<_> = findings
.iter()
.filter(|f| f.message.contains("chained"))
.collect();
assert!(
chain_findings.is_empty(),
"Safe chain patterns should not be flagged: {:?}",
chain_findings
);
}
#[test]
fn test_npe_null_check_after_dereference() {
let content = r#"
public class Test {
public void process(User user) {
String name = user.getName();
if (user != null) {
System.out.println(name);
}
}
}
"#;
let parsed = parse_java(content);
let rule = NpePronePatternsRule;
let findings = rule.check(&parsed);
let late_check_findings: Vec<_> = findings
.iter()
.filter(|f| f.message.contains("too late"))
.collect();
assert!(
!late_check_findings.is_empty(),
"Null check after dereference should be flagged"
);
}
#[test]
fn test_unclosed_file_input_stream() {
let content = r#"
public class Test {
public void read() {
FileInputStream fis = new FileInputStream("test.txt");
fis.read();
}
}
"#;
let parsed = parse_java(content);
let rule = UnclosedResourceRule;
let findings = rule.check(&parsed);
assert!(
!findings.is_empty(),
"Unclosed FileInputStream should be flagged"
);
assert!(findings[0].message.contains("FileInputStream"));
assert!(findings[0].message.contains("try-with-resources"));
}
#[test]
fn test_unclosed_connection() {
let content = r#"
public class Test {
public void query() {
Connection conn = DriverManager.getConnection(url);
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("SELECT 1");
}
}
"#;
let parsed = parse_java(content);
let rule = UnclosedResourceRule;
let findings = rule.check(&parsed);
assert!(findings.is_empty() || !findings[0].message.contains("Connection"));
}
#[test]
fn test_resource_in_try_with_resources_not_flagged() {
let content = r#"
public class Test {
public void read() {
try (FileInputStream fis = new FileInputStream("test.txt")) {
fis.read();
}
}
}
"#;
let parsed = parse_java(content);
let rule = UnclosedResourceRule;
let findings = rule.check(&parsed);
assert!(
findings.is_empty(),
"Resource in try-with-resources should not be flagged"
);
}
#[test]
fn test_unclosed_buffered_reader() {
let content = r#"
public class Test {
public void read() {
BufferedReader reader = new BufferedReader(new FileReader("test.txt"));
String line = reader.readLine();
}
}
"#;
let parsed = parse_java(content);
let rule = UnclosedResourceRule;
let findings = rule.check(&parsed);
assert!(
!findings.is_empty(),
"Unclosed BufferedReader should be flagged"
);
}
#[test]
fn test_log_injection_with_request_parameter() {
let content = r#"
public class Controller {
private Logger logger = LoggerFactory.getLogger(Controller.class);
public void handle(HttpServletRequest request) {
logger.info("User logged in: " + request.getParameter("username"));
}
}
"#;
let parsed = parse_java(content);
let rule = LogInjectionRule;
let findings = rule.check(&parsed);
assert!(!findings.is_empty(), "Log injection should be flagged");
assert!(findings[0].message.contains("log injection"));
assert_eq!(findings[0].severity, Severity::Warning);
}
#[test]
fn test_log_injection_parameterized_not_flagged() {
let content = r#"
public class Controller {
private Logger logger = LoggerFactory.getLogger(Controller.class);
public void handle() {
String user = "admin";
logger.info("User logged in: {}", user);
}
}
"#;
let parsed = parse_java(content);
let rule = LogInjectionRule;
let findings = rule.check(&parsed);
let high_severity: Vec<_> = findings
.iter()
.filter(|f| f.severity == Severity::Warning || f.severity == Severity::Error)
.collect();
assert!(
high_severity.is_empty(),
"Parameterized logging without user input should not be flagged"
);
}
#[test]
fn test_log_with_header_input() {
let content = r#"
public class Controller {
private static final Logger LOG = LoggerFactory.getLogger(Controller.class);
public void handle(HttpServletRequest request) {
LOG.warn("Auth header received: " + request.getHeader("Authorization"));
}
}
"#;
let parsed = parse_java(content);
let rule = LogInjectionRule;
let findings = rule.check(&parsed);
assert!(!findings.is_empty(), "Log with header should be flagged");
}
#[test]
fn test_csrf_disabled_flagged() {
let content = r#"
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
}
}
"#;
let parsed = parse_java(content);
let rule = SpringSecurityMisconfigRule;
let findings = rule.check(&parsed);
assert!(!findings.is_empty(), "CSRF disable should be flagged");
assert!(findings[0].message.contains("CSRF"));
assert_eq!(findings[0].confidence, Confidence::High);
}
#[test]
fn test_permit_all_flagged() {
let content = r#"
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().permitAll();
}
}
"#;
let parsed = parse_java(content);
let rule = SpringSecurityMisconfigRule;
let findings = rule.check(&parsed);
assert!(
!findings.is_empty(),
"permitAll for anyRequest should be flagged"
);
assert!(
findings
.iter()
.any(|f| f.message.contains("permitted without authentication"))
);
}
#[test]
fn test_cross_origin_wildcard_flagged() {
let content = r#"
@RestController
public class ApiController {
@CrossOrigin(origins = "*")
@GetMapping("/api/data")
public String getData() {
return "data";
}
}
"#;
let parsed = parse_java(content);
let rule = SpringSecurityMisconfigRule;
let findings = rule.check(&parsed);
assert!(
!findings.is_empty(),
"@CrossOrigin with wildcard should be flagged"
);
assert!(findings[0].message.contains("wildcard"));
}
#[test]
fn test_cross_origin_specific_origin_not_flagged() {
let content = r#"
@RestController
public class ApiController {
@CrossOrigin(origins = "https://example.com")
@GetMapping("/api/data")
public String getData() {
return "data";
}
}
"#;
let parsed = parse_java(content);
let rule = SpringSecurityMisconfigRule;
let findings = rule.check(&parsed);
let wildcard_findings: Vec<_> = findings
.iter()
.filter(|f| f.message.contains("wildcard"))
.collect();
assert!(
wildcard_findings.is_empty(),
"Specific origin should not be flagged as wildcard"
);
}
#[test]
fn test_cross_origin_with_credentials_and_wildcard() {
let content = r#"
@RestController
public class ApiController {
@CrossOrigin(origins = "*", allowCredentials = true)
@GetMapping("/api/data")
public String getData() {
return "data";
}
}
"#;
let parsed = parse_java(content);
let rule = SpringSecurityMisconfigRule;
let findings = rule.check(&parsed);
let critical_findings: Vec<_> = findings
.iter()
.filter(|f| f.severity == Severity::Error)
.collect();
assert!(
!critical_findings.is_empty(),
"Wildcard with credentials should be flagged as Error"
);
}
#[test]
fn test_proper_security_config_minimal_findings() {
let content = r#"
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
.and()
.authorizeRequests()
.antMatchers("/public/**").permitAll()
.anyRequest().authenticated();
}
}
"#;
let parsed = parse_java(content);
let rule = SpringSecurityMisconfigRule;
let findings = rule.check(&parsed);
let high_severity: Vec<_> = findings
.iter()
.filter(|f| f.severity == Severity::Warning || f.severity == Severity::Error)
.collect();
assert!(
high_severity.is_empty(),
"Proper security config should not have warnings: {:?}",
high_severity
);
}
#[test]
fn test_rule_ids_correct() {
assert_eq!(NpePronePatternsRule.id(), "java/potential-npe");
assert_eq!(UnclosedResourceRule.id(), "java/unclosed-resource");
assert_eq!(LogInjectionRule.id(), "java/log-injection");
assert_eq!(
SpringSecurityMisconfigRule.id(),
"java/spring-security-misconfig"
);
}
#[test]
fn test_rules_apply_to_java_only() {
assert!(NpePronePatternsRule.applies_to(Language::Java));
assert!(!NpePronePatternsRule.applies_to(Language::JavaScript));
assert!(!NpePronePatternsRule.applies_to(Language::Python));
assert!(UnclosedResourceRule.applies_to(Language::Java));
assert!(LogInjectionRule.applies_to(Language::Java));
assert!(SpringSecurityMisconfigRule.applies_to(Language::Java));
}
#[test]
fn test_unclosed_resource_uses_flow() {
assert!(UnclosedResourceRule.uses_flow());
assert!(!NpePronePatternsRule.uses_flow());
assert!(!LogInjectionRule.uses_flow());
assert!(!SpringSecurityMisconfigRule.uses_flow());
}
#[test]
fn test_npe_map_get_method_call_pattern() {
let content = r#"
public class Test {
public void process(Map<String, User> users, String id) {
String name = users.get(id).getName().toUpperCase();
}
}
"#;
let parsed = parse_java(content);
let rule = NpePronePatternsRule;
let findings = rule.check(&parsed);
let npe_findings: Vec<_> = findings
.iter()
.filter(|f| f.message.contains("chained") || f.message.contains("NPE"))
.collect();
assert!(
!npe_findings.is_empty(),
"map.get().method() pattern should be flagged"
);
}
#[test]
fn test_npe_null_check_before_use_not_flagged() {
let content = r#"
public class Test {
public void process(User user) {
if (user != null) {
String name = user.getName();
System.out.println(name);
}
}
}
"#;
let parsed = parse_java(content);
let rule = NpePronePatternsRule;
let findings = rule.check(&parsed);
let late_check: Vec<_> = findings
.iter()
.filter(|f| f.message.contains("too late"))
.collect();
assert!(
late_check.is_empty(),
"Null check before use should not trigger 'too late' finding"
);
}
#[test]
fn test_npe_short_chain_not_flagged() {
let content = r#"
public class Test {
public void process(User user) {
String name = user.getName();
}
}
"#;
let parsed = parse_java(content);
let rule = NpePronePatternsRule;
let findings = rule.check(&parsed);
let chain_findings: Vec<_> = findings
.iter()
.filter(|f| f.message.contains("chained"))
.collect();
assert!(
chain_findings.is_empty(),
"Simple method call should not be flagged as deep chain"
);
}
#[test]
fn test_unclosed_socket_flagged() {
let content = r#"
public class Test {
public void connect(String host, int port) {
Socket socket = new Socket(host, port);
OutputStream out = socket.getOutputStream();
out.write(data);
}
}
"#;
let parsed = parse_java(content);
let rule = UnclosedResourceRule;
let findings = rule.check(&parsed);
let socket_findings: Vec<_> = findings
.iter()
.filter(|f| f.message.contains("Socket"))
.collect();
assert!(
!socket_findings.is_empty(),
"Unclosed Socket should be flagged"
);
}
#[test]
fn test_prepared_statement_in_try_with_resources_not_flagged() {
let content = r#"
public class Test {
public void query(Connection conn) {
try (PreparedStatement stmt = conn.prepareStatement("SELECT 1")) {
ResultSet rs = stmt.executeQuery();
}
}
}
"#;
let parsed = parse_java(content);
let rule = UnclosedResourceRule;
let findings = rule.check(&parsed);
let stmt_findings: Vec<_> = findings
.iter()
.filter(|f| f.message.contains("PreparedStatement"))
.collect();
assert!(
stmt_findings.is_empty(),
"PreparedStatement in try-with-resources should not be flagged"
);
}
#[test]
fn test_field_assignment_resource_not_flagged() {
let content = r#"
public class ConnectionManager {
private Connection connection;
public void init() {
this.connection = new Connection(url);
}
public void close() {
this.connection.close();
}
}
"#;
let parsed = parse_java(content);
let rule = UnclosedResourceRule;
let findings = rule.check(&parsed);
let conn_findings: Vec<_> = findings
.iter()
.filter(|f| f.message.contains("Connection"))
.collect();
let _ = conn_findings; }
#[test]
fn test_log_debug_with_user_input_flagged() {
let content = r#"
public class Controller {
private Logger logger = LoggerFactory.getLogger(Controller.class);
public void handle(HttpServletRequest request) {
logger.debug("Request from: " + request.getHeader("X-Forwarded-For"));
}
}
"#;
let parsed = parse_java(content);
let rule = LogInjectionRule;
let findings = rule.check(&parsed);
assert!(
!findings.is_empty(),
"logger.debug with user input should be flagged"
);
}
#[test]
fn test_log_with_sanitized_input_info_level() {
let content = r#"
public class Controller {
private Logger logger = LoggerFactory.getLogger(Controller.class);
public void handle(HttpServletRequest request) {
String sanitized = sanitize(request.getParameter("input"));
logger.info("Received: {}", sanitized);
}
}
"#;
let parsed = parse_java(content);
let rule = LogInjectionRule;
let findings = rule.check(&parsed);
let info_findings: Vec<_> = findings
.iter()
.filter(|f| f.severity == Severity::Info)
.collect();
assert!(
!info_findings.is_empty() || findings.is_empty(),
"Log with sanitized user input should be Info or not flagged"
);
}
#[test]
fn test_log_static_message_not_flagged() {
let content = r#"
public class Service {
private Logger log = LoggerFactory.getLogger(Service.class);
public void process() {
log.info("Processing started");
log.error("An error occurred");
log.warn("This is a warning");
}
}
"#;
let parsed = parse_java(content);
let rule = LogInjectionRule;
let findings = rule.check(&parsed);
let injection_findings: Vec<_> = findings
.iter()
.filter(|f| f.severity == Severity::Warning || f.severity == Severity::Error)
.collect();
assert!(
injection_findings.is_empty(),
"Static log messages should not be flagged as injection risk"
);
}
#[test]
fn test_http_security_with_default_csrf_not_flagged() {
let content = r#"
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/public/**").permitAll()
.anyRequest().authenticated();
// CSRF enabled by default
}
}
"#;
let parsed = parse_java(content);
let rule = SpringSecurityMisconfigRule;
let findings = rule.check(&parsed);
let csrf_findings: Vec<_> = findings
.iter()
.filter(|f| f.message.contains("CSRF"))
.collect();
assert!(
csrf_findings.is_empty(),
"Default CSRF (enabled) should not be flagged"
);
}
#[test]
fn test_csrf_explicitly_disabled_flagged() {
let content = r#"
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests().anyRequest().authenticated();
}
}
"#;
let parsed = parse_java(content);
let rule = SpringSecurityMisconfigRule;
let findings = rule.check(&parsed);
let csrf_findings: Vec<_> = findings
.iter()
.filter(|f| f.message.contains("CSRF"))
.collect();
assert!(
!csrf_findings.is_empty(),
"Explicit CSRF disable should be flagged"
);
}
#[test]
fn test_authenticated_endpoint_not_flagged() {
let content = r#"
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/api/public/**").permitAll()
.anyRequest().authenticated();
}
}
"#;
let parsed = parse_java(content);
let rule = SpringSecurityMisconfigRule;
let findings = rule.check(&parsed);
let permit_findings: Vec<_> = findings
.iter()
.filter(|f| f.message.contains("permitted without authentication"))
.collect();
assert!(
permit_findings.is_empty(),
"Proper auth config with authenticated() should not be flagged"
);
}
}