lonkero 3.6.2

Web scanner built for actual pentests. Fast, modular, Rust.
Documentation
// Copyright (c) 2026 Bountyy Oy. All rights reserved.
// This software is proprietary and confidential.

use crate::http_client::HttpResponse;
use crate::types::{Confidence, Severity, Vulnerability};
use chrono::Utc;

pub struct VulnerabilityDetector;

impl VulnerabilityDetector {
    pub fn new() -> Self {
        Self
    }

    /// Detect XSS vulnerability
    pub fn detect_xss(
        &self,
        url: &str,
        parameter: &str,
        payload: &str,
        response: &HttpResponse,
    ) -> Option<Vulnerability> {
        // Check if payload is reflected in response
        if !response.contains(payload) {
            return None;
        }

        // Check for script execution indicators
        let has_script_tag = response.contains("<script") || response.contains("</script>");
        let has_event_handler = response.contains("onerror=") || response.contains("onload=");
        let has_javascript_protocol = response.contains("javascript:");

        let (severity, confidence) =
            if has_script_tag || has_event_handler || has_javascript_protocol {
                (Severity::High, Confidence::High)
            } else {
                (Severity::Medium, Confidence::Medium)
            };

        Some(Vulnerability {
            id: format!("xss_{}", uuid::Uuid::new_v4().to_string()),
            vuln_type: "Cross-Site Scripting (XSS)".to_string(),
            severity,
            confidence,
            category: "Injection".to_string(),
            url: url.to_string(),
            parameter: Some(parameter.to_string()),
            payload: payload.to_string(),
            description: format!(
                "Reflected XSS vulnerability detected in parameter '{}'. The application reflects user input without proper sanitization.",
                parameter
            ),
            evidence: Some(format!(
                "Payload '{}' was reflected in the response",
                payload
            )),
            cwe: "CWE-79".to_string(),
            cvss: 7.5,
            verified: true,
            false_positive: false,
            remediation: "1. Implement output encoding/escaping for all user input\n2. Use Content Security Policy (CSP) headers\n3. Enable X-XSS-Protection header\n4. Validate and sanitize all input server-side".to_string(),
            discovered_at: Utc::now().to_rfc3339(),
            ml_data: None,
        })
    }

    /// Detect SQL injection vulnerability
    pub fn detect_sqli(
        &self,
        url: &str,
        parameter: &str,
        payload: &str,
        response: &HttpResponse,
        baseline_response: &HttpResponse,
    ) -> Option<Vulnerability> {
        // SQL error patterns that indicate actual SQL injection (case-insensitive)
        let sql_errors = vec![
            "sql syntax",
            "mysql_fetch",
            "ora-",
            "postgresql",
            "microsoft sql server",
            "sqlite",
            "syntax error",
            "warning: mysql",
            "pg_query",
            "mysqli",
            "sqlstate",
            "sql server",
            "oledbexception",
            "sqlexception",
            "pdoexception",
            "unclosed quotation mark",
            "quoted string not properly terminated",
            "unterminated quoted string",
            // Additional common SQL error patterns
            "you have an error in your sql",
            "mysql error",
            "database error",
            "query failed",
            "sql error",
            "invalid query",
            "unexpected end of sql",
            "near \"",       // SQLite style error
            "at line ",      // MySQL line error
            "column count",  // UNION column mismatch
            "unknown column",
            "table doesn't exist",
            "no such table",
            "incorrect syntax near",
            "division by zero",
        ];

        // Convert responses to lowercase for case-insensitive matching
        let response_lower = response.body.to_lowercase();
        let baseline_lower = baseline_response.body.to_lowercase();

        // Check for SQL errors that are NEW (not in baseline)
        // This prevents false positives from pages that normally show SQL-related text
        let has_new_sql_error = sql_errors.iter().any(|pattern| {
            response_lower.contains(pattern) && !baseline_lower.contains(pattern)
        });

        // CRITICAL: Response size change alone is NOT evidence of SQL injection!
        // A size change could happen for many legitimate reasons (dynamic content,
        // different query params returning different results, etc.)
        // We ONLY report SQLi if we have actual SQL error patterns in the response
        // that were NOT present in the baseline

        if !has_new_sql_error {
            return None;
        }

        // Find which specific error was detected for evidence
        let detected_error = sql_errors
            .iter()
            .find(|pattern| {
                response_lower.contains(*pattern) && !baseline_lower.contains(*pattern)
            })
            .map(|s| s.to_string())
            .unwrap_or_else(|| "SQL error pattern".to_string());

        Some(Vulnerability {
            id: format!("sqli_{}", uuid::Uuid::new_v4().to_string()),
            vuln_type: "SQL Injection".to_string(),
            severity: Severity::Critical,
            confidence: Confidence::High,
            category: "Injection".to_string(),
            url: url.to_string(),
            parameter: Some(parameter.to_string()),
            payload: payload.to_string(),
            description: format!(
                "SQL injection vulnerability detected in parameter '{}'. The application does not properly sanitize database queries.",
                parameter
            ),
            evidence: Some(format!("SQL error detected in response: '{}'", detected_error)),
            cwe: "CWE-89".to_string(),
            cvss: 9.8,
            verified: true,
            false_positive: false,
            remediation: "1. Use parameterized queries/prepared statements\n2. Implement input validation and sanitization\n3. Apply principle of least privilege for database accounts\n4. Use an ORM framework".to_string(),
            discovered_at: Utc::now().to_rfc3339(),
            ml_data: None,
        })
    }

    /// Detect command injection vulnerability
    pub fn detect_command_injection(
        &self,
        url: &str,
        parameter: &str,
        payload: &str,
        response: &HttpResponse,
    ) -> Option<Vulnerability> {
        // Command execution indicators
        let command_indicators = vec![
            "root:",
            "daemon:",
            "/bin/",
            "/usr/bin/",
            "uid=",
            "gid=",
            "groups=",
        ];

        let has_command_output = command_indicators
            .iter()
            .any(|pattern| response.body.contains(pattern));

        if !has_command_output {
            return None;
        }

        Some(Vulnerability {
            id: format!("cmdi_{}", uuid::Uuid::new_v4().to_string()),
            vuln_type: "Command Injection".to_string(),
            severity: Severity::Critical,
            confidence: Confidence::High,
            category: "Injection".to_string(),
            url: url.to_string(),
            parameter: Some(parameter.to_string()),
            payload: payload.to_string(),
            description: format!(
                "Command injection vulnerability detected in parameter '{}'. The application allows execution of arbitrary system commands.",
                parameter
            ),
            evidence: Some("System command output detected in response".to_string()),
            cwe: "CWE-78".to_string(),
            cvss: 9.8,
            verified: true,
            false_positive: false,
            remediation: "1. Avoid executing system commands with user input\n2. Use parameterized APIs when possible\n3. Implement strict input validation\n4. Use allowlists for permitted commands".to_string(),
            discovered_at: Utc::now().to_rfc3339(),
            ml_data: None,
        })
    }
}

// Add uuid crate for generating IDs
mod uuid {
    use rand::Rng;

    pub struct Uuid;

    impl Uuid {
        pub fn new_v4() -> Self {
            Self
        }

        pub fn to_string(&self) -> String {
            let mut rng = rand::rng();
            format!(
                "{:08x}-{:04x}-{:04x}-{:04x}-{:012x}",
                rng.random::<u32>(),
                rng.random::<u16>(),
                rng.random::<u16>(),
                rng.random::<u16>(),
                rng.random::<u64>() & 0xffffffffffff
            )
        }
    }
}