use crate::rules::{Rule, create_finding, create_finding_at_line};
use regex::Regex;
use rma_common::{Finding, Language, Severity};
use rma_parser::ParsedFile;
use std::collections::HashSet;
use std::path::Path;
use std::sync::LazyLock;
use tree_sitter::Node;
#[inline]
pub fn is_generated_file(path: &Path, content: &str) -> bool {
if let Some(file_name) = path.file_name().and_then(|n| n.to_str()) {
let name_lower = file_name.to_lowercase();
if name_lower.starts_with("zz_generated")
|| name_lower.contains("_zz_generated")
|| name_lower.ends_with("_generated.go")
|| name_lower.starts_with("generated_")
{
return true;
}
if name_lower.ends_with(".pb.go")
|| name_lower.ends_with("_pb2.py")
|| name_lower.ends_with(".pb.ts")
|| name_lower.ends_with(".pb.js")
|| name_lower.ends_with("_grpc.pb.go")
{
return true;
}
if name_lower.ends_with(".gen.go")
|| name_lower.ends_with("_gen.go")
|| name_lower.ends_with(".generated.ts")
|| name_lower.ends_with(".generated.js")
|| name_lower.contains("_mock.go") || name_lower.contains("mock_")
{
return true;
}
}
let header = content.lines().take(10).collect::<Vec<_>>().join("\n");
let header_upper = header.to_uppercase();
if header_upper.contains("DO NOT EDIT")
|| header_upper.contains("AUTOMATICALLY GENERATED")
|| header_upper.contains("AUTO-GENERATED")
|| header_upper.contains("CODE GENERATED BY")
|| header_upper.contains("GENERATED BY")
|| header_upper.contains("THIS FILE IS GENERATED")
{
return true;
}
false
}
#[inline]
pub fn is_test_or_fixture_file(path: &Path) -> bool {
let path_str = path.to_string_lossy().to_lowercase();
if path_str.contains("/test/")
|| path_str.contains("/tests/")
|| path_str.contains("/testing/")
|| path_str.contains("/__tests__/")
|| path_str.contains("/spec/")
|| path_str.contains("/specs/")
|| path_str.contains("/fixture/")
|| path_str.contains("/fixtures/")
|| path_str.contains("/testdata/")
|| path_str.contains("/test_data/")
|| path_str.contains("/mock/")
|| path_str.contains("/mocks/")
|| path_str.contains("/fake/")
|| path_str.contains("/fakes/")
|| path_str.contains("/stub/")
|| path_str.contains("/stubs/")
|| path_str.contains("/example/")
|| path_str.contains("/examples/")
|| path_str.contains("/sample/")
|| path_str.contains("/samples/")
|| path_str.contains("/demo/")
|| path_str.contains("/testutil/")
|| path_str.contains("/testutils/")
{
return true;
}
if let Some(file_name) = path.file_name().and_then(|n| n.to_str()) {
let name_lower = file_name.to_lowercase();
if name_lower.starts_with("test_")
|| name_lower.starts_with("test.")
|| name_lower.ends_with("_test.go")
|| name_lower.ends_with("_test.rs")
|| name_lower.ends_with("_test.py")
|| name_lower.ends_with(".test.js")
|| name_lower.ends_with(".test.ts")
|| name_lower.ends_with(".test.jsx")
|| name_lower.ends_with(".test.tsx")
|| name_lower.ends_with(".spec.js")
|| name_lower.ends_with(".spec.ts")
|| name_lower.ends_with(".spec.jsx")
|| name_lower.ends_with(".spec.tsx")
|| name_lower.ends_with("_spec.rb")
|| name_lower.contains("_mock")
|| name_lower.contains("_fake")
|| name_lower.contains("_stub")
|| name_lower.contains("_fixture")
|| name_lower == "conftest.py"
|| name_lower == "setup_test.go"
{
return true;
}
}
false
}
static SECRET_PATTERN: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r#"(?i)\b(api[_-]?key|secret[_-]?key|auth[_-]?token|access[_-]?token|private[_-]?key|access[_-]?key|client[_-]?secret|db[_-]?password|database[_-]?password|admin[_-]?password)\s*[:=]\s*["'][^"']{8,}["']"#).unwrap()
});
static AWS_KEY_PATTERN: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r#"AKIA[0-9A-Z]{16}"#).unwrap());
static AWS_SECRET_PATTERN: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r#"(?i)aws[_-]?secret[_-]?access[_-]?key\s*[:=]\s*["'][A-Za-z0-9/+=]{40}["']"#)
.unwrap()
});
static GITHUB_TOKEN_PATTERN: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r#"gh[ps]_[A-Za-z0-9]{36,}"#).unwrap());
static PRIVATE_KEY_PATTERN: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r#"-----BEGIN\s+(RSA\s+)?PRIVATE\s+KEY-----"#).unwrap());
static PASSWORD_ASSIGNMENT_PATTERN: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r#"(?i)\b(password|passwd|pwd)\s*[:=]\s*["']([^"']{6,})["']"#).unwrap()
});
pub struct TodoFixmeRule;
impl Rule for TodoFixmeRule {
fn id(&self) -> &str {
"generic/todo-fixme"
}
fn description(&self) -> &str {
"Detects TODO and FIXME comments that may indicate incomplete functionality"
}
fn applies_to(&self, _lang: Language) -> bool {
true
}
fn check(&self, parsed: &ParsedFile) -> Vec<Finding> {
let mut findings = Vec::new();
for (line_num, line) in parsed.content.lines().enumerate() {
let upper = line.to_uppercase();
if upper.contains("TODO")
|| upper.contains("FIXME")
|| upper.contains("HACK")
|| upper.contains("XXX")
{
let mut finding = Finding {
id: format!("{}-{}", self.id(), line_num),
rule_id: self.id().to_string(),
message: "TODO/FIXME comment indicates potentially incomplete code".to_string(),
severity: Severity::Info,
location: rma_common::SourceLocation::new(
parsed.path.clone(),
line_num + 1,
1,
line_num + 1,
line.len(),
),
language: parsed.language,
snippet: Some(line.trim().to_string()),
suggestion: None,
confidence: rma_common::Confidence::High,
category: rma_common::FindingCategory::Style,
fingerprint: None,
properties: None,
};
finding.compute_fingerprint();
findings.push(finding);
}
}
findings
}
}
pub struct LongFunctionRule {
max_lines: usize,
}
impl LongFunctionRule {
pub fn new(max_lines: usize) -> Self {
Self { max_lines }
}
}
impl Rule for LongFunctionRule {
fn id(&self) -> &str {
"generic/long-function"
}
fn description(&self) -> &str {
"Detects functions that exceed the recommended line count"
}
fn applies_to(&self, _lang: Language) -> bool {
true
}
fn check(&self, parsed: &ParsedFile) -> Vec<Finding> {
let mut findings = Vec::new();
let mut cursor = parsed.tree.walk();
let function_kinds = [
"function_item",
"function_declaration",
"function_definition",
"method_declaration",
"arrow_function",
];
find_nodes_by_kinds(&mut cursor, &function_kinds, |node: Node| {
let start = node.start_position().row;
let end = node.end_position().row;
let lines = end - start + 1;
if lines > self.max_lines {
findings.push(create_finding(
self.id(),
&node,
&parsed.path,
&parsed.content,
Severity::Warning,
&format!(
"Function has {} lines (max: {}) - consider refactoring",
lines, self.max_lines
),
parsed.language,
));
}
});
findings
}
}
pub struct HighComplexityRule {
max_complexity: usize,
}
impl HighComplexityRule {
pub fn new(max_complexity: usize) -> Self {
Self { max_complexity }
}
}
pub struct DuplicateFunctionRule {
min_lines: usize,
}
impl DuplicateFunctionRule {
pub fn new(min_lines: usize) -> Self {
Self { min_lines }
}
fn normalize_body(content: &str, node: &Node) -> String {
let mut cursor = node.walk();
let mut body_node: Option<Node> = None;
if cursor.goto_first_child() {
loop {
let child = cursor.node();
if child.kind() == "block"
|| child.kind() == "statement_block"
|| child.kind() == "compound_statement"
|| child.kind() == "function_body"
{
body_node = Some(child);
break;
}
if !cursor.goto_next_sibling() {
break;
}
}
}
let body = if let Some(bn) = body_node {
let start = bn.start_byte();
let end = bn.end_byte();
if end <= content.len() && start < end {
&content[start..end]
} else {
return String::new();
}
} else {
let start = node.start_byte();
let end = node.end_byte();
if end > content.len() || start >= end {
return String::new();
}
&content[start..end]
};
let mut result = String::new();
let mut in_line_comment = false;
let mut in_block_comment = false;
let mut prev_char = ' ';
for c in body.chars() {
if in_line_comment {
if c == '\n' {
in_line_comment = false;
}
continue;
}
if in_block_comment {
if prev_char == '*' && c == '/' {
in_block_comment = false;
}
prev_char = c;
continue;
}
if prev_char == '/' && c == '/' {
in_line_comment = true;
result.pop(); continue;
}
if prev_char == '/' && c == '*' {
in_block_comment = true;
result.pop(); continue;
}
if !c.is_whitespace() {
result.push(c.to_ascii_lowercase());
}
prev_char = c;
}
result
}
fn get_function_name(node: &Node, content: &str) -> Option<String> {
let mut cursor = node.walk();
if cursor.goto_first_child() {
loop {
let child = cursor.node();
if child.kind() == "identifier"
|| child.kind() == "name"
|| child.kind() == "property_identifier"
{
let start = child.start_byte();
let end = child.end_byte();
if end <= content.len() {
return Some(content[start..end].to_string());
}
}
if !cursor.goto_next_sibling() {
break;
}
}
}
None
}
}
impl Rule for HighComplexityRule {
fn id(&self) -> &str {
"generic/high-complexity"
}
fn description(&self) -> &str {
"Detects functions with high cyclomatic complexity"
}
fn applies_to(&self, _lang: Language) -> bool {
true
}
fn check(&self, parsed: &ParsedFile) -> Vec<Finding> {
let mut findings = Vec::new();
let mut cursor = parsed.tree.walk();
let function_kinds = [
"function_item",
"function_declaration",
"function_definition",
"method_declaration",
];
find_nodes_by_kinds(&mut cursor, &function_kinds, |node: Node| {
let complexity = count_branches(&node, parsed.language);
if complexity > self.max_complexity {
findings.push(create_finding(
self.id(),
&node,
&parsed.path,
&parsed.content,
Severity::Warning,
&format!(
"Function has complexity {} (max: {}) - consider simplifying",
complexity, self.max_complexity
),
parsed.language,
));
}
});
findings
}
}
impl Rule for DuplicateFunctionRule {
fn id(&self) -> &str {
"generic/duplicate-function"
}
fn description(&self) -> &str {
"Detects duplicate functions that could be refactored"
}
fn applies_to(&self, _lang: Language) -> bool {
true
}
fn check(&self, parsed: &ParsedFile) -> Vec<Finding> {
use std::collections::HashMap;
let mut findings = Vec::new();
let mut cursor = parsed.tree.walk();
let function_kinds = [
"function_item",
"function_declaration",
"function_definition",
"method_declaration",
"arrow_function",
];
struct FuncInfo {
name: String,
line: usize,
col: usize,
}
let mut body_to_funcs: HashMap<String, Vec<FuncInfo>> = HashMap::new();
find_nodes_by_kinds(&mut cursor, &function_kinds, |node: Node| {
let start = node.start_position().row;
let end = node.end_position().row;
let lines = end - start + 1;
if lines < self.min_lines {
return;
}
let normalized = Self::normalize_body(&parsed.content, &node);
if normalized.len() < 50 {
return;
}
let name = Self::get_function_name(&node, &parsed.content)
.unwrap_or_else(|| format!("anonymous@{}", start + 1));
body_to_funcs.entry(normalized).or_default().push(FuncInfo {
name,
line: start + 1,
col: node.start_position().column + 1,
});
});
for (_body, funcs) in body_to_funcs.iter() {
if funcs.len() > 1 {
let first = &funcs[0];
for dup in funcs.iter().skip(1) {
let mut finding = Finding {
id: format!("{}-{}-{}", self.id(), dup.line, dup.col),
rule_id: self.id().to_string(),
message: format!(
"Function '{}' is a duplicate of '{}' at line {} - consider extracting to shared function",
dup.name, first.name, first.line
),
severity: Severity::Warning,
location: rma_common::SourceLocation::new(
parsed.path.clone(),
dup.line,
dup.col,
dup.line,
dup.col + 10,
),
language: parsed.language,
snippet: Some(format!("fn {}(...)", dup.name)),
suggestion: Some(format!(
"Extract shared logic from '{}' and '{}'",
first.name, dup.name
)),
confidence: rma_common::Confidence::High,
category: rma_common::FindingCategory::Style,
fingerprint: None,
properties: None,
};
finding.compute_fingerprint();
findings.push(finding);
}
}
}
findings
}
}
pub struct HardcodedSecretRule;
impl HardcodedSecretRule {
fn is_real_password(value: &str) -> bool {
let lower = value.to_lowercase();
if lower.is_empty()
|| lower == "password"
|| lower == "changeme"
|| lower == "placeholder"
|| lower == "your_password"
|| lower == "your-password"
|| lower == "xxx"
|| lower == "***"
|| lower.starts_with("${")
|| lower.starts_with("{{")
|| lower.contains("example")
|| lower.contains("test")
|| lower.contains("dummy")
|| lower.contains("sample")
|| lower.contains("fake")
|| lower.contains("mock")
{
return false;
}
let has_digit = value.chars().any(|c| c.is_ascii_digit());
let has_upper = value.chars().any(|c| c.is_ascii_uppercase());
let has_lower = value.chars().any(|c| c.is_ascii_lowercase());
let has_special = value.chars().any(|c| !c.is_alphanumeric());
(has_digit && has_upper && has_lower)
|| (has_special && value.len() >= 8)
|| value.len() >= 16
}
fn is_false_positive_context(line: &str) -> bool {
let lower = line.to_lowercase();
if lower.contains("accessorkey")
|| lower.contains("storagekey")
|| lower.contains("cachekey")
|| lower.contains("localstoragekey")
|| lower.contains("sessionkey")
|| lower.contains("sortkey")
|| lower.contains("primarykey")
|| lower.contains("foreignkey")
|| lower.contains("uniquekey")
|| lower.contains("indexkey")
{
return true;
}
if lower.contains("cache-control")
|| lower.contains("content-type")
|| lower.contains("accept")
|| lower.contains("authorization: bearer") || lower.contains("x-api-key")
{
return true;
}
if lower.contains("accessor:")
|| lower.contains("header:")
|| lower.contains("field:")
|| lower.contains("dataindex:")
{
return true;
}
if lower.contains("t('") || lower.contains("i18n") || lower.contains("translate") {
return true;
}
if lower.contains(": string") || lower.contains(": number") || lower.contains("interface ")
{
return true;
}
false
}
}
impl Rule for HardcodedSecretRule {
fn id(&self) -> &str {
"generic/hardcoded-secret"
}
fn description(&self) -> &str {
"Detects hardcoded secrets, API keys, and passwords"
}
fn applies_to(&self, _lang: Language) -> bool {
true
}
fn check(&self, parsed: &ParsedFile) -> Vec<Finding> {
if is_test_or_fixture_file(&parsed.path) {
return Vec::new();
}
let mut findings = Vec::new();
for (line_num, line) in parsed.content.lines().enumerate() {
let trimmed = line.trim();
if trimmed.starts_with("//")
|| trimmed.starts_with('#')
|| trimmed.starts_with("/*")
|| trimmed.starts_with('*')
|| trimmed.starts_with("'''")
|| trimmed.starts_with("\"\"\"")
{
continue;
}
if Self::is_false_positive_context(line) {
continue;
}
if SECRET_PATTERN.is_match(line) {
findings.push(create_finding_at_line(
self.id(),
&parsed.path,
line_num + 1,
"[REDACTED SECRET]",
Severity::Critical,
"Hardcoded secret detected - use environment variables or a secrets manager",
parsed.language,
));
continue;
}
if AWS_KEY_PATTERN.is_match(line) {
findings.push(create_finding_at_line(
self.id(),
&parsed.path,
line_num + 1,
"[REDACTED AWS KEY]",
Severity::Critical,
"AWS access key ID detected - never commit credentials",
parsed.language,
));
continue;
}
if AWS_SECRET_PATTERN.is_match(line) {
findings.push(create_finding_at_line(
self.id(),
&parsed.path,
line_num + 1,
"[REDACTED AWS SECRET]",
Severity::Critical,
"AWS secret access key detected - never commit credentials",
parsed.language,
));
continue;
}
if GITHUB_TOKEN_PATTERN.is_match(line) {
findings.push(create_finding_at_line(
self.id(),
&parsed.path,
line_num + 1,
"[REDACTED GITHUB TOKEN]",
Severity::Critical,
"GitHub token detected - use GITHUB_TOKEN secret instead",
parsed.language,
));
continue;
}
if PRIVATE_KEY_PATTERN.is_match(line) {
findings.push(create_finding_at_line(
self.id(),
&parsed.path,
line_num + 1,
"[REDACTED PRIVATE KEY]",
Severity::Critical,
"Private key detected in source - store in secure key management",
parsed.language,
));
continue;
}
if let Some(caps) = PASSWORD_ASSIGNMENT_PATTERN.captures(line)
&& let Some(value_match) = caps.get(2)
{
let value = value_match.as_str();
if Self::is_real_password(value) {
findings.push(create_finding_at_line(
self.id(),
&parsed.path,
line_num + 1,
"[REDACTED PASSWORD]",
Severity::Critical,
"Hardcoded password detected - use environment variables or a secrets manager",
parsed.language,
));
}
}
}
findings
}
}
pub struct InsecureCryptoRule;
impl Rule for InsecureCryptoRule {
fn id(&self) -> &str {
"generic/insecure-crypto"
}
fn description(&self) -> &str {
"Detects use of weak or deprecated cryptographic algorithms"
}
fn applies_to(&self, _lang: Language) -> bool {
true
}
fn check(&self, parsed: &ParsedFile) -> Vec<Finding> {
let mut findings = Vec::new();
for (line_num, line) in parsed.content.lines().enumerate() {
let trimmed = line.trim();
let lower = line.to_lowercase();
if trimmed.starts_with("//")
|| trimmed.starts_with("/*")
|| trimmed.starts_with('*')
|| trimmed.starts_with('#')
|| trimmed.starts_with("<!--")
{
continue;
}
if lower.contains(".contains(")
|| lower.contains(".is_match(")
|| lower.contains("regex")
{
continue;
}
if is_in_string_literal(&lower, "md5")
|| is_in_string_literal(&lower, "sha1")
|| is_in_string_literal(&lower, "des")
|| is_in_string_literal(&lower, "rc4")
|| is_in_string_literal(&lower, "ecb")
{
continue;
}
if lower.contains("md5")
&& (lower.contains("hash")
|| lower.contains("digest")
|| lower.contains("::")
|| lower.contains("import")
|| lower.contains("require"))
{
findings.push(create_finding_at_line(
self.id(),
&parsed.path,
line_num + 1,
line.trim(),
Severity::Error,
"MD5 is cryptographically broken - use SHA-256 or better for security",
parsed.language,
));
}
if lower.contains("sha1")
&& !lower.contains("sha1sum")
&& (lower.contains("hash")
|| lower.contains("digest")
|| lower.contains("::")
|| lower.contains("import"))
{
findings.push(create_finding_at_line(
self.id(),
&parsed.path,
line_num + 1,
line.trim(),
Severity::Warning,
"SHA-1 is deprecated for security - use SHA-256 or better",
parsed.language,
));
}
if (lower.contains("des") || lower.contains("3des") || lower.contains("triple_des"))
&& (lower.contains("encrypt")
|| lower.contains("cipher")
|| lower.contains("crypto"))
{
findings.push(create_finding_at_line(
self.id(),
&parsed.path,
line_num + 1,
line.trim(),
Severity::Error,
"DES/3DES is insecure - use AES-256-GCM or ChaCha20-Poly1305",
parsed.language,
));
}
if lower.contains("rc4")
&& (lower.contains("cipher")
|| lower.contains("crypto")
|| lower.contains("encrypt"))
{
findings.push(create_finding_at_line(
self.id(),
&parsed.path,
line_num + 1,
line.trim(),
Severity::Critical,
"RC4 is completely broken - use AES-GCM or ChaCha20-Poly1305",
parsed.language,
));
}
if lower.contains("ecb")
&& (lower.contains("mode") || lower.contains("cipher") || lower.contains("aes"))
{
findings.push(create_finding_at_line(
self.id(),
&parsed.path,
line_num + 1,
line.trim(),
Severity::Error,
"ECB mode is insecure - use GCM, CBC with HMAC, or authenticated encryption",
parsed.language,
));
}
}
findings
}
}
fn is_in_string_literal(line: &str, term: &str) -> bool {
if let Some(pos) = line.find(term) {
let before = &line[..pos];
let double_quotes = before.matches('"').count();
let single_quotes = before.matches('\'').count();
double_quotes % 2 == 1 || single_quotes % 2 == 1
} else {
false
}
}
fn find_nodes_by_kinds<F>(cursor: &mut tree_sitter::TreeCursor, kinds: &[&str], mut callback: F)
where
F: FnMut(Node),
{
let kinds_set: HashSet<&str> = kinds.iter().copied().collect();
loop {
let node = cursor.node();
if kinds_set.contains(node.kind()) {
callback(node);
}
if cursor.goto_first_child() {
continue;
}
loop {
if cursor.goto_next_sibling() {
break;
}
if !cursor.goto_parent() {
return;
}
}
}
}
static RUST_BRANCH_KINDS: LazyLock<HashSet<&'static str>> = LazyLock::new(|| {
[
"if_expression",
"match_expression",
"while_expression",
"for_expression",
]
.into_iter()
.collect()
});
static JS_BRANCH_KINDS: LazyLock<HashSet<&'static str>> = LazyLock::new(|| {
[
"if_statement",
"switch_statement",
"for_statement",
"while_statement",
]
.into_iter()
.collect()
});
static PYTHON_BRANCH_KINDS: LazyLock<HashSet<&'static str>> = LazyLock::new(|| {
[
"if_statement",
"for_statement",
"while_statement",
"try_statement",
]
.into_iter()
.collect()
});
static GO_BRANCH_KINDS: LazyLock<HashSet<&'static str>> = LazyLock::new(|| {
["if_statement", "for_statement", "switch_statement"]
.into_iter()
.collect()
});
static JAVA_BRANCH_KINDS: LazyLock<HashSet<&'static str>> = LazyLock::new(|| {
[
"if_statement",
"for_statement",
"while_statement",
"switch_expression",
]
.into_iter()
.collect()
});
fn count_branches(node: &Node, lang: Language) -> usize {
let branch_kinds: &HashSet<&str> = match lang {
Language::Rust => &RUST_BRANCH_KINDS,
Language::JavaScript | Language::TypeScript => &JS_BRANCH_KINDS,
Language::Python => &PYTHON_BRANCH_KINDS,
Language::Go => &GO_BRANCH_KINDS,
Language::Java => &JAVA_BRANCH_KINDS,
Language::Unknown => return 1,
};
let mut count = 1;
let mut cursor = node.walk();
loop {
let current = cursor.node();
if branch_kinds.contains(current.kind()) {
count += 1;
}
if cursor.goto_first_child() {
continue;
}
loop {
if cursor.goto_next_sibling() {
break;
}
if !cursor.goto_parent() {
return count;
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_generated_file_by_name() {
assert!(is_generated_file(
Path::new("/project/pkg/apis/v1/zz_generated.conversion.go"),
""
));
assert!(is_generated_file(
Path::new("/project/pkg/apis/v1/zz_generated.deepcopy.go"),
""
));
assert!(is_generated_file(Path::new("/project/api/types.pb.go"), ""));
assert!(is_generated_file(
Path::new("/project/api/service_grpc.pb.go"),
""
));
assert!(is_generated_file(
Path::new("/project/proto/types_pb2.py"),
""
));
assert!(is_generated_file(
Path::new("/project/api/types.gen.go"),
""
));
assert!(is_generated_file(
Path::new("/project/api/types_gen.go"),
""
));
assert!(is_generated_file(Path::new("/project/mock_service.go"), ""));
assert!(is_generated_file(Path::new("/project/service_mock.go"), ""));
assert!(!is_generated_file(Path::new("/project/main.go"), ""));
assert!(!is_generated_file(Path::new("/project/pkg/handler.go"), ""));
assert!(!is_generated_file(
Path::new("/project/internal/utils.go"),
""
));
}
#[test]
fn test_is_generated_file_by_content() {
let go_generated = r#"// Code generated by controller-gen. DO NOT EDIT.
package v1
import (
"unsafe"
)
"#;
assert!(is_generated_file(
Path::new("/project/types.go"),
go_generated
));
let proto_generated = r#"// Code generated by protoc-gen-go. DO NOT EDIT.
// source: api.proto
package api
"#;
assert!(is_generated_file(
Path::new("/project/api.go"),
proto_generated
));
let auto_generated = r#"// AUTO-GENERATED - Do not modify manually
package gen
"#;
assert!(is_generated_file(
Path::new("/project/gen.go"),
auto_generated
));
let regular = r#"// Package main provides the entry point
package main
func main() {}
"#;
assert!(!is_generated_file(Path::new("/project/main.go"), regular));
}
#[test]
fn test_is_test_or_fixture_file_directories() {
assert!(is_test_or_fixture_file(Path::new(
"/project/test/utils/helper.go"
)));
assert!(is_test_or_fixture_file(Path::new(
"/project/tests/integration.py"
)));
assert!(is_test_or_fixture_file(Path::new(
"/project/__tests__/component.test.js"
)));
assert!(is_test_or_fixture_file(Path::new(
"/project/spec/models/user_spec.rb"
)));
assert!(is_test_or_fixture_file(Path::new(
"/project/fixtures/data.json"
)));
assert!(is_test_or_fixture_file(Path::new(
"/project/testdata/sample.txt"
)));
assert!(is_test_or_fixture_file(Path::new("/project/mocks/api.ts")));
assert!(is_test_or_fixture_file(Path::new(
"/project/examples/demo.go"
)));
assert!(is_test_or_fixture_file(Path::new(
"/project/testutil/admission_webhook.go"
)));
assert!(!is_test_or_fixture_file(Path::new("/project/src/main.go")));
assert!(!is_test_or_fixture_file(Path::new("/project/lib/utils.py")));
assert!(!is_test_or_fixture_file(Path::new(
"/project/pkg/handler.go"
)));
assert!(!is_test_or_fixture_file(Path::new(
"/project/cmd/server/main.go"
)));
}
#[test]
fn test_is_test_or_fixture_file_names() {
assert!(is_test_or_fixture_file(Path::new(
"/project/src/handler_test.go"
)));
assert!(is_test_or_fixture_file(Path::new(
"/project/src/utils_test.rs"
)));
assert!(is_test_or_fixture_file(Path::new(
"/project/src/test_handler.py"
)));
assert!(is_test_or_fixture_file(Path::new(
"/project/src/component.test.js"
)));
assert!(is_test_or_fixture_file(Path::new(
"/project/src/component.spec.ts"
)));
assert!(is_test_or_fixture_file(Path::new("/project/conftest.py")));
assert!(is_test_or_fixture_file(Path::new("/project/api_mock.go")));
assert!(!is_test_or_fixture_file(Path::new("/project/src/main.go")));
assert!(!is_test_or_fixture_file(Path::new(
"/project/src/handler.py"
)));
assert!(!is_test_or_fixture_file(Path::new("/project/src/utils.ts")));
assert!(!is_test_or_fixture_file(Path::new(
"/project/pkg/testing_helper.go"
))); }
#[test]
fn test_hardcoded_secret_skips_test_files() {
use rma_common::RmaConfig;
use rma_parser::ParserEngine;
let config = RmaConfig::default();
let parser = ParserEngine::new(config);
let content = r#"
var LocalhostKey = []byte(`-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEA1Z5/aTwqY706M34tn60l8ZHkanWDl8mM1pYf4Q7qg3zA9XqW
-----END RSA PRIVATE KEY-----`)
"#;
let parsed_test = parser
.parse_file(Path::new("/project/test/utils/webhook.go"), content)
.unwrap();
let rule = HardcodedSecretRule;
let findings_test = rule.check(&parsed_test);
assert!(
findings_test.is_empty(),
"Should skip secrets in test files"
);
let parsed_prod = parser
.parse_file(Path::new("/project/pkg/webhook.go"), content)
.unwrap();
let findings_prod = rule.check(&parsed_prod);
assert!(
!findings_prod.is_empty(),
"Should detect secrets in production files"
);
}
}