codesearch 0.1.15

A fast, intelligent CLI tool with multiple search modes (regex, fuzzy, semantic), code analysis, and dead code detection for popular programming languages
Documentation
//! Basic security pattern detection
//!
//! Lightweight checks for common dangerous patterns—like CodeQL/Joern queries
//! but instant, no indexing, no query language. Beats them on setup time.
//!
//! Performance note: Uses pre-compiled regex::RegexSet for simultaneous
//! matching of all patterns against each line. Patterns are compiled once
//! at startup via lazy_static, avoiding repeated compilation in hot loops.

use crate::search::list_files;
use regex::RegexSet;
use serde::Serialize;
use std::path::Path;

/// Pre-compiled security patterns and their metadata.
/// Using RegexSet allows matching all patterns against a line in a single pass.
struct SecurityPatternSet {
    set: RegexSet,
    kinds: Vec<SecurityKind>,
    severities: Vec<Severity>,
}

lazy_static::lazy_static! {
    static ref SECURITY_PATTERNS: SecurityPatternSet = {
        let raw_patterns: Vec<(&str, SecurityKind, Severity)> = vec![
            (
                r"(?i)eval\s*\(\s*(?:req|request|input|params|query|body|user)",
                SecurityKind::EvalWithInput,
                Severity::High,
            ),
            (
                r"(?i)(?:exec|system|popen|shell_exec|passthru)\s*\(\s*(?:req|request|input|params|query|\$_)",
                SecurityKind::ExecWithInput,
                Severity::High,
            ),
            (
                r"(?i)(?:execute|query|prepare)\s*\(\s*[^?]*\+|SELECT.*\+.*(?:req|request|input|params)",
                SecurityKind::SqlConcat,
                Severity::High,
            ),
            (
                r"(?i)(?:os\.system|subprocess\.call|exec)\s*\([^)]*(?:input|request|argv)",
                SecurityKind::CommandInjection,
                Severity::High,
            ),
            (
                r"(?i)(?:pickle\.loads|yaml\.load\s*\(|unserialize)\s*\([^)]*\)",
                SecurityKind::UnsafeDeserialize,
                Severity::Medium,
            ),
            (
                r"(?i)innerHTML\s*=|document\.write\s*\(|dangerouslySetInnerHTML",
                SecurityKind::XssSink,
                Severity::Medium,
            ),
            (
                r#"(?i)(?:password|api_key|secret)\s*=\s*['"][^'"]{8,}['"]"#,
                SecurityKind::HardcodedSecret,
                Severity::Low,
            ),
        ];

        let patterns: Vec<&str> = raw_patterns.iter().map(|(p, _, _)| *p).collect();
        let set = RegexSet::new(&patterns).expect("Security patterns must be valid regex");

        SecurityPatternSet {
            set,
            kinds: raw_patterns.iter().map(|(_, k, _)| k.clone()).collect(),
            severities: raw_patterns.iter().map(|(_, _, s)| s.clone()).collect(),
        }
    };
}

/// A suspected security issue
#[derive(Debug, Clone, Serialize)]
pub struct SecurityFinding {
    pub kind: SecurityKind,
    pub file: String,
    pub line: usize,
    pub content: String,
    pub severity: Severity,
}

#[derive(Debug, Clone, Serialize, PartialEq)]
pub enum SecurityKind {
    EvalWithInput,
    ExecWithInput,
    SqlConcat,
    CommandInjection,
    HardcodedSecret,
    UnsafeDeserialize,
    XssSink,
}

#[derive(Debug, Clone, Serialize, PartialEq)]
pub enum Severity {
    High,
    Medium,
    Low,
}

/// Scan for dangerous security patterns. Returns findings.
pub fn scan_security_patterns(
    path: &Path,
    extensions: Option<&[String]>,
    exclude: Option<&[String]>,
) -> Result<Vec<SecurityFinding>, Box<dyn std::error::Error>> {
    let files = list_files(path, extensions, exclude)?;
    let mut findings = Vec::new();

    for file in &files {
        let content = match std::fs::read_to_string(&file.path) {
            Ok(c) => c,
            Err(_) => continue,
        };

        for (line_num, line) in content.lines().enumerate() {
            let trimmed = line.trim();
            if trimmed.is_empty() || trimmed.starts_with("//") || trimmed.starts_with("#") {
                continue;
            }

            let matches = SECURITY_PATTERNS.set.matches(trimmed);
            for idx in matches.iter() {
                findings.push(SecurityFinding {
                    kind: SECURITY_PATTERNS.kinds[idx].clone(),
                    file: file.path.clone(),
                    line: line_num + 1,
                    content: trimmed.to_string(),
                    severity: SECURITY_PATTERNS.severities[idx].clone(),
                });
            }
        }
    }

    Ok(findings)
}

/// Print security findings to stdout
pub fn print_security_report(findings: &[SecurityFinding]) {
    use colored::*;

    if findings.is_empty() {
        println!("{}", "No security issues found.".green());
        return;
    }

    println!("{}", "Security Scan".cyan().bold());
    println!("{}", "".repeat(50).cyan());
    println!();

    let high: Vec<_> = findings
        .iter()
        .filter(|f| f.severity == Severity::High)
        .collect();
    let medium: Vec<_> = findings
        .iter()
        .filter(|f| f.severity == Severity::Medium)
        .collect();
    let low: Vec<_> = findings
        .iter()
        .filter(|f| f.severity == Severity::Low)
        .collect();

    if !high.is_empty() {
        println!("{}", "HIGH".red().bold());
        for f in &high {
            println!(
                "  {} {}:{} {}",
                "".red(),
                f.file.cyan(),
                f.line.to_string().yellow(),
                f.content
            );
        }
        println!();
    }
    if !medium.is_empty() {
        println!("{}", "MEDIUM".yellow().bold());
        for f in &medium {
            println!(
                "  {} {}:{} {}",
                "".yellow(),
                f.file.cyan(),
                f.line.to_string().yellow(),
                f.content
            );
        }
        println!();
    }
    if !low.is_empty() {
        println!("{}", "LOW".dimmed().bold());
        for f in &low {
            println!(
                "  {} {}:{} {}",
                "·".dimmed(),
                f.file.cyan(),
                f.line.to_string().yellow(),
                f.content
            );
        }
    }

    println!();
    println!("{} {} total", "".dimmed(), findings.len());
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_scan_eval_pattern() {
        let dir = tempfile::tempdir().unwrap();
        let f = dir.path().join("bad.js");
        std::fs::write(&f, "eval(req.body.input);").unwrap();
        let findings = scan_security_patterns(dir.path(), Some(&["js".to_string()]), None).unwrap();
        assert!(!findings.is_empty());
        assert!(
            findings
                .iter()
                .any(|f| f.kind == SecurityKind::EvalWithInput)
        );
    }
}