use crate::signing::ReportSignature;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum ScanMode {
Fast,
Normal,
Thorough,
Insane,
Intelligent,
}
impl Default for ScanMode {
fn default() -> Self {
ScanMode::Intelligent
}
}
impl std::fmt::Display for ScanMode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ScanMode::Fast => write!(f, "fast"),
ScanMode::Normal => write!(f, "normal"),
ScanMode::Thorough => write!(f, "thorough"),
ScanMode::Insane => write!(f, "insane"),
ScanMode::Intelligent => write!(f, "intelligent"),
}
}
}
impl ScanMode {
pub fn as_str(&self) -> &'static str {
match self {
ScanMode::Fast => "fast",
ScanMode::Normal => "normal",
ScanMode::Thorough => "thorough",
ScanMode::Insane => "insane",
ScanMode::Intelligent => "intelligent",
}
}
pub fn is_intelligent(&self) -> bool {
matches!(self, ScanMode::Intelligent)
}
pub fn is_legacy(&self) -> bool {
!self.is_intelligent()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ScanJob {
pub scan_id: String,
pub target: String,
pub config: ScanConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ScanConfig {
#[serde(default)]
pub scan_mode: ScanMode,
#[serde(default)]
pub enable_crawler: bool,
#[serde(default = "default_max_depth")]
pub max_depth: u32,
#[serde(default = "default_max_pages")]
pub max_pages: u32,
#[serde(default)]
pub enum_subdomains: bool,
#[serde(default)]
pub auth_cookie: Option<String>,
#[serde(default)]
pub auth_token: Option<String>,
#[serde(default)]
pub auth_basic: Option<String>,
#[serde(default)]
pub custom_headers: Option<HashMap<String, String>>,
#[serde(default)]
pub only_modules: Vec<String>,
#[serde(default)]
pub skip_modules: Vec<String>,
}
impl ScanConfig {
pub fn should_run_module(&self, module_id: &str) -> bool {
if !self.only_modules.is_empty() {
if !self.only_modules.iter().any(|m| m == module_id) {
return false;
}
}
if self.skip_modules.iter().any(|m| m == module_id) {
return false;
}
true
}
pub fn should_run_any_module(&self, module_ids: &[&str]) -> bool {
if self.only_modules.is_empty() {
return module_ids.iter().any(|id| !self.skip_modules.contains(&id.to_string()));
}
module_ids.iter().any(|id| self.should_run_module(id))
}
}
fn default_max_depth() -> u32 {
3
}
fn default_max_pages() -> u32 {
1000
}
impl Default for ScanConfig {
fn default() -> Self {
Self {
scan_mode: ScanMode::Fast,
enable_crawler: false,
max_depth: 3,
max_pages: 1000,
enum_subdomains: false,
auth_cookie: None,
auth_token: None,
auth_basic: None,
custom_headers: None,
only_modules: Vec::new(),
skip_modules: Vec::new(),
}
}
}
impl ScanConfig {
pub fn payload_count(&self) -> usize {
match self.scan_mode {
ScanMode::Fast => 50,
ScanMode::Normal => 500,
ScanMode::Thorough => 5000,
ScanMode::Insane => usize::MAX, ScanMode::Intelligent => 0,
}
}
pub fn enable_cloud_scanning(&self) -> bool {
matches!(
self.scan_mode,
ScanMode::Thorough | ScanMode::Insane | ScanMode::Intelligent
)
}
pub fn subdomain_extended(&self) -> bool {
matches!(
self.scan_mode,
ScanMode::Thorough | ScanMode::Insane | ScanMode::Intelligent
)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ScanResults {
pub scan_id: String,
pub target: String,
pub tests_run: u64,
pub vulnerabilities: Vec<Vulnerability>,
pub started_at: String,
pub completed_at: String,
pub duration_seconds: f64,
#[serde(default)]
pub early_terminated: bool,
#[serde(default)]
pub termination_reason: Option<String>,
#[serde(default)]
pub scanner_version: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub license_signature: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub quantum_signature: Option<ReportSignature>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub authorization_token_id: Option<String>,
}
#[derive(Debug, Clone)]
pub struct MlResponseData {
pub features: crate::ml::VulnFeatures,
pub payload_category: Option<String>,
}
#[derive(Debug, Clone)]
pub struct MlHttpResponse {
pub status_code: u16,
pub body_length: usize,
pub duration_ms: u64,
pub content_type: Option<String>,
}
impl MlHttpResponse {
pub fn from_http_response(resp: &crate::http_client::HttpResponse) -> Self {
Self {
status_code: resp.status_code,
body_length: resp.body.len(),
duration_ms: resp.duration_ms,
content_type: resp.headers.get("content-type").cloned(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase", default)]
pub struct Vulnerability {
pub id: String,
#[serde(rename = "type")]
pub vuln_type: String,
#[serde(default)]
pub severity: Severity,
#[serde(default)]
pub confidence: Confidence,
pub category: String,
pub url: String,
pub parameter: Option<String>,
pub payload: String,
pub description: String,
pub evidence: Option<String>,
pub cwe: String,
pub cvss: f32,
pub verified: bool,
pub false_positive: bool,
pub remediation: String,
pub discovered_at: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub ml_confidence: Option<f64>,
#[serde(skip)]
pub ml_data: Option<MlResponseData>,
}
impl Vulnerability {
pub fn with_ml_data(
mut self,
response: &crate::http_client::HttpResponse,
baseline: Option<&crate::http_client::HttpResponse>,
payload: Option<&str>,
) -> Self {
let extractor = crate::ml::FeatureExtractor::new();
let features = extractor.extract(response, baseline, payload);
let payload_category = payload.map(|p| Self::categorize_payload(p));
self.ml_data = Some(MlResponseData {
features,
payload_category,
});
self
}
fn categorize_payload(payload: &str) -> String {
let p = payload.to_lowercase();
if p.contains("select") || p.contains("union") || p.contains("'--") {
"sqli".to_string()
} else if p.contains("<script") || p.contains("javascript:") || p.contains("onerror") {
"xss".to_string()
} else if p.contains("http://") || p.contains("https://") || p.contains("file://") {
"ssrf".to_string()
} else if p.contains(";") && (p.contains("ls") || p.contains("cat") || p.contains("id")) {
"cmdi".to_string()
} else if p.contains("../") || p.contains("..\\") {
"path_traversal".to_string()
} else if p.contains("sleep") || p.contains("waitfor") || p.contains("benchmark") {
"time_based".to_string()
} else {
"other".to_string()
}
}
pub fn has_ml_data(&self) -> bool {
self.ml_data.is_some()
}
pub fn get_ml_features(&self) -> Option<&crate::ml::VulnFeatures> {
self.ml_data.as_ref().map(|ml| &ml.features)
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum Severity {
Critical,
High,
#[default]
Medium,
Low,
Info,
}
impl std::fmt::Display for Severity {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Severity::Critical => write!(f, "CRITICAL"),
Severity::High => write!(f, "HIGH"),
Severity::Medium => write!(f, "MEDIUM"),
Severity::Low => write!(f, "LOW"),
Severity::Info => write!(f, "INFO"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum Confidence {
High,
#[default]
Medium,
Low,
}
impl std::fmt::Display for Confidence {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Confidence::High => write!(f, "HIGH"),
Confidence::Medium => write!(f, "MEDIUM"),
Confidence::Low => write!(f, "LOW"),
}
}
}
#[derive(Debug, Clone)]
pub struct ScanProgress {
pub scan_id: String,
pub progress: u8,
pub phase: String,
pub message: String,
}
impl Serialize for ScanProgress {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
use serde::ser::SerializeStruct;
let mut state = serializer.serialize_struct("ScanProgress", 4)?;
state.serialize_field("scanId", &self.scan_id)?;
state.serialize_field("progress", &self.progress)?;
state.serialize_field("phase", &self.phase)?;
state.serialize_field("message", &self.message)?;
state.end()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum ParameterSource {
HtmlForm,
UrlQueryString,
JavaScriptMined,
ApiEndpoint,
GraphQL,
RequestHeader,
Cookie,
Unknown,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum EndpointType {
FormSubmission,
RestApi,
GraphQlApi,
JsonRpc,
StaticContent,
Unknown,
}
#[derive(Debug, Clone)]
pub struct ScanContext {
pub parameter_source: ParameterSource,
pub endpoint_type: EndpointType,
pub detected_tech: Vec<String>,
pub framework: Option<String>,
pub server: Option<String>,
pub other_parameters: Vec<String>,
pub is_json_api: bool,
pub is_graphql: bool,
pub form_fields: Vec<String>,
pub content_type: Option<String>,
}
impl Default for ScanContext {
fn default() -> Self {
Self {
parameter_source: ParameterSource::Unknown,
endpoint_type: EndpointType::Unknown,
detected_tech: Vec::new(),
framework: None,
server: None,
other_parameters: Vec::new(),
is_json_api: false,
is_graphql: false,
form_fields: Vec::new(),
content_type: None,
}
}
}
impl ScanContext {
pub fn new() -> Self {
Self::default()
}
pub fn has_tech(&self, tech: &str) -> bool {
self.detected_tech
.iter()
.any(|t| t.to_lowercase().contains(&tech.to_lowercase()))
}
pub fn is_framework(&self, name: &str) -> bool {
self.framework
.as_ref()
.map(|f| f.to_lowercase().contains(&name.to_lowercase()))
.unwrap_or(false)
}
}