use crate::core::{Finding, Severity};
use crate::plugins::traits::{PluginError, PluginReport, ScanContext, ScanPhase, SecurityPlugin};
use async_trait::async_trait;
use lazy_static::lazy_static;
use regex::Regex;
use std::time::Instant;
lazy_static! {
static ref PATTERNS: Vec<(Regex, &'static str, Severity)> = vec![
(
Regex::new(r"eval\s*\(").unwrap(),
"Dangerous eval() usage",
Severity::High
),
(
Regex::new(r"exec\s*\(").unwrap(),
"Dangerous exec() usage",
Severity::High
),
(
Regex::new(r"system\s*\(").unwrap(),
"Shell command execution",
Severity::Medium
),
(
Regex::new(r#"(?i)password\s*=\s*['"][^'"]+['"]"#).unwrap(),
"Hardcoded password",
Severity::Critical
),
(
Regex::new(r#"(?i)api[_-]?key\s*=\s*['"][^'"]+['"]"#).unwrap(),
"Hardcoded API key",
Severity::Critical
),
(
Regex::new(r#"(?i)secret\s*=\s*['"][^'"]+['"]"#).unwrap(),
"Hardcoded secret",
Severity::High
),
(
Regex::new(r"(?i)(bitcoin|btc)[_-]?(private[_-]?key|privkey)").unwrap(),
"Crypto private key",
Severity::Critical
),
(
Regex::new(r"/bin/(sh|bash|zsh|fish)\s+-c").unwrap(),
"Shell command injection risk",
Severity::High
),
(
Regex::new(r"(?i)backdoor").unwrap(),
"Potential backdoor",
Severity::Critical
),
(
Regex::new(r"(?i)(reverse|bind)[_-]?shell").unwrap(),
"Reverse/bind shell",
Severity::Critical
),
(
Regex::new(r"base64[_-]?decode").unwrap(),
"Base64 decode (potential obfuscation)",
Severity::Low
),
(
Regex::new(r"0x[0-9a-fA-F]{40,}").unwrap(),
"Long hex string (potential obfuscation)",
Severity::Low
),
];
}
pub struct PatternScanner;
impl Default for PatternScanner {
fn default() -> Self {
Self::new()
}
}
impl PatternScanner {
pub fn new() -> Self {
Self
}
}
#[async_trait]
impl SecurityPlugin for PatternScanner {
fn name(&self) -> &str {
"patterns"
}
fn version(&self) -> &str {
"0.1.0"
}
fn description(&self) -> &str {
"Pattern-based threat detection using regex"
}
fn scan_phase(&self) -> ScanPhase {
ScanPhase::All
}
async fn initialize(&mut self) -> Result<(), PluginError> {
Ok(())
}
async fn scan(&self, context: &ScanContext<'_>) -> Result<PluginReport, PluginError> {
let start = Instant::now();
let mut report = PluginReport::new(self.name().to_string());
if let Some(content) = context.file_content {
let content_str = String::from_utf8_lossy(content);
for (pattern, description, severity) in PATTERNS.iter() {
for (line_num, line) in content_str.lines().enumerate() {
if pattern.is_match(line) {
let finding = Finding::new(
format!("PAT-{:03}", line_num),
description.to_string(),
*severity,
)
.with_file(context.path.to_path_buf())
.with_line((line_num + 1) as u32)
.with_evidence(line.to_string());
report.findings.push(finding);
}
}
}
report.scanned_files = 1;
}
report.duration_ms = start.elapsed().as_millis() as u64;
Ok(report)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::plugins::traits::ScanContext;
#[tokio::test]
async fn test_eval_detection() {
let scanner = PatternScanner::new();
let content = b"eval(user_input)";
let context = ScanContext {
path: std::path::Path::new("test.txt"),
scan_phase: ScanPhase::PostExtract,
file_content: Some(content),
metadata: std::collections::HashMap::new(),
};
let report = scanner.scan(&context).await.unwrap();
assert!(!report.findings.is_empty());
}
#[tokio::test]
async fn test_hardcoded_password() {
let scanner = PatternScanner::new();
let content = br#"password = "s3cret""#;
let context = ScanContext {
path: std::path::Path::new("test.txt"),
scan_phase: ScanPhase::PostExtract,
file_content: Some(content),
metadata: std::collections::HashMap::new(),
};
let report = scanner.scan(&context).await.unwrap();
assert!(!report.findings.is_empty());
}
#[tokio::test]
async fn test_reverse_shell() {
let scanner = PatternScanner::new();
let content = b"reverse_shell";
let context = ScanContext {
path: std::path::Path::new("test.txt"),
scan_phase: ScanPhase::PostExtract,
file_content: Some(content),
metadata: std::collections::HashMap::new(),
};
let report = scanner.scan(&context).await.unwrap();
assert!(!report.findings.is_empty());
}
#[tokio::test]
async fn test_clean_code() {
let scanner = PatternScanner::new();
let content = b"let x = 42;";
let context = ScanContext {
path: std::path::Path::new("test.txt"),
scan_phase: ScanPhase::PostExtract,
file_content: Some(content),
metadata: std::collections::HashMap::new(),
};
let report = scanner.scan(&context).await.unwrap();
assert_eq!(report.findings.len(), 0);
}
}