deslop 0.1.0

A static analyzer that spots low-context and AI-assisted code patterns across naming, concurrency, security, performance, and test quality.
Documentation
use crate::analysis::{ParsedFile, ParsedFunction};
use crate::model::{Finding, Severity};

use super::common::{identifier_tokens, import_alias_lookup};

const WEAK_CRYPTO_IMPORTS: &[&str] = &["crypto/md5", "crypto/sha1", "crypto/des", "crypto/rc4"];

pub(super) fn weak_crypto_findings(file: &ParsedFile, function: &ParsedFunction) -> Vec<Finding> {
    let import_aliases = import_alias_lookup(&file.imports);
    let mut findings = Vec::new();

    for call in &function.calls {
        let Some(receiver) = &call.receiver else {
            continue;
        };
        let Some(import_path) = import_aliases.get(receiver) else {
            continue;
        };
        if !WEAK_CRYPTO_IMPORTS.contains(&import_path.as_str()) {
            continue;
        }

        findings.push(Finding {
            rule_id: "weak_crypto".to_string(),
            severity: Severity::Warning,
            path: file.path.clone(),
            function_name: Some(function.fingerprint.name.clone()),
            start_line: call.line,
            end_line: call.line,
            message: format!(
                "function {} uses weak cryptographic primitive {}",
                function.fingerprint.name, import_path
            ),
            evidence: vec![
                format!("import alias {receiver} resolves to {import_path}"),
                format!("weak crypto call: {receiver}.{}", call.name),
            ],
        });
    }

    findings
}

pub(super) fn package_hardcoded_secret_findings(file: &ParsedFile) -> Vec<Finding> {
    file.package_string_literals
        .iter()
        .filter(|literal| is_secret_like_name(&literal.name) && looks_like_secret_value(&literal.value))
        .map(|literal| Finding {
            rule_id: "hardcoded_secret".to_string(),
            severity: Severity::Warning,
            path: file.path.clone(),
            function_name: None,
            start_line: literal.line,
            end_line: literal.line,
            message: format!(
                "file declares a hardcoded secret-like literal in {}",
                literal.name
            ),
            evidence: vec![
                format!("secret-like identifier: {}", literal.name),
                format!("literal length: {}", literal.value.len()),
            ],
        })
        .collect()
}

pub(super) fn hardcoded_secret_findings(file: &ParsedFile, function: &ParsedFunction) -> Vec<Finding> {
    function
        .local_string_literals
        .iter()
        .filter(|literal| is_secret_like_name(&literal.name) && looks_like_secret_value(&literal.value))
        .map(|literal| Finding {
            rule_id: "hardcoded_secret".to_string(),
            severity: Severity::Warning,
            path: file.path.clone(),
            function_name: Some(function.fingerprint.name.clone()),
            start_line: literal.line,
            end_line: literal.line,
            message: format!(
                "function {} assigns a hardcoded secret-like literal",
                function.fingerprint.name
            ),
            evidence: vec![
                format!("secret-like identifier: {}", literal.name),
                format!("literal length: {}", literal.value.len()),
            ],
        })
        .collect()
}

pub(super) fn sql_string_concat_findings(file: &ParsedFile, function: &ParsedFunction) -> Vec<Finding> {
    function
        .db_query_calls
        .iter()
        .filter(|query_call| query_call.query_uses_dynamic_construction)
        .map(|query_call| Finding {
            rule_id: "sql_string_concat".to_string(),
            severity: Severity::Warning,
            path: file.path.clone(),
            function_name: Some(function.fingerprint.name.clone()),
            start_line: query_call.line,
            end_line: query_call.line,
            message: format!(
                "function {} builds a SQL query dynamically before execution",
                function.fingerprint.name
            ),
            evidence: vec![
                format!("query method: {}", query_call.method_name),
                format!(
                    "query expression: {}",
                    query_call.query_argument_text.as_deref().unwrap_or("<unknown>")
                ),
            ],
        })
        .collect()
}

fn is_secret_like_name(name: &str) -> bool {
    let tokens = identifier_tokens(name);
    if tokens.is_empty() {
        return false;
    }

    let joined = tokens.join("_");
    matches!(
        joined.as_str(),
        "password"
            | "passwd"
            | "pwd"
            | "secret"
            | "client_secret"
            | "api_key"
            | "apikey"
            | "private_key"
            | "access_token"
            | "refresh_token"
            | "auth_token"
    ) || tokens.contains(&"password".to_string())
        || tokens.contains(&"secret".to_string())
        || tokens.contains(&"apikey".to_string())
        || tokens.contains(&"private".to_string()) && tokens.contains(&"key".to_string())
        || tokens.contains(&"token".to_string())
            && tokens.iter().any(|token| matches!(token.as_str(), "api" | "auth" | "access" | "refresh" | "client"))
}

fn looks_like_secret_value(value: &str) -> bool {
    let trimmed = value.trim();
    let normalized = trimmed.to_ascii_lowercase();

    trimmed.len() >= 8
        && !matches!(normalized.as_str(), "placeholder" | "example" | "sample" | "changeme" | "your-api-key" | "your-secret")
        && normalized != "bearer"
}