use anyhow::Result;
use clap::{Parser, Subcommand, ValueEnum};
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Instant;
use tracing::{debug, error, info, warn, Level};
use std::collections::HashSet;
use lonkero_scanner::config::ScannerConfig;
use lonkero_scanner::crawler::CrawlResults;
use lonkero_scanner::detection_helpers::detect_technology;
use lonkero_scanner::http_client::HttpClient;
use lonkero_scanner::license::{self, LicenseStatus, LicenseType};
use lonkero_scanner::modules::ids as module_ids;
use lonkero_scanner::scanners::{
IntelligentScanOrchestrator, IntelligentScanPlan, PayloadIntensity, ScanEngine, TechCategory,
};
use lonkero_scanner::signing::{self, ScanToken, SigningError};
use lonkero_scanner::types::{ScanConfig, ScanJob, ScanMode, ScanResults};
use lonkero_scanner::reporting::deduplication::VulnerabilityDeduplicator;
use lonkero_scanner::analysis::{AttackPlanner, IntelligenceBus, ResponseAnalyzer, StateUpdate};
#[derive(Parser)]
#[command(name = "lonkero")]
#[command(author = "Bountyy Oy <info@bountyy.fi>")]
#[command(version = "3.5.0")]
#[command(about = "Web scanner built for actual pentests. Fast, modular, Rust.", long_about = None)]
#[command(propagate_version = true)]
struct Cli {
#[command(subcommand)]
command: Commands,
#[arg(short, long, global = true)]
verbose: bool,
#[arg(short, long, global = true)]
debug: bool,
#[arg(short, long, global = true)]
quiet: bool,
#[arg(short, long, global = true)]
config: Option<PathBuf>,
#[arg(short = 'L', long, global = true, env = "LONKERO_LICENSE_KEY")]
license_key: Option<String>,
}
#[derive(Subcommand)]
enum Commands {
Scan {
#[arg(required = true)]
targets: Vec<String>,
#[arg(short, long, default_value = "intelligent")]
mode: ScanModeArg,
#[arg(short, long)]
output: Option<PathBuf>,
#[arg(short, long, default_value = "json")]
format: OutputFormat,
#[arg(long)]
subdomains: bool,
#[arg(long)]
dorks: bool,
#[arg(long, default_value = "true")]
crawl: bool,
#[arg(long, default_value = "3")]
max_depth: u32,
#[arg(long, default_value = "50")]
concurrency: usize,
#[arg(long, default_value = "30")]
timeout: u64,
#[arg(long)]
user_agent: Option<String>,
#[arg(long)]
cookie: Option<String>,
#[arg(long)]
token: Option<String>,
#[arg(long)]
basic_auth: Option<String>,
#[arg(long)]
auth_username: Option<String>,
#[arg(long)]
auth_password: Option<String>,
#[arg(long)]
auth_login_url: Option<String>,
#[arg(short = 'H', long)]
header: Vec<String>,
#[arg(long)]
skip: Vec<String>,
#[arg(long)]
only: Vec<String>,
#[arg(long)]
proxy: Option<String>,
#[arg(long)]
insecure: bool,
#[arg(long, default_value = "100")]
rate_limit: u32,
#[arg(long)]
no_rate_limit: bool,
#[arg(long)]
admin_username: Option<String>,
#[arg(long)]
admin_password: Option<String>,
#[arg(long)]
admin_login_url: Option<String>,
#[arg(long)]
multi_role: bool,
#[arg(long)]
record_session: bool,
#[arg(long)]
session_output: Option<PathBuf>,
#[arg(long, default_value = "har")]
session_format: SessionRecordingFormat,
#[arg(long, default_value = "auto")]
payload_intensity: PayloadIntensityArg,
},
List {
#[arg(short, long)]
verbose: bool,
#[arg(short, long)]
category: Option<String>,
},
Validate {
targets: Vec<String>,
},
Init {
#[arg(short, long, default_value = "lonkero.toml")]
output: PathBuf,
},
Version,
License {
#[command(subcommand)]
action: LicenseAction,
},
Ml {
#[command(subcommand)]
action: MlAction,
},
}
#[derive(Subcommand)]
enum LicenseAction {
Activate {
key: String,
},
Status,
Deactivate,
}
#[derive(Subcommand)]
enum MlAction {
Enable {
#[arg(long)]
federated: bool,
},
Disable {
#[arg(long)]
delete_data: bool,
},
Stats,
Export {
#[arg(short, long, default_value = "ml_data_export.json")]
output: PathBuf,
},
DeleteData,
Sync,
}
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
enum ScanModeArg {
Intelligent,
Fast,
Normal,
Thorough,
Insane,
}
impl From<ScanModeArg> for ScanMode {
fn from(mode: ScanModeArg) -> Self {
match mode {
ScanModeArg::Intelligent => ScanMode::Intelligent,
ScanModeArg::Fast => ScanMode::Fast,
ScanModeArg::Normal => ScanMode::Normal,
ScanModeArg::Thorough => ScanMode::Thorough,
ScanModeArg::Insane => ScanMode::Insane,
}
}
}
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
enum OutputFormat {
Json,
Html,
Pdf,
Sarif,
Markdown,
Csv,
Xlsx,
Junit,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
enum SessionRecordingFormat {
Har,
Json,
Html,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Default)]
enum PayloadIntensityArg {
#[default]
Auto,
Minimal,
Standard,
Extended,
Maximum,
}
impl PayloadIntensityArg {
fn to_intensity(self) -> Option<PayloadIntensity> {
match self {
PayloadIntensityArg::Auto => None,
PayloadIntensityArg::Minimal => Some(PayloadIntensity::Minimal),
PayloadIntensityArg::Standard => Some(PayloadIntensity::Standard),
PayloadIntensityArg::Extended => Some(PayloadIntensity::Extended),
PayloadIntensityArg::Maximum => Some(PayloadIntensity::Maximum),
}
}
}
fn main() -> Result<()> {
let cli = Cli::parse();
let log_level = if cli.debug {
Level::DEBUG
} else if cli.verbose {
Level::INFO
} else if cli.quiet {
Level::ERROR
} else {
Level::INFO
};
use tracing_subscriber::EnvFilter;
let filter = EnvFilter::new(format!(
"{},html5ever=off,html5ever::tree_builder=off,selectors=off,scraper=off,markup5ever=off,headless_chrome=warn",
log_level
));
let _ = tracing_log::LogTracer::builder()
.with_max_level(tracing_log::log::LevelFilter::Error)
.init();
let _ = tracing_subscriber::fmt()
.with_env_filter(filter)
.with_target(false)
.with_thread_ids(false)
.try_init();
let runtime = tokio::runtime::Builder::new_multi_thread()
.worker_threads(num_cpus::get())
.thread_name("lonkero-scanner")
.enable_all()
.build()?;
runtime.block_on(async_main(cli))
}
async fn async_main(cli: Cli) -> Result<()> {
match cli.command {
Commands::Scan {
targets,
mode,
output,
format,
subdomains,
dorks,
crawl,
max_depth,
concurrency,
timeout,
user_agent,
cookie,
token,
basic_auth,
auth_username,
auth_password,
auth_login_url,
header,
skip,
only,
proxy,
insecure,
rate_limit,
no_rate_limit,
admin_username,
admin_password,
admin_login_url,
multi_role,
record_session,
session_output,
session_format,
payload_intensity,
} => {
let requested_modules = determine_requested_modules(&skip, &only);
let (license_status, scan_token) = verify_license_before_scan(
cli.license_key.as_deref(),
targets.len(),
requested_modules,
)
.await?;
info!(
"[Auth] {} modules authorized by server",
scan_token.authorized_modules.len()
);
let denied = scan_token.get_denied_modules(&determine_requested_modules(&skip, &only));
if !denied.is_empty() {
warn!(
"[Auth] {} modules were not authorized: {:?}",
denied.len(),
if denied.len() <= 5 {
&denied[..]
} else {
&denied[..5]
}
);
}
run_scan(
targets,
mode.into(),
output,
format,
subdomains,
dorks,
crawl,
max_depth,
concurrency,
timeout,
user_agent,
cookie,
token,
basic_auth,
auth_username,
auth_password,
auth_login_url,
header,
skip,
only,
proxy,
insecure,
rate_limit,
no_rate_limit,
license_status,
scan_token,
admin_username,
admin_password,
admin_login_url,
multi_role,
record_session,
session_output,
session_format,
payload_intensity.to_intensity(),
)
.await
}
Commands::List { verbose, category } => list_scanners(verbose, category),
Commands::Validate { targets } => validate_targets(targets).await,
Commands::Init { output } => generate_config(output),
Commands::Version => show_version(),
Commands::License { action } => {
handle_license_command(action, cli.license_key.as_deref()).await
}
Commands::Ml { action } => handle_ml_command(action).await,
}
}
fn determine_requested_modules(skip: &[String], only: &[String]) -> Vec<String> {
let all_modules = module_ids::get_all_module_ids();
let result: Vec<String> = if !only.is_empty() {
let only_set: HashSet<&str> = only.iter().map(|s| s.as_str()).collect();
all_modules
.into_iter()
.filter(|m| only_set.contains(*m))
.map(|s| s.to_string())
.collect()
} else if !skip.is_empty() {
let skip_set: HashSet<&str> = skip.iter().map(|s| s.as_str()).collect();
all_modules
.into_iter()
.filter(|m| !skip_set.contains(*m))
.map(|s| s.to_string())
.collect()
} else {
all_modules.into_iter().map(|s| s.to_string()).collect()
};
if result.is_empty() && !only.is_empty() {
warn!("[Auth] No modules matched --only filter {:?}. Only FREE tier modules will be authorized.", only);
}
result
}
async fn verify_license_before_scan(
license_key: Option<&str>,
target_count: usize,
requested_modules: Vec<String>,
) -> Result<(LicenseStatus, ScanToken)> {
let is_commercial = std::env::var("CI").is_ok()
|| std::env::var("GITHUB_ACTIONS").is_ok()
|| std::env::var("GITLAB_CI").is_ok()
|| std::env::var("JENKINS_URL").is_ok();
let status =
match license::verify_license_for_scan(license_key, target_count, is_commercial).await {
Ok(status) => {
license::print_license_info(&status);
status
}
Err(e) => {
error!("========================================================");
error!("LICENSE VERIFICATION FAILED");
error!("========================================================");
error!("");
error!("{}", e);
error!("");
error!("To obtain a license, visit: https://bountyy.fi");
error!("For support, contact: info@bountyy.fi");
error!("");
error!("========================================================");
return Err(e);
}
};
let hardware_id = signing::get_hardware_id();
info!(
"Authorizing scan with {} requested modules...",
requested_modules.len()
);
let scan_token = match signing::authorize_scan(
target_count as u32,
&hardware_id,
license_key,
Some(env!("CARGO_PKG_VERSION")),
requested_modules,
)
.await
{
Ok(token) => {
info!(
"[OK] Scan authorized: {} license, max {} targets, {} modules authorized",
token.license_type,
token.max_targets,
token.authorized_modules.len()
);
token
}
Err(SigningError::Banned(reason)) => {
error!("========================================================");
error!("ACCESS DENIED - USER BANNED");
error!("========================================================");
error!("");
error!("Reason: {}", reason);
error!("");
error!("If you believe this is an error, contact:");
error!(" info@bountyy.fi");
error!("");
error!("========================================================");
std::process::exit(1);
}
Err(SigningError::LicenseError(msg)) => {
error!("========================================================");
error!("LICENSE ERROR");
error!("========================================================");
error!("");
error!("{}", msg);
error!("");
error!("========================================================");
std::process::exit(1);
}
Err(SigningError::ServerUnreachable(msg)) => {
error!("========================================================");
error!("SERVER UNREACHABLE - SCAN BLOCKED");
error!("========================================================");
error!("");
error!("Cannot connect to authorization server: {}", msg);
error!("");
error!("Server connectivity is REQUIRED for scanning.");
error!("Please check your network connection and try again.");
error!("");
error!("========================================================");
std::process::exit(1);
}
Err(e) => {
error!("========================================================");
error!("AUTHORIZATION FAILED");
error!("========================================================");
error!("");
error!("{}", e);
error!("");
error!("========================================================");
std::process::exit(1);
}
};
Ok((status, scan_token))
}
async fn handle_license_command(action: LicenseAction, _current_key: Option<&str>) -> Result<()> {
let mut manager = license::LicenseManager::new()?;
match action {
LicenseAction::Activate { key } => {
println!("Activating license key...");
manager.set_license_key(key.clone());
let status = manager.validate().await?;
if status.valid {
manager.save_license(&key)?;
println!();
println!("License activated successfully!");
println!();
license::print_license_info(&status);
println!();
println!("License saved. You can now run scans without specifying the key.");
} else {
error!("License validation failed");
if let Some(msg) = status.message {
error!("{}", msg);
}
std::process::exit(1);
}
}
LicenseAction::Status => {
manager.load_license()?;
let status = manager.validate().await?;
println!();
println!("========================================================");
println!("LONKERO LICENSE STATUS");
println!("========================================================");
println!();
if status.valid {
if let Some(lt) = status.license_type {
match lt {
LicenseType::Personal => println!("License Type: Free Non-Commercial"),
_ => println!("License Type: {}", lt),
}
}
if let Some(ref licensee) = status.licensee {
println!("Licensed to: {}", licensee);
}
if let Some(ref org) = status.organization {
println!("Organization: {}", org);
}
if let Some(ref expires) = status.expires_at {
println!("Expires: {}", expires);
}
if let Some(max) = status.max_targets {
println!("Max targets: {}", max);
}
if !status.features.is_empty() {
println!("Features: {}", status.features.join(", "));
}
} else {
println!("Status: No valid license");
println!();
println!("Running in Personal/Non-Commercial mode.");
println!("For commercial use, obtain a license at:");
println!(" https://bountyy.fi/license");
}
println!();
println!("========================================================");
}
LicenseAction::Deactivate => {
let config_dir = dirs::config_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join("lonkero");
let license_file = config_dir.join("license.key");
let cache_file = config_dir.join(".license_cache");
if license_file.exists() {
std::fs::remove_file(&license_file)?;
println!("License key removed.");
}
if cache_file.exists() {
std::fs::remove_file(&cache_file)?;
}
println!("License deactivated successfully.");
println!("You can activate a new license with: lonkero license activate <KEY>");
}
}
Ok(())
}
async fn handle_ml_command(action: MlAction) -> Result<()> {
use lonkero_scanner::ml::{MlPipeline, PrivacyManager};
match action {
MlAction::Enable { federated } => {
println!();
println!("========================================================");
println!("LONKERO ML - GDPR CONSENT");
println!("========================================================");
println!();
println!("By enabling ML features, you consent to:");
println!();
println!(" 1. Local collection of anonymized vulnerability patterns");
println!(" - No URLs, hostnames, or IPs are stored");
println!(" - Only statistical features (response codes, timing, etc.)");
println!(" - Data stored in ~/.lonkero/training_data/");
println!();
if federated {
println!(" 2. Federated Learning (you opted in with --federated):");
println!(" - Model WEIGHTS are shared (not your actual data)");
println!(" - Differential privacy noise applied before sharing");
println!(" - Cannot reconstruct your findings from weights");
println!(" - Benefits: Better detection accuracy for everyone");
println!();
}
println!("You can withdraw consent at any time with: lonkero ml disable");
println!("You can delete all data with: lonkero ml delete-data");
println!();
let mut privacy = PrivacyManager::new()?;
privacy.record_consent(federated)?;
println!(
"[OK] ML features enabled{}",
if federated {
" with federated learning"
} else {
""
}
);
println!();
}
MlAction::Disable { delete_data } => {
let mut privacy = PrivacyManager::new()?;
if delete_data {
privacy.withdraw_consent()?;
println!("[OK] ML disabled and all data deleted (GDPR Article 17)");
} else {
println!("[OK] ML disabled (data retained for future use)");
println!(" To delete data: lonkero ml delete-data");
}
}
MlAction::Stats => {
println!();
println!("========================================================");
println!("LONKERO ML STATISTICS");
println!("========================================================");
println!();
match MlPipeline::new() {
Ok(pipeline) => {
let stats = pipeline.get_stats().await;
println!(
"Status: {}",
if stats.enabled { "Enabled" } else { "Disabled" }
);
println!();
println!("Training Data:");
println!(" True positives: {}", stats.total_confirmed);
println!(" False positives: {}", stats.total_rejected);
println!(" Pending: {}", stats.pending_learning);
println!(" Endpoint patterns: {}", stats.endpoint_patterns);
println!();
println!("Federated Learning:");
println!(
" Connected: {}",
if stats.federated_enabled { "Yes" } else { "No" }
);
if let Some(contributors) = stats.federated_contributors {
println!(" Contributors: {}", contributors);
}
println!(
" Can contribute: {}",
if stats.can_contribute {
"Yes (50+ examples)"
} else {
"No (need 50+ examples)"
}
);
}
Err(e) => {
println!("ML not initialized: {}", e);
println!();
println!("Enable with: lonkero ml enable");
}
}
println!();
println!("========================================================");
}
MlAction::Export { output } => {
println!("Exporting ML data (GDPR Article 15 - Right to access)...");
let privacy = PrivacyManager::new()?;
let export = privacy.export_personal_data()?;
let json = serde_json::to_string_pretty(&export)?;
std::fs::write(&output, json)?;
println!();
println!("[OK] Data exported to: {}", output.display());
println!(" Training examples: {}", export.training_examples.len());
println!(
" Pending contributions: {}",
export.pending_contributions
);
}
MlAction::DeleteData => {
println!();
println!("WARNING: This will permanently delete all ML training data.");
println!("This action cannot be undone.");
println!();
let privacy = PrivacyManager::new()?;
privacy.delete_all_data()?;
println!("[OK] All ML data deleted (GDPR Article 17 - Right to erasure)");
}
MlAction::Sync => {
println!("Syncing with federated network...");
let privacy = PrivacyManager::new()?;
if !privacy.is_federated_allowed() {
println!();
println!("Federated learning not enabled.");
println!("Enable with: lonkero ml enable --federated");
return Ok(());
}
match MlPipeline::new() {
Ok(mut pipeline) => {
pipeline.on_scan_complete().await?;
println!();
println!("[OK] Sync complete");
}
Err(e) => {
error!("Sync failed: {}", e);
}
}
}
}
Ok(())
}
async fn run_scan(
targets: Vec<String>,
mode: ScanMode,
output: Option<PathBuf>,
format: OutputFormat,
subdomains: bool,
dorks: bool,
crawl: bool,
max_depth: u32,
concurrency: usize,
timeout: u64,
user_agent: Option<String>,
cookie: Option<String>,
token: Option<String>,
basic_auth: Option<String>,
auth_username: Option<String>,
auth_password: Option<String>,
auth_login_url: Option<String>,
headers: Vec<String>,
_skip: Vec<String>,
_only: Vec<String>,
_proxy: Option<String>,
_insecure: bool,
rate_limit: u32,
no_rate_limit: bool,
license_status: LicenseStatus,
scan_token: ScanToken,
admin_username: Option<String>,
admin_password: Option<String>,
admin_login_url: Option<String>,
multi_role: bool,
record_session: bool,
session_output: Option<PathBuf>,
session_format: SessionRecordingFormat,
payload_intensity_override: Option<PayloadIntensity>,
) -> Result<()> {
if license_status.killswitch_active {
error!("========================================================");
error!("SCANNER DISABLED");
error!("========================================================");
error!("");
error!("This scanner has been remotely disabled.");
if let Some(reason) = &license_status.killswitch_reason {
error!("Reason: {}", reason);
}
error!("");
error!("If you believe this is an error, please contact:");
error!(" info@bountyy.fi");
error!("");
error!("========================================================");
std::process::exit(1);
}
if let Some(max_targets) = license_status.max_targets {
if targets.len() as u32 > max_targets {
error!("========================================================");
error!("LICENSE LIMIT EXCEEDED");
error!("========================================================");
error!("");
error!(
"Your license allows {} target(s), but you specified {}.",
max_targets,
targets.len()
);
error!("");
error!("To scan more targets, upgrade your license at:");
error!(" https://bountyy.fi");
error!("");
error!("========================================================");
std::process::exit(1);
}
}
print_banner();
info!("Initializing Lonkero Scanner v3.5.0");
info!("Scan mode: {:?}", mode);
info!("Targets: {}", targets.len());
let scanner_config = ScannerConfig {
max_concurrency: concurrency,
request_timeout_secs: timeout,
max_retries: 2,
rate_limit_rps: if no_rate_limit { 0 } else { rate_limit },
rate_limit_enabled: !no_rate_limit,
rate_limit_adaptive: !no_rate_limit,
http2_enabled: true,
http2_adaptive_window: true,
http2_max_concurrent_streams: 100,
pool_max_idle_per_host: 10,
cache_enabled: true,
cache_max_capacity: 10000,
cache_ttl_secs: 300,
dns_cache_enabled: true,
subdomain_enum_enabled: subdomains,
subdomain_enum_thorough: matches!(
mode,
ScanMode::Thorough | ScanMode::Insane | ScanMode::Intelligent
),
cdn_detection_enabled: true,
early_termination_enabled: false,
adaptive_concurrency_enabled: true,
initial_concurrency: 10,
max_concurrency_per_target: concurrency,
request_batching_enabled: false,
batch_size: 50,
..Default::default()
};
let mut custom_headers = std::collections::HashMap::new();
for h in headers {
if let Some((key, value)) = h.split_once(':') {
custom_headers.insert(key.trim().to_string(), value.trim().to_string());
}
}
if let Some(ua) = &user_agent {
custom_headers.insert("User-Agent".to_string(), ua.clone());
}
use lonkero_scanner::auth_context::{AuthSession, Authenticator, LoginCredentials};
let auth_session: Option<AuthSession> =
if let (Some(username), Some(password)) = (&auth_username, &auth_password) {
info!("[Auth] Auto-login enabled for user: {}", username);
let authenticator = Authenticator::new(timeout);
let mut creds = LoginCredentials::new(username, password);
if let Some(login_url) = &auth_login_url {
creds = creds.with_login_url(login_url);
}
let base_url = targets.first().map(|t| t.as_str()).unwrap_or("");
match authenticator.login(base_url, &creds).await {
Ok(session) => {
if session.is_authenticated {
info!(
"[Auth] Login successful - {} cookies, JWT: {}",
session.cookies.len(),
session.find_jwt().is_some()
);
Some(session)
} else {
warn!("[Auth] Login may have failed - proceeding without auth");
None
}
}
Err(e) => {
warn!("[Auth] Auto-login failed: {} - proceeding without auth", e);
None
}
}
} else if let Some(tok) = &token {
info!("[Auth] Using provided bearer token");
Some(Authenticator::from_token(tok, "bearer"))
} else if let Some(cook) = &cookie {
info!("[Auth] Using provided cookies");
Some(Authenticator::from_token(cook, "cookie"))
} else {
None
};
if let Some(ref session) = auth_session {
for (key, value) in session.auth_headers() {
custom_headers.insert(key, value);
}
info!("[Auth] Authentication context ready - scanning authenticated endpoints");
}
let _admin_session: Option<AuthSession> = if multi_role {
if let (Some(admin_user), Some(admin_pass)) = (&admin_username, &admin_password) {
info!("[MultiRole] Initializing admin session for authorization testing");
let authenticator = Authenticator::new(timeout);
let mut creds = LoginCredentials::new(admin_user, admin_pass);
if let Some(login_url) = admin_login_url.as_ref().or(auth_login_url.as_ref()) {
creds = creds.with_login_url(login_url);
}
let base_url = targets.first().map(|t| t.as_str()).unwrap_or("");
match authenticator.login(base_url, &creds).await {
Ok(session) => {
if session.is_authenticated {
info!(
"[MultiRole] Admin login successful - {} cookies",
session.cookies.len()
);
Some(session)
} else {
warn!(
"[MultiRole] Admin login may have failed - multi-role testing disabled"
);
None
}
}
Err(e) => {
warn!(
"[MultiRole] Admin auto-login failed: {} - multi-role testing disabled",
e
);
None
}
}
} else {
warn!("[MultiRole] --multi-role requires --admin-username and --admin-password");
None
}
} else {
None
};
let engine = Arc::new(ScanEngine::new(scanner_config.clone())?);
info!("[OK] Scan engine initialized with {} scanner modules", 60);
if dorks {
info!("");
info!("=== Google Dorking Reconnaissance ===");
info!(
"Generating Google dork queries for {} target(s)...",
targets.len()
);
info!("");
use lonkero_scanner::scanners::GoogleDorkingScanner;
for target in &targets {
let dork_results = engine.google_dorking_scanner.generate_dorks(target);
let output_text = GoogleDorkingScanner::format_dorks_for_display(&dork_results);
println!("{}", output_text);
if let Some(ref out_path) = output {
let dorks_filename = out_path.with_file_name(format!(
"{}_dorks.json",
out_path
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("scan")
));
let dorks_json = GoogleDorkingScanner::format_dorks_as_json(&dork_results);
if let Ok(json_str) = serde_json::to_string_pretty(&dorks_json) {
if let Err(e) = std::fs::write(&dorks_filename, json_str) {
warn!("Failed to write dorks file: {}", e);
} else {
info!("Google dorks saved to: {}", dorks_filename.display());
}
}
}
}
info!("");
info!("=== Google Dorking Complete ===");
info!("Copy the queries above and use them in Google Search manually.");
info!("Note: Automated Google searches violate Google's Terms of Service.");
info!("");
}
let start_time = Instant::now();
let mut all_results: Vec<ScanResults> = Vec::new();
let mut total_vulns = 0;
let mut total_tests = 0;
for (idx, target) in targets.iter().enumerate() {
info!("");
info!(
"=== Scanning target {}/{}: {} ===",
idx + 1,
targets.len(),
target
);
if let Err(e) = url::Url::parse(target) {
error!("Invalid target URL '{}': {}", target, e);
continue;
}
let scan_config = ScanConfig {
scan_mode: mode,
enable_crawler: crawl,
max_depth,
max_pages: 1000,
enum_subdomains: subdomains,
auth_cookie: cookie.clone(),
auth_token: token.clone(),
auth_basic: basic_auth.clone(),
custom_headers: if custom_headers.is_empty() {
None
} else {
Some(custom_headers.clone())
},
};
let job = Arc::new(ScanJob {
scan_id: format!("scan_{}", uuid::Uuid::new_v4()),
target: target.clone(),
config: scan_config,
});
match execute_standalone_scan(Arc::clone(&engine), job, &scanner_config, payload_intensity_override).await {
Ok(results) => {
let vuln_count = results.vulnerabilities.len();
total_vulns += vuln_count;
total_tests += results.tests_run;
print_vulnerability_summary(&results);
all_results.push(results);
}
Err(e) => {
error!("Scan failed for {}: {}", target, e);
}
}
}
if multi_role {
if let (
Some(ref user_name),
Some(ref user_pass),
Some(ref admin_name),
Some(ref admin_pass),
) = (
&auth_username,
&auth_password,
&admin_username,
&admin_password,
) {
use lonkero_scanner::multi_role::compare_user_admin;
info!("");
info!("=== Multi-Role Authorization Testing ===");
info!("[MultiRole] Comparing access patterns between user and admin roles");
let base_url = targets.first().map(|t| t.as_str()).unwrap_or("");
match compare_user_admin(
Arc::clone(&engine.http_client),
base_url,
(user_name.as_str(), user_pass.as_str()),
(admin_name.as_str(), admin_pass.as_str()),
)
.await
{
Ok(vulns) => {
if !vulns.is_empty() {
info!("[MultiRole] Found {} authorization issues:", vulns.len());
for vuln in &vulns {
warn!(" - {}: {} at {}", vuln.severity, vuln.vuln_type, vuln.url);
}
total_vulns += vulns.len();
if let Some(last_result) = all_results.last_mut() {
last_result.vulnerabilities.extend(vulns);
}
} else {
info!("[MultiRole] No authorization issues found between roles");
}
}
Err(e) => {
warn!("[MultiRole] Authorization analysis failed: {}", e);
}
}
info!("=== Multi-Role Testing Complete ===");
info!("");
} else {
warn!("[MultiRole] Multi-role testing requires both user (--auth-username, --auth-password) and admin (--admin-username, --admin-password) credentials");
}
}
if record_session {
info!("[Recording] Session recording was enabled - note: full integration pending");
let output_file = session_output.clone().unwrap_or_else(|| {
PathBuf::from(format!(
"session_recording.{}",
match session_format {
SessionRecordingFormat::Har => "har",
SessionRecordingFormat::Json => "json",
SessionRecordingFormat::Html => "html",
}
))
});
info!(
"[Recording] Session would be exported to: {} (format: {:?})",
output_file.display(),
session_format
);
info!("[Recording] Note: Full session recording integration is available via the SessionRecorder API");
}
let elapsed = start_time.elapsed();
{
use lonkero_scanner::ml::{MlPipeline, VulnFeatures};
match MlPipeline::new() {
Ok(mut ml_pipeline) => {
if ml_pipeline.is_enabled() {
info!(
"[ML] Processing {} vulnerabilities for auto-learning",
total_vulns
);
let mut ml_processed = 0;
let mut ml_with_scanner_data = 0;
let mut ml_auto_extracted = 0;
for result in &all_results {
for vuln in &result.vulnerabilities {
let features = if let Some(f) = vuln.get_ml_features() {
ml_with_scanner_data += 1;
f.clone()
} else {
ml_auto_extracted += 1;
VulnFeatures::from_vulnerability(vuln)
};
match ml_pipeline.process_features(vuln, &features).await {
Ok(true) => {
ml_processed += 1;
}
Ok(false) => {
}
Err(e) => {
debug!("[ML] Failed to process finding: {}", e);
}
}
}
}
if ml_processed > 0 {
info!(
"[ML] Processed {} vulnerabilities ({} from scanners, {} auto-extracted)",
ml_processed, ml_with_scanner_data, ml_auto_extracted
);
}
if let Err(e) = ml_pipeline.on_scan_complete().await {
warn!("[ML] Failed to complete scan processing: {}", e);
} else {
let stats = ml_pipeline.get_stats().await;
if stats.federated_enabled || stats.can_contribute {
info!(
"[ML] Federated sync complete (contributors: {:?})",
stats.federated_contributors
);
}
}
} else {
debug!(
"[ML] Auto-learning disabled - enable with: lonkero ml enable --federated"
);
}
}
Err(e) => {
debug!("[ML] Could not initialize ML pipeline: {}", e);
}
}
}
println!();
println!("{}", "=".repeat(60));
println!("SCAN COMPLETE");
println!("{}", "=".repeat(60));
println!("Targets scanned: {}", targets.len());
println!("Total tests run: {}", total_tests);
println!("Vulnerabilities: {}", total_vulns);
println!("Duration: {:.2}s", elapsed.as_secs_f64());
println!("{}", "=".repeat(60));
if let Some(output_path) = output {
write_results(&all_results, &output_path, format)?;
info!("Results written to: {}", output_path.display());
} else {
let json = serde_json::to_string_pretty(&all_results)?;
println!("{}", json);
}
if total_vulns > 0 {
std::process::exit(1);
}
Ok(())
}
fn get_dummy_value(field_name: &str) -> String {
let name_lower = field_name.to_lowercase();
if name_lower.contains("email") || name_lower.contains("mail") {
return "test@example.com".to_string();
}
if name_lower.contains("phone") || name_lower.contains("tel") || name_lower.contains("mobile") {
return "+1234567890".to_string();
}
if name_lower.contains("name") || name_lower.contains("nimi") {
return "Test User".to_string();
}
if name_lower.contains("message")
|| name_lower.contains("comment")
|| name_lower.contains("viesti")
|| name_lower.contains("description")
|| name_lower.contains("text")
|| name_lower.contains("body")
{
return "Test message content".to_string();
}
if name_lower.contains("password") || name_lower.contains("pass") {
return "TestPass123!".to_string();
}
if name_lower.contains("url") || name_lower.contains("website") || name_lower.contains("link") {
return "https://example.com".to_string();
}
if name_lower.contains("amount")
|| name_lower.contains("price")
|| name_lower.contains("number")
|| name_lower.contains("quantity")
|| name_lower.contains("age")
{
return "100".to_string();
}
if name_lower.contains("subject") || name_lower.contains("title") || name_lower.contains("aihe")
{
return "Test Subject".to_string();
}
if name_lower.contains("company")
|| name_lower.contains("organization")
|| name_lower.contains("yritys")
{
return "Test Company Ltd".to_string();
}
if name_lower.contains("address")
|| name_lower.contains("street")
|| name_lower.contains("osoite")
{
return "123 Test Street".to_string();
}
if name_lower.contains("city") || name_lower.contains("kaupunki") {
return "Helsinki".to_string();
}
if name_lower.contains("country") || name_lower.contains("maa") {
return "Finland".to_string();
}
if name_lower.contains("zip")
|| name_lower.contains("postal")
|| name_lower.contains("postinumero")
{
return "00100".to_string();
}
"test_value".to_string()
}
fn get_form_input_value(input: &lonkero_scanner::crawler::FormInput) -> String {
if input.input_type.eq_ignore_ascii_case("select") {
if let Some(options) = &input.options {
if !options.is_empty() {
return options[0].clone();
}
}
}
if let Some(value) = &input.value {
if !value.is_empty() {
return value.clone();
}
}
get_dummy_value(&input.name)
}
fn should_skip_form_input(input: &lonkero_scanner::crawler::FormInput) -> bool {
let name_lower = input.name.to_lowercase();
let input_type_lower = input.input_type.to_lowercase();
if input_type_lower == "button" || input_type_lower == "submit" || input_type_lower == "reset" {
return true;
}
if name_lower.starts_with("gtm-") || name_lower.starts_with("gtm_") {
return true;
}
if input_type_lower == "select" {
let is_auto_generated = name_lower.starts_with("input_")
|| name_lower.starts_with("select_")
|| name_lower.starts_with("field_")
|| name_lower.starts_with("form_")
|| name_lower == "input"
|| name_lower == "select"
|| name_lower.chars().all(|c| c.is_ascii_digit() || c == '_');
if is_auto_generated {
return true;
}
if let Some(options) = &input.options {
let lang_codes = [
"en", "fi", "sv", "de", "fr", "es", "it", "nl", "pt", "ja", "zh", "ko", "ru",
"en-us", "en-gb", "fi-fi", "sv-se", "de-de", "fr-fr", "es-es", "english",
"finnish", "swedish", "german", "french", "spanish",
];
let has_language_options = options.iter().any(|opt| {
let opt_lower = opt.to_lowercase();
lang_codes
.iter()
.any(|lc| opt_lower == *lc || opt_lower.starts_with(&format!("{}-", lc)))
});
if has_language_options {
return true;
}
}
}
let is_lang_field_name = name_lower.contains("lang")
|| name_lower.contains("locale")
|| name_lower.contains("language")
|| name_lower.contains("country")
|| name_lower.contains("region");
if is_lang_field_name {
return true;
}
false
}
fn is_language_selector_form(
form_inputs: &[lonkero_scanner::crawler::FormInput],
action: &str,
) -> bool {
if form_inputs.iter().all(should_skip_form_input) {
return true;
}
let action_lower = action.to_lowercase();
let action_no_fragment = action_lower.split('#').next().unwrap_or(&action_lower);
let action_clean = action_no_fragment
.split('?')
.next()
.unwrap_or(action_no_fragment);
let path = if let Some(pos) = action_clean.find("://") {
let after_scheme = &action_clean[pos + 3..];
after_scheme
.find('/')
.map(|p| &after_scheme[p..])
.unwrap_or("")
} else {
action_clean
};
let is_language_url = path.contains("/en/")
|| path.contains("/fi/")
|| path.contains("/sv/")
|| path.contains("/de/")
|| path.contains("/fr/")
|| path.contains("/es/")
|| path.contains("/it/")
|| path.contains("/nl/")
|| path.contains("/pt/")
|| path.contains("/ja/")
|| path.contains("/zh/")
|| path.contains("/ko/")
|| path.contains("/ru/")
|| path == "/en"
|| path == "/fi"
|| path == "/sv"
|| path == "/de"
|| path == "/fr"
|| path == "/es"
|| path == "/it"
|| path == "/nl"
|| path == "/pt";
if form_inputs.len() == 1
&& form_inputs[0].input_type.eq_ignore_ascii_case("select")
&& is_language_url
{
return true;
}
false
}
async fn execute_standalone_scan(
engine: Arc<ScanEngine>,
job: Arc<ScanJob>,
config: &ScannerConfig,
payload_intensity_override: Option<PayloadIntensity>,
) -> Result<ScanResults> {
use lonkero_scanner::crawler::WebCrawler;
use lonkero_scanner::framework_detector::FrameworkDetector;
use lonkero_scanner::headless_crawler::{should_use_headless, HeadlessCrawler};
use lonkero_scanner::types::Vulnerability;
if !signing::is_scan_authorized() {
return Err(anyhow::anyhow!(
"SCAN BLOCKED: Authorization required before scanning. \
This check prevents banned users from accessing the scanner."
));
}
let scan_token = signing::get_scan_token()
.ok_or_else(|| anyhow::anyhow!("No valid scan token available. Re-authorize to continue."))?
.clone();
let start_time = Instant::now();
let started_at = chrono::Utc::now().to_rfc3339();
let target = &job.target;
let scan_config = &job.config;
info!("Starting scan for: {}", target);
let http_client = Arc::new(HttpClient::with_config(
config.request_timeout_secs,
config.max_retries,
config.http2_enabled,
config.http2_adaptive_window,
config.http2_max_concurrent_streams,
config.pool_max_idle_per_host,
)?);
let mut all_vulnerabilities: Vec<Vulnerability> = Vec::new();
let mut total_tests: u64 = 0;
let intelligence_bus = Arc::new(IntelligenceBus::new());
let response_analyzer = Arc::new(ResponseAnalyzer::new());
let mut attack_planner = AttackPlanner::new();
info!("[Intelligence] Scanner intelligence system initialized");
debug!("[Intelligence] Components: IntelligenceBus, ResponseAnalyzer, AttackPlanner");
info!("Phase 0: Reconnaissance");
let mut discovered_params: Vec<String> = Vec::new();
let mut discovered_forms: Vec<(String, Vec<lonkero_scanner::crawler::FormInput>)> = Vec::new(); let mut is_spa_detected = false; let mut crawl_results: Option<CrawlResults> = None;
if scan_config.enable_crawler {
info!(" - Running web crawler (depth: {})", scan_config.max_depth);
let crawler = WebCrawler::new(
Arc::clone(&http_client),
scan_config.max_depth as usize,
scan_config.max_pages as usize,
);
match crawler.crawl(target).await {
Ok(results) => {
info!(
" - Discovered {} URLs, {} forms",
results.crawled_urls.len(),
results.forms.len()
);
is_spa_detected = results.is_spa; crawl_results = Some(results.clone());
for form in &results.forms {
let form_inputs: Vec<lonkero_scanner::crawler::FormInput> = form
.inputs
.iter()
.filter(|input| {
!input.input_type.eq_ignore_ascii_case("hidden")
&& !input.input_type.eq_ignore_ascii_case("submit")
&& !input.name.is_empty()
})
.cloned()
.collect();
if is_language_selector_form(&form_inputs, &form.action) {
debug!(
"[Crawler] Skipping language selector form at {}",
form.action
);
continue;
}
if !form_inputs.is_empty() {
let action_url = if form.action.is_empty() {
target.to_string()
} else {
form.action.clone()
};
let param_names: Vec<String> =
form_inputs.iter().map(|i| i.name.clone()).collect();
discovered_forms.push((action_url, form_inputs));
discovered_params.extend(param_names);
}
}
if !discovered_params.is_empty() {
info!(
" - Found {} input fields to test for XSS",
discovered_params.len()
);
}
}
Err(e) => {
warn!(" - Crawler failed: {}", e);
}
}
}
info!(" - Running endpoint discovery (wordlist fuzzing)");
use lonkero_scanner::discovery::EndpointDiscovery;
let endpoint_discovery = EndpointDiscovery::new(Arc::clone(&http_client));
match endpoint_discovery.discover(target).await {
Ok(endpoints) => {
if !endpoints.is_empty() {
info!(
"[SUCCESS] Discovered {} hidden endpoints via fuzzing",
endpoints.len()
);
for endpoint in &endpoints {
debug!(
" - {} [{}] {:?}",
endpoint.url, endpoint.status_code, endpoint.category
);
}
if let Some(ref mut results) = crawl_results {
for endpoint in &endpoints {
results.links.insert(endpoint.url.clone());
results.crawled_urls.insert(endpoint.url.clone());
if matches!(
endpoint.category,
lonkero_scanner::discovery::EndpointCategory::Api
) {
results.api_endpoints.insert(endpoint.url.clone());
}
}
} else {
let mut new_results = CrawlResults::new();
for endpoint in &endpoints {
new_results.links.insert(endpoint.url.clone());
new_results.crawled_urls.insert(endpoint.url.clone());
if matches!(
endpoint.category,
lonkero_scanner::discovery::EndpointCategory::Api
) {
new_results.api_endpoints.insert(endpoint.url.clone());
}
}
crawl_results = Some(new_results);
}
info!(
"[SUCCESS] Endpoint discovery complete - added {} URLs to scan queue",
endpoints.len()
);
} else {
info!(" - No hidden endpoints discovered");
}
}
Err(e) => {
warn!(" - Endpoint discovery failed: {}", e);
}
}
info!(" - Detecting technologies");
let detector = FrameworkDetector::new(Arc::clone(&http_client));
let detected_technologies: std::collections::HashSet<String> =
match detector.detect(target).await {
Ok(techs) => {
if !techs.is_empty() {
info!("[SUCCESS] Detected {} technologies", techs.len());
for tech in &techs {
info!(" - {} ({:?})", tech.name, tech.category);
let version = tech.version.as_deref();
let confidence = match tech.confidence {
lonkero_scanner::framework_detector::Confidence::High => 0.9,
lonkero_scanner::framework_detector::Confidence::Medium => 0.7,
lonkero_scanner::framework_detector::Confidence::Low => 0.5,
};
intelligence_bus
.report_framework(&tech.name, version, confidence)
.await;
}
}
techs.iter().map(|t| t.name.to_lowercase()).collect()
}
Err(e) => {
warn!(" - Technology detection failed: {}", e);
std::collections::HashSet::new()
}
};
let is_nodejs_stack = detected_technologies.iter().any(|t| {
t.contains("next")
|| t.contains("node")
|| t.contains("express")
|| t.contains("react")
|| t.contains("vue")
|| t.contains("angular")
|| t.contains("nuxt")
|| t.contains("gatsby")
});
let is_php_stack = detected_technologies.iter().any(|t| {
t.contains("php")
|| t.contains("wordpress")
|| t.contains("laravel")
|| t.contains("drupal")
|| t.contains("magento")
});
let is_python_stack = detected_technologies.iter().any(|t| {
t.contains("python")
|| t.contains("django")
|| t.contains("flask")
|| t.contains("jinja")
|| t.contains("fastapi")
});
let is_java_stack = detected_technologies.iter().any(|t| {
t.contains("java")
|| t.contains("spring")
|| t.contains("tomcat")
|| t.contains("struts")
|| t.contains("jsp")
});
let is_static_site = false;
if scan_config.enable_crawler && !is_static_site {
if let Some(ref static_crawl_results) = crawl_results {
let html_content: Option<String> = match http_client.get(target).await {
Ok(response) => Some(response.body),
Err(_) => None,
};
if should_use_headless(
static_crawl_results,
&detected_technologies,
html_content.as_deref(),
) {
info!(
"[HeadlessCrawler] Switching to headless crawling for enhanced SPA discovery"
);
let headless = HeadlessCrawler::new(30); let max_pages = scan_config.max_pages.min(50) as usize;
match headless.crawl_authenticated_site(target, max_pages).await {
Ok(headless_results) => {
info!("[HeadlessCrawler] Headless crawl complete:");
info!(
" - Pages visited: {}",
headless_results.pages_visited.len()
);
info!(" - Forms discovered: {}", headless_results.forms.len());
info!(
" - API endpoints found: {}",
headless_results.api_endpoints.len()
);
if let Some(ref mut results) = crawl_results {
headless_results.merge_into(results);
results.deduplicate_forms();
for form in &results.forms {
let form_inputs: Vec<lonkero_scanner::crawler::FormInput> = form
.inputs
.iter()
.filter(|input| {
!input.input_type.eq_ignore_ascii_case("hidden")
&& !input.input_type.eq_ignore_ascii_case("submit")
&& !input.name.is_empty()
})
.cloned()
.collect();
if is_language_selector_form(&form_inputs, &form.action) {
continue;
}
if !form_inputs.is_empty() {
let action_url = if form.action.is_empty() {
target.to_string()
} else {
form.action.clone()
};
if !discovered_forms.iter().any(|(url, _)| url == &action_url) {
let param_names: Vec<String> =
form_inputs.iter().map(|i| i.name.clone()).collect();
discovered_forms.push((action_url, form_inputs));
discovered_params.extend(param_names);
}
}
}
info!(
"[HeadlessCrawler] After merge: {} total forms, {} total params",
discovered_forms.len(),
discovered_params.len()
);
}
}
Err(e) => {
warn!("[HeadlessCrawler] Headless crawl failed: {}", e);
}
}
}
}
}
let intelligent_scan_plan: Option<IntelligentScanPlan> = if scan_config
.scan_mode
.is_intelligent()
{
info!("[Orchestrator] Intelligent mode enabled - generating context-aware scan plan");
let tech_categories: Vec<TechCategory> = detected_technologies
.iter()
.map(|t| TechCategory::from_detected_technology(t, ""))
.collect();
let orchestrator = IntelligentScanOrchestrator::new();
let plan = orchestrator.generate_scan_plan(
crawl_results.as_ref().unwrap_or(&CrawlResults::new()),
&tech_categories,
target,
);
info!("[Orchestrator] Scan plan generated:");
info!(" - Scanners selected: {}", plan.stats.scanners_selected);
info!(
" - Technologies detected: {:?}",
plan.stats.technologies_detected
);
info!(
" - Targets: {}/{} ({:.1}% reduction from deduplication)",
plan.stats.total_deduplicated, plan.stats.total_original, plan.stats.reduction_percent
);
info!(
" - Parameter risk distribution: {} high, {} medium, {} low",
plan.stats.high_risk_params, plan.stats.medium_risk_params, plan.stats.low_risk_params
);
if !plan.prioritized_params.is_empty() {
info!(" - Top priority parameters:");
for param in plan.prioritized_params.iter().take(5) {
info!(
" - {} (risk: {}, intensity: {:?})",
param.name, param.risk_score, param.intensity
);
}
}
Some(plan)
} else {
info!(
"[Orchestrator] Legacy mode ({}) - using global payload count",
scan_config.scan_mode
);
None
};
let get_param_intensity = |param_name: &str| -> PayloadIntensity {
if let Some(override_intensity) = payload_intensity_override {
return override_intensity;
}
if let Some(ref plan) = intelligent_scan_plan {
plan.prioritized_params
.iter()
.find(|p| p.name == param_name)
.map(|p| p.intensity)
.unwrap_or(PayloadIntensity::Standard)
} else {
match scan_config.scan_mode {
ScanMode::Fast => PayloadIntensity::Minimal,
ScanMode::Normal => PayloadIntensity::Standard,
ScanMode::Thorough => PayloadIntensity::Extended,
ScanMode::Insane => PayloadIntensity::Maximum,
ScanMode::Intelligent => PayloadIntensity::Standard, }
}
};
if let Some(override_intensity) = payload_intensity_override {
info!(
"[Config] Payload intensity override: {:?} (applies to all parameters)",
override_intensity
);
}
let has_auth = scan_config.auth_token.is_some() || scan_config.custom_headers.is_some();
let needs_headless =
((is_nodejs_stack || is_spa_detected) && discovered_forms.is_empty()) || has_auth;
let mut intercepted_endpoints: Vec<String> = Vec::new();
if needs_headless {
if has_auth {
info!(
" - Authentication provided, using headless browser for authenticated crawling..."
);
} else {
info!(" - SPA detected with no forms found, using headless browser to discover real endpoints...");
}
let headless_headers = scan_config.custom_headers.clone().unwrap_or_default();
let headless =
HeadlessCrawler::with_headers(30, scan_config.auth_token.clone(), headless_headers);
if scan_config.auth_token.is_some() || scan_config.custom_headers.is_some() {
info!(" - Using authenticated headless session - performing full site crawl");
let max_pages = 50;
match headless.crawl_authenticated_site(target, max_pages).await {
Ok(headless_crawl_results) => {
info!("[SUCCESS] Authenticated site crawl complete:");
info!(
" - Pages visited: {}",
headless_crawl_results.pages_visited.len()
);
info!(
" - Forms discovered: {}",
headless_crawl_results.forms.len()
);
info!(
" - API endpoints found: {}",
headless_crawl_results.api_endpoints.len()
);
for form in &headless_crawl_results.forms {
let form_inputs: Vec<lonkero_scanner::crawler::FormInput> = form
.inputs
.iter()
.filter(|input| {
!input.input_type.eq_ignore_ascii_case("hidden")
&& !input.input_type.eq_ignore_ascii_case("submit")
&& !input.name.is_empty()
})
.cloned()
.collect();
if is_language_selector_form(&form_inputs, &form.action) {
debug!(
"[Headless] Skipping language selector form at {}",
form.action
);
continue;
}
if !form_inputs.is_empty() {
let action_url = if form.action.is_empty() {
form.discovered_at.clone()
} else {
form.action.clone()
};
let param_names: Vec<String> =
form_inputs.iter().map(|i| i.name.clone()).collect();
discovered_forms.push((action_url, form_inputs));
discovered_params.extend(param_names);
}
}
for ep in &headless_crawl_results.api_endpoints {
info!(" - API: {} {}", ep.method, ep.url);
intercepted_endpoints.push(ep.url.clone());
}
if !headless_crawl_results.js_files.is_empty() {
info!(
" - JS files discovered: {}",
headless_crawl_results.js_files.len()
);
}
if !headless_crawl_results.graphql_endpoints.is_empty() {
info!(
" - GraphQL endpoints: {}",
headless_crawl_results.graphql_endpoints.len()
);
for gql_ep in &headless_crawl_results.graphql_endpoints {
info!(" - {}", gql_ep);
if !intercepted_endpoints.contains(gql_ep) {
intercepted_endpoints.push(gql_ep.clone());
}
}
}
if !headless_crawl_results.graphql_operations.is_empty() {
info!(
" - GraphQL operations discovered: {}",
headless_crawl_results.graphql_operations.len()
);
for op in &headless_crawl_results.graphql_operations {
info!(
" - {} {} (from {})",
op.operation_type, op.name, op.source
);
}
}
for page_url in &headless_crawl_results.pages_visited {
if !intercepted_endpoints.contains(page_url) {
intercepted_endpoints.push(page_url.clone());
}
}
info!(" - Total: {} forms with {} fields, {} API endpoints, {} GraphQL endpoints",
discovered_forms.len(), discovered_params.len(),
intercepted_endpoints.len(), headless_crawl_results.graphql_endpoints.len());
}
Err(e) => {
warn!(" - Full site crawl failed: {}", e);
warn!(" - Falling back to single-page scan");
}
}
} else {
info!(" - Intercepting network requests to discover form endpoints...");
match headless.discover_form_endpoints(target).await {
Ok(endpoints) => {
if !endpoints.is_empty() {
info!(
"[SUCCESS] Intercepted {} form submission endpoints:",
endpoints.len()
);
for ep in &endpoints {
info!(
" - {} {} ({})",
ep.method,
ep.url,
ep.content_type.as_deref().unwrap_or("unknown")
);
intercepted_endpoints.push(ep.url.clone());
}
} else {
info!(" - No POST requests intercepted during form submission");
}
}
Err(e) => {
warn!(" - Network interception failed: {}", e);
}
}
match headless.extract_forms(target).await {
Ok(forms) => {
if !forms.is_empty() {
info!("[SUCCESS] Headless browser found {} forms", forms.len());
for form in &forms {
let form_inputs: Vec<lonkero_scanner::crawler::FormInput> = form
.inputs
.iter()
.filter(|input| {
!input.input_type.eq_ignore_ascii_case("hidden")
&& !input.input_type.eq_ignore_ascii_case("submit")
&& !input.name.is_empty()
})
.cloned()
.collect();
if is_language_selector_form(&form_inputs, &form.action) {
debug!(
"[Headless] Skipping language selector form at {}",
form.action
);
continue;
}
if !form_inputs.is_empty() {
let action_url = if !intercepted_endpoints.is_empty() {
intercepted_endpoints[0].clone()
} else if form.action.is_empty() {
target.to_string()
} else {
form.action.clone()
};
let param_names: Vec<String> =
form_inputs.iter().map(|i| i.name.clone()).collect();
discovered_forms.push((action_url.clone(), form_inputs.clone()));
discovered_params.extend(param_names.clone());
for ep in intercepted_endpoints.iter().skip(1) {
discovered_forms.push((ep.clone(), form_inputs.clone()));
}
}
}
info!(
" - Found {} real form fields from rendered page",
discovered_params.len()
);
if !intercepted_endpoints.is_empty() {
info!(" - Using intercepted API endpoint(s) instead of page URL");
}
}
}
Err(e) => {
warn!(
" - Headless browser failed: {} (Chrome/Chromium may not be installed)",
e
);
}
}
}
}
if is_nodejs_stack
|| detected_technologies
.iter()
.any(|t| t.contains("next") || t.contains("react"))
{
info!(" - Checking CVE-2025-55182 (React Server Components RCE)");
let (vulns, tests) = engine
.cve_2025_55182_scanner
.scan(target, scan_config)
.await?;
if vulns
.iter()
.any(|v| v.severity == lonkero_scanner::types::Severity::Critical)
{
warn!("[CRITICAL] CVE-2025-55182 vulnerability detected!");
} else if !vulns.is_empty() {
info!("[OK] CVE-2025-55182: Protected by WAF");
}
all_vulnerabilities.extend(vulns);
total_tests += tests as u64;
info!(" - Checking CVE-2025-55183 (RSC Source Code Exposure)");
let (vulns, tests) = engine
.cve_2025_55183_scanner
.scan(target, scan_config)
.await?;
if vulns.iter().any(|v| {
v.severity == lonkero_scanner::types::Severity::Medium
|| v.severity == lonkero_scanner::types::Severity::High
}) {
warn!("[ALERT] CVE-2025-55183 vulnerability detected!");
} else if !vulns.is_empty() {
info!("[OK] CVE-2025-55183: Protected by WAF");
}
all_vulnerabilities.extend(vulns);
total_tests += tests as u64;
info!(" - Checking CVE-2025-55184 (RSC Denial of Service)");
let (vulns, tests) = engine
.cve_2025_55184_scanner
.scan(target, scan_config)
.await?;
if vulns.iter().any(|v| {
v.severity == lonkero_scanner::types::Severity::High
|| v.severity == lonkero_scanner::types::Severity::Critical
}) {
warn!("[ALERT] CVE-2025-55184 vulnerability detected!");
} else if !vulns.is_empty() {
info!("[OK] CVE-2025-55184: Protected by WAF");
}
all_vulnerabilities.extend(vulns);
total_tests += tests as u64;
}
{
info!(" - Checking Azure APIM Cross-Tenant Signup Bypass");
let (vulns, tests) = engine.azure_apim_scanner.scan(target, scan_config).await?;
if !vulns.is_empty() {
warn!("[ALERT] Azure APIM Cross-Tenant Signup Bypass detected!");
}
all_vulnerabilities.extend(vulns);
total_tests += tests as u64;
}
info!(" - Pre-scanning JavaScript for API endpoints and parameters");
let js_miner_results = engine
.js_miner_scanner
.scan_with_extraction(target, scan_config)
.await?;
all_vulnerabilities.extend(js_miner_results.vulnerabilities);
total_tests += js_miner_results.tests_run as u64;
let js_param_count: usize = js_miner_results.parameters.values().map(|s| s.len()).sum();
if !js_miner_results.api_endpoints.is_empty() || !js_miner_results.graphql_endpoints.is_empty()
{
info!(
"[SUCCESS] JS Mining found {} API endpoints, {} GraphQL endpoints, {} parameters",
js_miner_results.api_endpoints.len(),
js_miner_results.graphql_endpoints.len(),
js_param_count
);
}
if scan_token.is_module_authorized(module_ids::advanced_scanning::COGNITO_ENUM) {
info!(" - Testing AWS Cognito User Enumeration (early - before rate limiting)");
let mut cognito_endpoints: Vec<String> = intercepted_endpoints.clone();
for (form_action_url, _) in &discovered_forms {
if !cognito_endpoints.contains(form_action_url) {
cognito_endpoints.push(form_action_url.clone());
}
}
let (vulns, tests) = engine
.cognito_enum_scanner
.scan_with_endpoints(target, scan_config, &cognito_endpoints)
.await?;
all_vulnerabilities.extend(vulns);
total_tests += tests as u64;
}
let mut auth_tests_done = false;
if scan_config.auth_token.is_some() {
info!("Phase 0.5: Early authentication testing (JWT token may expire)");
info!(" - Testing JWT Security (priority: token freshness)");
if let Some(ref token) = scan_config.auth_token {
let (vulns, tests) = engine
.jwt_scanner
.scan_jwt(target, token, scan_config)
.await?;
all_vulnerabilities.extend(vulns);
total_tests += tests as u64;
}
info!(" - Testing JWT Vulnerabilities");
let (vulns, tests) = engine
.jwt_vulnerabilities_scanner
.scan(target, scan_config)
.await?;
all_vulnerabilities.extend(vulns);
total_tests += tests as u64;
info!(" - Testing GraphQL Security (uses auth token)");
let (vulns, tests) = engine.graphql_scanner.scan(target, scan_config).await?;
all_vulnerabilities.extend(vulns);
total_tests += tests as u64;
info!(" - Testing Advanced GraphQL Security");
let (vulns, tests) = engine
.graphql_security_scanner
.scan(target, scan_config)
.await?;
all_vulnerabilities.extend(vulns);
total_tests += tests as u64;
for ep in &intercepted_endpoints {
if ep.to_lowercase().contains("graphql") {
info!(" - Testing discovered GraphQL endpoint: {}", ep);
let (vulns, tests) = engine.graphql_scanner.scan(ep, scan_config).await?;
all_vulnerabilities.extend(vulns);
total_tests += tests as u64;
}
}
info!(" - Testing API Security");
let (vulns, tests) = engine
.api_security_scanner
.scan(target, scan_config)
.await?;
all_vulnerabilities.extend(vulns);
total_tests += tests as u64;
info!(" - Testing Authentication Bypass");
let (vulns, tests) = engine.auth_bypass_scanner.scan(target, scan_config).await?;
all_vulnerabilities.extend(vulns);
total_tests += tests as u64;
info!(" - Testing IDOR");
let (vulns, tests) = engine.idor_scanner.scan(target, scan_config).await?;
all_vulnerabilities.extend(vulns);
total_tests += tests as u64;
info!(" - Testing BOLA");
let (vulns, tests) = engine.bola_scanner.scan(target, scan_config).await?;
all_vulnerabilities.extend(vulns);
total_tests += tests as u64;
auth_tests_done = true;
info!("[SUCCESS] Early auth testing complete - token-dependent tests done");
}
info!("Phase 1: Parameter injection testing");
let parsed_url = url::Url::parse(target)?;
let mut test_params: Vec<(String, String)> = parsed_url
.query_pairs()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect();
for param in &discovered_params {
if !test_params.iter().any(|(name, _)| name == param) {
test_params.push((param.clone(), "test".to_string()));
}
}
for (endpoint, param_set) in js_miner_results.parameters.iter() {
if endpoint == "global" || endpoint == "*" {
continue;
}
for param in param_set {
if !test_params.iter().any(|(name, _)| name == param) {
test_params.push((param.clone(), "test".to_string()));
}
}
}
const MAX_PARAMS_TO_TEST: usize = 100;
let original_count = test_params.len();
if test_params.len() > MAX_PARAMS_TO_TEST {
info!(
" [NOTE] Limiting parameter tests from {} to {} (max limit)",
original_count, MAX_PARAMS_TO_TEST
);
test_params.truncate(MAX_PARAMS_TO_TEST);
}
let has_real_params = !test_params.is_empty();
if !has_real_params {
info!(" [NOTE] No parameters found - skipping parameter injection tests");
} else {
info!(
" [OK] Found {} parameters to test (URL + discovered forms)",
test_params.len()
);
}
let has_graphql_from_miner = !js_miner_results.graphql_endpoints.is_empty();
let all_intercepted_are_graphql = intercepted_endpoints
.iter()
.all(|ep| ep.to_lowercase().contains("graphql"));
let utility_endpoints = [
"ping", "health", "healthz", "status", "ready", "live", "metrics", "version",
];
let has_non_graphql_api = js_miner_results.api_endpoints.iter().any(|ep| {
let ep_lower = ep.to_lowercase();
let is_graphql = ep_lower.contains("graphql");
let is_utility = utility_endpoints.iter().any(|u| {
ep_lower.ends_with(&format!("/{}", u)) || ep_lower.ends_with(&format!("/{}/", u))
});
!is_graphql && !is_utility
});
let is_graphql_only = has_graphql_from_miner
&& !has_non_graphql_api
&& (intercepted_endpoints.is_empty() || all_intercepted_are_graphql);
if is_graphql_only {
info!(
" [GraphQL] Detected GraphQL-only backend (found {} GraphQL endpoints)",
js_miner_results.graphql_endpoints.len()
);
}
let baseline_response = (engine.http_client.get(target).await).ok();
let detected_server = baseline_response
.as_ref()
.and_then(|r| r.headers.get("server"))
.cloned();
let content_type = baseline_response
.as_ref()
.and_then(|r| r.headers.get("content-type"))
.cloned();
let primary_framework = if is_nodejs_stack {
detected_technologies
.iter()
.find(|t| t.contains("next") || t.contains("nuxt") || t.contains("gatsby"))
.or_else(|| {
detected_technologies
.iter()
.find(|t| t.contains("react") || t.contains("vue") || t.contains("angular"))
})
.or_else(|| {
detected_technologies
.iter()
.find(|t| t.contains("express") || t.contains("node"))
})
.cloned()
} else if is_php_stack {
detected_technologies
.iter()
.find(|t| {
t.contains("laravel")
|| t.contains("wordpress")
|| t.contains("drupal")
|| t.contains("magento")
})
.or_else(|| detected_technologies.iter().find(|t| t.contains("php")))
.cloned()
} else if is_python_stack {
detected_technologies
.iter()
.find(|t| t.contains("django") || t.contains("flask") || t.contains("fastapi"))
.or_else(|| detected_technologies.iter().find(|t| t.contains("python")))
.cloned()
} else if is_java_stack {
detected_technologies
.iter()
.find(|t| t.contains("spring") || t.contains("struts") || t.contains("jsp"))
.or_else(|| {
detected_technologies
.iter()
.find(|t| t.contains("tomcat") || t.contains("java"))
})
.cloned()
} else {
None
};
if has_real_params {
if !discovered_forms.is_empty() && !is_graphql_only {
use std::collections::HashMap;
let mut form_signatures: HashMap<String, (String, Vec<&lonkero_scanner::crawler::FormInput>)> = HashMap::new();
for (action_url, form_inputs) in &discovered_forms {
let mut sig_parts: Vec<String> = form_inputs.iter()
.map(|f| format!("{}:{}", f.name, f.input_type))
.collect();
sig_parts.sort();
let signature = sig_parts.join("|");
form_signatures.entry(signature)
.or_insert((action_url.clone(), form_inputs.iter().collect()));
}
let original_count = discovered_forms.len();
let deduplicated_count = form_signatures.len();
if deduplicated_count < original_count {
info!(
" - Testing {} unique forms (deduplicated from {} total forms)",
deduplicated_count, original_count
);
info!(
" - Skipped {} duplicate forms (same fields on different pages)",
original_count - deduplicated_count
);
} else {
info!(
" - Testing {} discovered forms with POST",
discovered_forms.len()
);
}
let discovered_forms: Vec<(String, Vec<&lonkero_scanner::crawler::FormInput>)> =
form_signatures.into_values().collect();
if !js_miner_results.api_endpoints.is_empty() {
info!(" [JS-Miner] Discovered API endpoints:");
for ep in &js_miner_results.api_endpoints {
info!(" - {}", ep);
}
}
if !js_miner_results.form_actions.is_empty() {
info!(" [JS-Miner] Discovered form actions:");
for ep in &js_miner_results.form_actions {
info!(" - {}", ep);
}
}
let framework_noise = [
"traceparent",
"csrftoken",
"baggage",
"sentry-trace",
"sentry.sample_rand",
"sentry.sample_rate",
"sentry.dsc",
"next-router-prefetch",
"next-url",
"next-router-state-tree",
"rsc",
"_rsc",
"__next",
"__nextjs",
"x-middleware-prefetch",
"x-invoke-path",
"x-invoke-query",
];
let form_api_endpoints: Vec<String> = js_miner_results
.api_endpoints
.iter()
.chain(js_miner_results.form_actions.iter())
.filter(|ep| {
let ep_lower = ep.to_lowercase();
let ep_clean = ep_lower.trim_start_matches('/');
!framework_noise.iter().any(|noise| {
ep_clean == *noise || ep_clean.starts_with(&format!("{}.", noise))
})
})
.cloned()
.collect();
for (action_url, form_inputs) in &discovered_forms {
let base_body: String = form_inputs
.iter()
.map(|input| format!("{}={}", input.name, get_form_input_value(input)))
.collect::<Vec<_>>()
.join("&");
let mut test_urls: Vec<String> = vec![action_url.clone()];
let parsed_target = url::Url::parse(target).ok();
let action_normalized = action_url.trim_end_matches('/');
let target_normalized = target.trim_end_matches('/');
let is_page_url = action_normalized == target_normalized
|| parsed_target
.as_ref()
.map(|t| {
let origin = t.origin().ascii_serialization();
action_normalized == origin
|| action_normalized == format!("{}/", origin)
})
.unwrap_or(false);
debug!(
" action_url={}, target={}, is_page_url={}",
action_url, target, is_page_url
);
if is_page_url && !form_api_endpoints.is_empty() {
info!(" [SPA] Form action is page URL, also testing {} potential API endpoints", form_api_endpoints.len());
for api_ep in &form_api_endpoints {
let full_url = if api_ep.starts_with("http") {
api_ep.clone()
} else if let Some(ref parsed) = parsed_target {
let path = if api_ep.starts_with('/') {
api_ep.as_str()
} else {
&format!("/{}", api_ep)
};
format!("{}{}", parsed.origin().ascii_serialization(), path)
} else {
continue;
};
if !test_urls.contains(&full_url) {
test_urls.push(full_url);
}
}
}
let json_body: String = format!(
"{{{}}}",
form_inputs
.iter()
.map(|input| format!(
"\"{}\":\"{}\"",
input.name,
get_form_input_value(input)
))
.collect::<Vec<_>>()
.join(",")
);
for input in form_inputs {
if should_skip_form_input(input) {
debug!(
" Skipping auto-generated/language field '{}' ({})",
input.name, input.input_type
);
continue;
}
for test_url in &test_urls {
let is_api_endpoint = test_url.contains("/api/");
info!(
" Testing form field '{}' ({}) at {}{}",
input.name,
input.input_type,
test_url,
if is_api_endpoint { " [API/JSON]" } else { "" }
);
let (body_to_test, content_type) = if is_api_endpoint {
(json_body.clone(), Some("application/json"))
} else {
(base_body.clone(), Some("application/x-www-form-urlencoded"))
};
if !is_static_site {
let (vulns, tests) = engine
.sqli_scanner
.scan_post_body(test_url, &input.name, &body_to_test, scan_config)
.await?;
all_vulnerabilities.extend(vulns);
total_tests += tests as u64;
}
}
}
}
} else if is_graphql_only && !discovered_forms.is_empty() {
info!(
" - Skipping {} form POST tests (GraphQL backend uses mutations)",
discovered_forms.len()
);
}
let build_scan_context = |param_name: &str| -> lonkero_scanner::types::ScanContext {
use lonkero_scanner::types::{EndpointType, ParameterSource, ScanContext};
let parameter_source = if discovered_params.contains(¶m_name.to_string()) {
ParameterSource::HtmlForm
} else if parsed_url.query_pairs().any(|(k, _)| k == param_name) {
ParameterSource::UrlQueryString
} else if js_miner_results
.parameters
.values()
.any(|params| params.contains(param_name))
{
ParameterSource::JavaScriptMined
} else {
ParameterSource::Unknown
};
let endpoint_type = if is_graphql_only || !js_miner_results.graphql_endpoints.is_empty()
{
EndpointType::GraphQlApi
} else if !js_miner_results.api_endpoints.is_empty() {
EndpointType::RestApi
} else if !discovered_forms.is_empty() {
EndpointType::FormSubmission
} else {
EndpointType::Unknown
};
let is_json_api = content_type
.as_ref()
.map(|ct| ct.contains("application/json"))
.unwrap_or(false)
|| !js_miner_results.api_endpoints.is_empty();
ScanContext {
parameter_source,
endpoint_type,
detected_tech: detected_technologies.iter().cloned().collect(),
framework: primary_framework.clone(),
server: detected_server.clone(),
other_parameters: test_params
.iter()
.map(|(name, _)| name.clone())
.filter(|name| name != param_name)
.collect(),
is_json_api,
is_graphql: !js_miner_results.graphql_endpoints.is_empty(),
form_fields: discovered_params.clone(),
content_type: content_type.clone(),
}
};
let mut xss_urls_to_test: Vec<String> = vec![target.to_string()];
if let Some(ref crawl_data) = crawl_results {
for crawled_url in &crawl_data.crawled_urls {
if crawled_url != target && !xss_urls_to_test.contains(crawled_url) {
xss_urls_to_test.push(crawled_url.clone());
}
}
}
if !is_graphql_only && !is_static_site {
let xss_authorized = scan_token.is_module_authorized(module_ids::advanced_scanning::XSS_SCANNER);
let intensity = payload_intensity_override.unwrap_or(PayloadIntensity::Standard);
info!(
" - Running 3 XSS scanners in PARALLEL on {} URLs (intensity: {:?})",
xss_urls_to_test.len(),
intensity
);
let engine_clone1 = Arc::clone(&engine);
let engine_clone2 = Arc::clone(&engine);
let engine_clone3 = Arc::clone(&engine);
let engine_clone4 = Arc::clone(&engine);
let engine_clone5 = Arc::clone(&engine);
let urls_clone1 = xss_urls_to_test.clone();
let urls_clone2 = xss_urls_to_test.clone();
let urls_clone3 = xss_urls_to_test.clone();
let urls_clone4 = xss_urls_to_test.clone();
let urls_clone5 = xss_urls_to_test.clone();
let config_clone1 = scan_config.clone();
let config_clone2 = scan_config.clone();
let config_clone3 = scan_config.clone();
let config_clone4 = scan_config.clone();
let config_clone5 = scan_config.clone();
let (hybrid_result, proof_result, reflection_result, postmessage_result, dom_clobbering_result) = tokio::join!(
async {
if xss_authorized {
engine_clone1
.hybrid_xss_detector
.scan_parallel(&urls_clone1, &config_clone1)
.await
} else {
Ok((Vec::new(), 0))
}
},
async {
let mut vulns = Vec::new();
let mut tests = 0usize;
for url in &urls_clone2 {
match engine_clone2.proof_xss_scanner.scan(url, &config_clone2).await {
Ok((v, t)) => {
vulns.extend(v);
tests += t;
}
Err(e) => {
debug!("[Proof-XSS] Error scanning {}: {}", url, e);
}
}
}
Ok::<_, anyhow::Error>((vulns, tests))
},
async {
let mut vulns = Vec::new();
let mut tests = 0usize;
for url in &urls_clone3 {
match engine_clone3
.reflection_xss_scanner
.scan_with_intensity(url, &config_clone3, intensity)
.await
{
Ok((v, t)) => {
vulns.extend(v);
tests += t;
}
Err(e) => {
debug!("[Reflection-XSS] Error scanning {}: {}", url, e);
}
}
}
Ok::<_, anyhow::Error>((vulns, tests))
},
async {
let mut vulns = Vec::new();
let mut tests = 0usize;
for url in &urls_clone4 {
match engine_clone4.postmessage_vulns_scanner.scan(url, &config_clone4).await {
Ok((v, t)) => {
vulns.extend(v);
tests += t;
}
Err(e) => {
debug!("[PostMessage] Error scanning {}: {}", url, e);
}
}
}
Ok::<_, anyhow::Error>((vulns, tests))
},
async {
let mut vulns = Vec::new();
let mut tests = 0usize;
for url in &urls_clone5 {
match engine_clone5.dom_clobbering_scanner.scan(url, &config_clone5).await {
Ok((v, t)) => {
vulns.extend(v);
tests += t;
}
Err(e) => {
debug!("[DOM-Clobbering] Error scanning {}: {}", url, e);
}
}
}
Ok::<_, anyhow::Error>((vulns, tests))
}
);
let mut xss_vulns_found = 0;
if let Ok((vulns, tests)) = hybrid_result {
xss_vulns_found += vulns.len();
all_vulnerabilities.extend(vulns);
total_tests += tests as u64;
info!(" [Hybrid XSS] {} vulns, {} tests", xss_vulns_found, tests);
}
if let Ok((vulns, tests)) = proof_result {
for vuln in vulns {
let already_found = all_vulnerabilities.iter().any(|v| {
v.url == vuln.url && v.parameter == vuln.parameter && v.category == "XSS"
});
if !already_found {
xss_vulns_found += 1;
all_vulnerabilities.push(vuln);
}
}
total_tests += tests as u64;
info!(" [Proof XSS] {} total vulns after dedup, {} tests", xss_vulns_found, tests);
}
if let Ok((vulns, tests)) = reflection_result {
for vuln in vulns {
let already_found = all_vulnerabilities.iter().any(|v| {
v.url == vuln.url && v.parameter == vuln.parameter && v.category == "XSS"
});
if !already_found {
xss_vulns_found += 1;
all_vulnerabilities.push(vuln);
}
}
total_tests += tests as u64;
info!(
" [Reflection XSS] {} total vulns after dedup, {} tests ({} payloads)",
xss_vulns_found,
tests,
intensity.payload_limit()
);
}
if let Ok((vulns, tests)) = postmessage_result {
for vuln in vulns {
let already_found = all_vulnerabilities.iter().any(|v| {
v.url == vuln.url && v.vuln_type == vuln.vuln_type
});
if !already_found {
xss_vulns_found += 1;
all_vulnerabilities.push(vuln);
}
}
total_tests += tests as u64;
info!(" [PostMessage] {} total vulns after dedup, {} tests", xss_vulns_found, tests);
}
if let Ok((vulns, tests)) = dom_clobbering_result {
for vuln in vulns {
let already_found = all_vulnerabilities.iter().any(|v| {
v.url == vuln.url && v.vuln_type == vuln.vuln_type
});
if !already_found {
xss_vulns_found += 1;
all_vulnerabilities.push(vuln);
}
}
total_tests += tests as u64;
info!(" [DOM Clobbering] {} total vulns after dedup, {} tests", xss_vulns_found, tests);
}
info!(
" - XSS scanning complete: {} unique vulnerabilities found",
xss_vulns_found
);
} else if is_graphql_only {
info!(" - Skipping XSS - GraphQL backend returns JSON, not HTML");
} else if is_static_site {
info!(" - Skipping XSS - Static site detected");
}
if !is_graphql_only && !is_static_site {
info!(" - Testing Second-Order Injection (stored XSS/SQLi/CMDi)");
let mut second_order_scanner = lonkero_scanner::scanners::SecondOrderInjectionScanner::new(Arc::clone(&engine.http_client));
match second_order_scanner.scan(target, &scan_config).await {
Ok((vulns, tests)) => {
for vuln in vulns {
let already_found = all_vulnerabilities.iter().any(|v| {
v.url == vuln.url && v.vuln_type == vuln.vuln_type && v.parameter == vuln.parameter
});
if !already_found {
all_vulnerabilities.push(vuln);
}
}
total_tests += tests as u64;
info!(" [Second-Order] {} tests completed", tests);
}
Err(e) => {
debug!("[Second-Order] Error: {}", e);
}
}
}
if scan_token.is_module_authorized(module_ids::advanced_scanning::SQLI_SCANNER) {
if !is_static_site && !is_graphql_only {
info!(
" - Testing SQL Injection ({} parameters)",
test_params.len()
);
for (param_name, _) in &test_params {
let context = build_scan_context(param_name);
let intensity = get_param_intensity(param_name);
debug!(
" [Intelligent] Parameter '{}' intensity: {:?} (limit: {} payloads)",
param_name,
intensity,
intensity.payload_limit()
);
let (vulns, tests) = engine
.sqli_scanner
.scan_parameter(target, param_name, scan_config, Some(&context))
.await?;
all_vulnerabilities.extend(vulns);
total_tests += tests as u64;
}
} else if is_graphql_only {
info!(" - Skipping SQLi (GraphQL uses parameterized queries)");
}
} else {
info!(" [SKIP] SQLi scanner requires Professional or higher license");
}
if scan_token.is_module_authorized(module_ids::advanced_scanning::COMMAND_INJECTION) {
if !is_static_site {
info!(" - Testing Command Injection");
for (param_name, _) in &test_params {
let intensity = get_param_intensity(param_name);
debug!(
" [Intelligent] Parameter '{}' intensity: {:?} (limit: {} payloads)",
param_name,
intensity,
intensity.payload_limit()
);
let (vulns, tests) = engine
.cmdi_scanner
.scan_parameter(target, param_name, scan_config)
.await?;
all_vulnerabilities.extend(vulns);
total_tests += tests as u64;
}
}
} else {
info!(" [SKIP] Command Injection scanner requires Professional or higher license");
}
if scan_token.is_module_authorized(module_ids::advanced_scanning::PATH_TRAVERSAL) {
if !is_static_site && !is_graphql_only {
info!(" - Testing Path Traversal");
for (param_name, _) in &test_params {
let intensity = get_param_intensity(param_name);
debug!(
" [Intelligent] Parameter '{}' intensity: {:?} (limit: {} payloads)",
param_name,
intensity,
intensity.payload_limit()
);
let (vulns, tests) = engine
.path_scanner
.scan_parameter(target, param_name, scan_config)
.await?;
all_vulnerabilities.extend(vulns);
total_tests += tests as u64;
}
} else if is_graphql_only {
info!(" - Skipping Path Traversal (GraphQL serves JSON data, not files)");
}
} else {
info!(" [SKIP] Path Traversal scanner requires Professional or higher license");
}
if scan_token.is_module_authorized(module_ids::advanced_scanning::SSRF_SCANNER) {
if !is_static_site {
let ssrf_keywords = [
"url", "link", "redirect", "callback", "webhook", "image", "img", "src",
"href", "file", "path", "endpoint", "uri", "dest", "target", "fetch", "load",
"proxy", "forward", "next", "return", "goto", "site",
];
let ssrf_params: Vec<_> = test_params
.iter()
.filter(|(name, _)| {
let name_lower = name.to_lowercase();
ssrf_keywords.iter().any(|kw| name_lower.contains(kw))
|| name_lower.starts_with("http")
})
.collect();
if !ssrf_params.is_empty() {
info!(
" - Testing SSRF ({} URL-like params of {} total)",
ssrf_params.len(),
test_params.len()
);
for (param_name, _) in &ssrf_params {
let (vulns, tests) = engine
.ssrf_scanner
.scan_parameter(target, param_name, scan_config)
.await?;
all_vulnerabilities.extend(vulns);
total_tests += tests as u64;
if scan_token
.is_module_authorized(module_ids::advanced_scanning::SSRF_BLIND)
{
info!(" Testing Blind SSRF on '{}'", param_name);
let (blind_vulns, blind_tests) = engine
.ssrf_blind_scanner
.scan_parameter(target, param_name, scan_config)
.await?;
all_vulnerabilities.extend(blind_vulns);
total_tests += blind_tests as u64;
}
}
} else {
info!(" - Skipping SSRF (no URL-like parameters found)");
}
}
} else {
info!(" [SKIP] SSRF scanner requires Professional or higher license");
}
}
info!("Phase 2: Security configuration testing");
info!(" - Testing Security Headers");
let (vulns, tests) = engine
.security_headers_scanner
.scan(target, scan_config)
.await?;
all_vulnerabilities.extend(vulns);
total_tests += tests as u64;
info!(" - Testing CORS Configuration");
let (vulns, tests) = engine.cors_scanner.scan(target, scan_config).await?;
all_vulnerabilities.extend(vulns);
total_tests += tests as u64;
if scan_token.is_module_authorized(module_ids::advanced_scanning::CORS_MISCONFIG) {
info!(" - Testing CORS Misconfiguration");
let (vulns, tests) = engine
.cors_misconfiguration_scanner
.scan(target, scan_config)
.await?;
all_vulnerabilities.extend(vulns);
total_tests += tests as u64;
}
if scan_token.is_module_authorized(module_ids::advanced_scanning::CSRF_SCANNER) {
info!(" - Testing CSRF Protection");
let (vulns, tests) = engine.csrf_scanner.scan(target, scan_config).await?;
all_vulnerabilities.extend(vulns);
total_tests += tests as u64;
}
info!(" - Testing Clickjacking Protection");
let (vulns, tests) = engine
.clickjacking_scanner
.scan(target, scan_config)
.await?;
all_vulnerabilities.extend(vulns);
total_tests += tests as u64;
info!("Phase 3: Authentication testing");
if !auth_tests_done {
if scan_token.is_module_authorized(module_ids::advanced_scanning::JWT_SCANNER) {
info!(" - Testing JWT Security");
if let Some(ref token) = scan_config.auth_token {
let (vulns, tests) = engine
.jwt_scanner
.scan_jwt(target, token, scan_config)
.await?;
all_vulnerabilities.extend(vulns);
total_tests += tests as u64;
}
info!(" - Testing JWT Vulnerabilities");
let (vulns, tests) = engine
.jwt_vulnerabilities_scanner
.scan(target, scan_config)
.await?;
all_vulnerabilities.extend(vulns);
total_tests += tests as u64;
}
if scan_token.is_module_authorized(module_ids::advanced_scanning::AUTH_BYPASS) {
info!(" - Testing Authentication Bypass");
let (vulns, tests) = engine.auth_bypass_scanner.scan(target, scan_config).await?;
all_vulnerabilities.extend(vulns);
total_tests += tests as u64;
}
if scan_token.is_module_authorized(module_ids::advanced_scanning::IDOR_SCANNER) {
info!(" - Testing IDOR");
let (vulns, tests) = engine.idor_scanner.scan(target, scan_config).await?;
all_vulnerabilities.extend(vulns);
total_tests += tests as u64;
}
if scan_token.is_module_authorized(module_ids::advanced_scanning::BOLA_SCANNER) {
info!(" - Testing BOLA");
let (vulns, tests) = engine.bola_scanner.scan(target, scan_config).await?;
all_vulnerabilities.extend(vulns);
total_tests += tests as u64;
}
} else {
info!(" - JWT/Auth Bypass/IDOR/BOLA already tested in early auth phase");
}
if scan_token.is_module_authorized(module_ids::advanced_scanning::OAUTH_SCANNER) {
info!(" - Testing OAuth Security");
let (vulns, tests) = engine.oauth_scanner.scan(target, scan_config).await?;
all_vulnerabilities.extend(vulns);
total_tests += tests as u64;
}
if scan_token.is_module_authorized(module_ids::advanced_scanning::SESSION_MANAGEMENT) {
info!(" - Testing Session Management");
let (vulns, tests) = engine
.session_management_scanner
.scan(target, scan_config)
.await?;
all_vulnerabilities.extend(vulns);
total_tests += tests as u64;
}
if scan_token.is_module_authorized(module_ids::advanced_scanning::ADVANCED_AUTH) {
info!(" - Testing Advanced Authentication");
let (vulns, tests) = engine
.advanced_auth_scanner
.scan(target, scan_config)
.await?;
all_vulnerabilities.extend(vulns);
total_tests += tests as u64;
}
if scan_token.is_module_authorized(module_ids::advanced_scanning::AUTH_MANAGER) {
info!(" - Testing Authentication Management");
let (vulns, tests) = engine
.auth_manager_scanner
.scan(target, scan_config)
.await?;
all_vulnerabilities.extend(vulns);
total_tests += tests as u64;
}
if scan_token.is_module_authorized(module_ids::advanced_scanning::MFA_SCANNER) {
info!(" - Testing MFA Security");
let (vulns, tests) = engine.mfa_scanner.scan(target, scan_config).await?;
all_vulnerabilities.extend(vulns);
total_tests += tests as u64;
}
if scan_token.is_module_authorized(module_ids::advanced_scanning::SAML_SCANNER) {
info!(" - Testing SAML Security");
let (vulns, tests) = engine.saml_scanner.scan(target, scan_config).await?;
all_vulnerabilities.extend(vulns);
total_tests += tests as u64;
}
if scan_token.is_module_authorized(module_ids::advanced_scanning::WEBAUTHN_SCANNER) {
info!(" - Testing WebAuthn/FIDO2 Security");
let (vulns, tests) = engine.webauthn_scanner.scan(target, scan_config).await?;
all_vulnerabilities.extend(vulns);
total_tests += tests as u64;
}
info!("Phase 4: API security testing");
if !auth_tests_done {
if scan_token.is_module_authorized(module_ids::advanced_scanning::GRAPHQL_SCANNER) {
info!(" - Testing GraphQL Security");
let (vulns, tests) = engine.graphql_scanner.scan(target, scan_config).await?;
all_vulnerabilities.extend(vulns);
total_tests += tests as u64;
info!(" - Testing Advanced GraphQL Security");
let (vulns, tests) = engine
.graphql_security_scanner
.scan(target, scan_config)
.await?;
all_vulnerabilities.extend(vulns);
total_tests += tests as u64;
}
if scan_token.is_module_authorized(module_ids::advanced_scanning::API_SECURITY) {
info!(" - Testing API Security");
let (vulns, tests) = engine
.api_security_scanner
.scan(target, scan_config)
.await?;
all_vulnerabilities.extend(vulns);
total_tests += tests as u64;
}
} else {
info!(" - GraphQL/API Security already tested in early auth phase");
}
if scan_token.is_module_authorized(module_ids::advanced_scanning::GRPC_SCANNER) {
info!(" - Testing gRPC Security");
let (vulns, tests) = engine.grpc_scanner.scan(target, scan_config).await?;
all_vulnerabilities.extend(vulns);
total_tests += tests as u64;
}
if scan_token.is_module_authorized(module_ids::advanced_scanning::API_FUZZER)
&& (!js_miner_results.api_endpoints.is_empty()
|| !js_miner_results.graphql_endpoints.is_empty())
{
info!(
" - Running Advanced API Fuzzing on {} discovered endpoints",
js_miner_results.api_endpoints.len() + js_miner_results.graphql_endpoints.len()
);
for api_url in &js_miner_results.api_endpoints {
let (vulns, tests) = engine.api_fuzzer_scanner.scan(api_url, scan_config).await?;
all_vulnerabilities.extend(vulns);
total_tests += tests as u64;
}
for gql_url in &js_miner_results.graphql_endpoints {
info!(" - Testing GraphQL injection on: {}", gql_url);
let (vulns, tests) = engine.api_fuzzer_scanner.scan(gql_url, scan_config).await?;
all_vulnerabilities.extend(vulns);
total_tests += tests as u64;
}
}
info!("Phase 5: Advanced injection testing");
if scan_token.is_module_authorized(module_ids::advanced_scanning::XXE_SCANNER) {
if has_real_params && !is_static_site && !is_nodejs_stack {
info!(" - Testing XXE");
for (param_name, _) in &test_params {
let (vulns, tests) = engine
.xxe_scanner
.scan_parameter(target, param_name, scan_config)
.await?;
all_vulnerabilities.extend(vulns);
total_tests += tests as u64;
}
} else {
info!(" - Skipping XXE (not applicable for detected stack)");
}
}
if scan_token.is_module_authorized(module_ids::advanced_scanning::SSTI_SCANNER) {
if is_python_stack || is_php_stack || is_java_stack {
info!(" - Testing Template Injection (SSTI)");
let (vulns, tests) = engine
.template_injection_scanner
.scan(target, scan_config)
.await?;
all_vulnerabilities.extend(vulns);
total_tests += tests as u64;
if scan_token.is_module_authorized(module_ids::advanced_scanning::SSTI_ADVANCED) {
info!(" - Testing Advanced SSTI");
let (vulns, tests) = engine
.ssti_advanced_scanner
.scan(target, scan_config)
.await?;
all_vulnerabilities.extend(vulns);
total_tests += tests as u64;
}
} else {
info!(" - Skipping SSTI (not applicable for Next.js/React stack)");
}
}
if scan_token.is_module_authorized(module_ids::advanced_scanning::NOSQL_SCANNER) {
if has_real_params && !is_static_site {
info!(" - Testing NoSQL Injection");
for (param_name, _) in &test_params {
let (vulns, tests) = engine
.nosql_scanner
.scan_parameter(target, param_name, scan_config)
.await?;
all_vulnerabilities.extend(vulns);
total_tests += tests as u64;
}
info!(" - Testing Advanced NoSQL Injection");
let (vulns, tests) = engine
.nosql_injection_scanner
.scan(target, scan_config)
.await?;
all_vulnerabilities.extend(vulns);
total_tests += tests as u64;
} else {
info!(" - Skipping NoSQL Injection (no parameters or static site)");
}
}
if scan_token.is_module_authorized(module_ids::advanced_scanning::LDAP_INJECTION) {
if is_java_stack || (is_php_stack && !is_nodejs_stack) {
info!(" - Testing LDAP Injection");
let (vulns, tests) = engine
.ldap_injection_scanner
.scan(target, scan_config)
.await?;
all_vulnerabilities.extend(vulns);
total_tests += tests as u64;
} else {
info!(" - Skipping LDAP Injection (not applicable for detected stack)");
}
}
if scan_token.is_module_authorized(module_ids::advanced_scanning::CODE_INJECTION) {
if is_php_stack || is_python_stack {
info!(" - Testing Code Injection");
let (vulns, tests) = engine
.code_injection_scanner
.scan(target, scan_config)
.await?;
all_vulnerabilities.extend(vulns);
total_tests += tests as u64;
if scan_token.is_module_authorized(module_ids::advanced_scanning::SSI_INJECTION) {
info!(" - Testing SSI Injection");
let (vulns, tests) = engine
.ssi_injection_scanner
.scan(target, scan_config)
.await?;
all_vulnerabilities.extend(vulns);
total_tests += tests as u64;
}
} else {
info!(" - Skipping Code Injection (not applicable for detected stack)");
}
}
if scan_token.is_module_authorized(module_ids::advanced_scanning::XML_INJECTION)
&& has_real_params
&& !is_static_site
&& !is_nodejs_stack
{
info!(" - Testing XML Injection");
let (vulns, tests) = engine
.xml_injection_scanner
.scan(target, scan_config)
.await?;
all_vulnerabilities.extend(vulns);
total_tests += tests as u64;
if scan_token.is_module_authorized(module_ids::advanced_scanning::XPATH_INJECTION) {
info!(" - Testing XPath Injection");
let (vulns, tests) = engine
.xpath_injection_scanner
.scan(target, scan_config)
.await?;
all_vulnerabilities.extend(vulns);
total_tests += tests as u64;
}
}
if scan_token.is_module_authorized(module_ids::advanced_scanning::DESERIALIZATION) {
if is_php_stack || is_java_stack {
info!(" - Testing Insecure Deserialization");
let (vulns, tests) = engine
.deserialization_scanner
.scan(target, scan_config)
.await?;
all_vulnerabilities.extend(vulns);
total_tests += tests as u64;
} else {
info!(" - Skipping Deserialization (not applicable for Node.js/Next.js stack)");
}
}
if scan_token.is_module_authorized(module_ids::advanced_scanning::REDOS_SCANNER)
&& has_real_params
&& !is_static_site
{
info!(" - Testing ReDoS (Regular Expression Denial of Service)");
for (param_name, _) in &test_params {
let (vulns, tests) = engine
.redos_scanner
.scan_parameter(target, param_name, scan_config)
.await?;
all_vulnerabilities.extend(vulns);
total_tests += tests as u64;
}
}
if scan_token.is_module_authorized(module_ids::advanced_scanning::EMAIL_HEADER_INJECTION)
&& has_real_params
&& !is_static_site
{
info!(" - Testing Email Header Injection");
let (vulns, tests) = engine
.email_header_injection_scanner
.scan(target, scan_config)
.await?;
all_vulnerabilities.extend(vulns);
total_tests += tests as u64;
}
info!("Phase 6: Protocol testing");
if scan_token.is_module_authorized(module_ids::advanced_scanning::HTTP_SMUGGLING) {
info!(" - Testing HTTP Smuggling");
let (vulns, tests) = engine
.http_smuggling_scanner
.scan(target, scan_config)
.await?;
all_vulnerabilities.extend(vulns);
total_tests += tests as u64;
}
if scan_token.is_module_authorized(module_ids::advanced_scanning::WEBSOCKET_SCANNER) {
info!(" - Testing WebSocket Security");
let (vulns, tests) = engine.websocket_scanner.scan(target, scan_config).await?;
all_vulnerabilities.extend(vulns);
total_tests += tests as u64;
}
if scan_token.is_module_authorized(module_ids::advanced_scanning::CRLF_INJECTION) {
info!(" - Testing CRLF Injection");
let (vulns, tests) = engine
.crlf_injection_scanner
.scan(target, scan_config)
.await?;
all_vulnerabilities.extend(vulns);
total_tests += tests as u64;
}
if scan_token.is_module_authorized(module_ids::advanced_scanning::HOST_HEADER_INJECTION) {
info!(" - Testing Host Header Injection");
let (vulns, tests) = engine
.host_header_injection_scanner
.scan(target, scan_config)
.await?;
all_vulnerabilities.extend(vulns);
total_tests += tests as u64;
}
info!("Phase 7: Business logic testing");
if scan_token.is_module_authorized(module_ids::advanced_scanning::RACE_CONDITION) {
info!(" - Testing Race Conditions");
let (vulns, tests) = engine
.race_condition_scanner
.scan(target, scan_config)
.await?;
all_vulnerabilities.extend(vulns);
total_tests += tests as u64;
}
if scan_token.is_module_authorized(module_ids::advanced_scanning::MASS_ASSIGNMENT) {
info!(" - Testing Mass Assignment");
let (vulns, tests) = engine
.mass_assignment_scanner
.scan(target, scan_config)
.await?;
all_vulnerabilities.extend(vulns);
total_tests += tests as u64;
}
if scan_token.is_module_authorized(module_ids::advanced_scanning::FILE_UPLOAD) {
info!(" - Testing File Upload Security");
let (vulns, tests) = engine.file_upload_scanner.scan(target, scan_config).await?;
all_vulnerabilities.extend(vulns);
total_tests += tests as u64;
info!(" - Testing File Upload Vulnerabilities");
let (vulns, tests) = engine
.file_upload_vulnerabilities_scanner
.scan(target, scan_config)
.await?;
all_vulnerabilities.extend(vulns);
total_tests += tests as u64;
}
if scan_token.is_module_authorized(module_ids::advanced_scanning::OPEN_REDIRECT) {
info!(" - Testing Open Redirect");
let (vulns, tests) = engine
.open_redirect_scanner
.scan(target, scan_config)
.await?;
all_vulnerabilities.extend(vulns);
total_tests += tests as u64;
}
info!(" - Testing Information Disclosure");
let (vulns, tests) = engine
.information_disclosure_scanner
.scan(target, scan_config)
.await?;
all_vulnerabilities.extend(vulns);
total_tests += tests as u64;
info!(" - Testing Sensitive Data Exposure");
let (vulns, tests) = engine
.sensitive_data_scanner
.scan(target, scan_config)
.await?;
all_vulnerabilities.extend(vulns);
total_tests += tests as u64;
if scan_token.is_module_authorized(module_ids::advanced_scanning::CACHE_POISONING) {
info!(" - Testing Cache Poisoning");
let (vulns, tests) = engine
.cache_poisoning_scanner
.scan(target, scan_config)
.await?;
all_vulnerabilities.extend(vulns);
total_tests += tests as u64;
}
if scan_token.is_module_authorized(module_ids::advanced_scanning::PROTOTYPE_POLLUTION) {
if is_nodejs_stack {
info!(" - Testing Prototype Pollution (JS-heavy site detected)");
} else {
info!(" - Testing Prototype Pollution");
}
let (vulns, tests) = engine
.prototype_pollution_scanner
.scan(target, scan_config)
.await?;
all_vulnerabilities.extend(vulns);
total_tests += tests as u64;
}
if scan_token.is_module_authorized(module_ids::advanced_scanning::BUSINESS_LOGIC) {
info!(" - Testing Business Logic Flaws");
let (vulns, tests) = engine
.business_logic_scanner
.scan(target, scan_config)
.await?;
all_vulnerabilities.extend(vulns);
total_tests += tests as u64;
}
if scan_token.is_module_authorized(module_ids::advanced_scanning::FRAMEWORK_VULNS) {
info!(" - Testing Framework Vulnerabilities");
let (vulns, tests) = engine
.framework_vulnerabilities_scanner
.scan(target, scan_config)
.await?;
all_vulnerabilities.extend(vulns);
total_tests += tests as u64;
}
if scan_token.is_module_authorized(module_ids::cms_security::WORDPRESS_SCANNER)
&& detected_technologies
.iter()
.any(|t| t.to_lowercase().contains("wordpress"))
{
info!(" - Testing WordPress Security");
let (vulns, tests) = engine
.wordpress_security_scanner
.scan(target, scan_config)
.await?;
all_vulnerabilities.extend(vulns);
total_tests += tests as u64;
}
if scan_token.is_module_authorized(module_ids::cms_security::DRUPAL_SCANNER)
&& detected_technologies
.iter()
.any(|t| t.to_lowercase().contains("drupal"))
{
info!(" - Testing Drupal Security");
let (vulns, tests) = engine
.drupal_security_scanner
.scan(target, scan_config)
.await?;
all_vulnerabilities.extend(vulns);
total_tests += tests as u64;
}
if scan_token.is_module_authorized(module_ids::cms_security::LARAVEL_SCANNER)
&& detected_technologies
.iter()
.any(|t| t.to_lowercase().contains("laravel"))
{
info!(" - Testing Laravel Security");
let (vulns, tests) = engine
.laravel_security_scanner
.scan(target, scan_config)
.await?;
all_vulnerabilities.extend(vulns);
total_tests += tests as u64;
}
if scan_token.is_module_authorized(module_ids::cms_security::DJANGO_SCANNER)
&& detected_technologies
.iter()
.any(|t| t.to_lowercase().contains("django"))
{
info!(" - Testing Django Security");
let (vulns, tests) = engine
.django_security_scanner
.scan(target, scan_config)
.await?;
all_vulnerabilities.extend(vulns);
total_tests += tests as u64;
}
if scan_token.is_module_authorized(module_ids::cms_security::EXPRESS_SCANNER)
&& detected_technologies
.iter()
.any(|t| t.to_lowercase().contains("express"))
{
info!(" - Testing Express.js Security");
let (vulns, tests) = engine
.express_security_scanner
.scan(target, scan_config)
.await?;
all_vulnerabilities.extend(vulns);
total_tests += tests as u64;
}
if scan_token.is_module_authorized(module_ids::cms_security::NEXTJS_SCANNER)
&& detected_technologies
.iter()
.any(|t| t.to_lowercase().contains("next"))
{
info!(" - Testing Next.js Security");
let (vulns, tests) = engine
.nextjs_security_scanner
.scan(target, scan_config)
.await?;
all_vulnerabilities.extend(vulns);
total_tests += tests as u64;
}
if scan_token.is_module_authorized(module_ids::cms_security::SVELTEKIT_SCANNER)
&& detected_technologies
.iter()
.any(|t| t.to_lowercase().contains("svelte"))
{
info!(" - Testing SvelteKit Security");
let (vulns, tests) = engine
.sveltekit_security_scanner
.scan(target, scan_config)
.await?;
all_vulnerabilities.extend(vulns);
total_tests += tests as u64;
}
if scan_token.is_module_authorized(module_ids::cms_security::REACT_SCANNER)
&& detected_technologies
.iter()
.any(|t| t.to_lowercase().contains("react"))
{
info!(" - Testing React Security");
let (vulns, tests) = engine
.react_security_scanner
.scan(target, scan_config)
.await?;
all_vulnerabilities.extend(vulns);
total_tests += tests as u64;
}
if scan_token.is_module_authorized(module_ids::cms_security::LIFERAY_SCANNER)
&& detected_technologies
.iter()
.any(|t| t.to_lowercase().contains("liferay"))
{
info!(" - Testing Liferay Security");
let (vulns, tests) = engine
.liferay_security_scanner
.scan(target, scan_config)
.await?;
all_vulnerabilities.extend(vulns);
total_tests += tests as u64;
}
if scan_token.is_module_authorized(module_ids::cms_security::JOOMLA_SCANNER)
&& detected_technologies
.iter()
.any(|t| t.to_lowercase().contains("joomla"))
{
info!(" - Testing Joomla Security");
let (vulns, tests) = engine.joomla_scanner.scan(target, scan_config).await?;
all_vulnerabilities.extend(vulns);
total_tests += tests as u64;
}
if scan_token.is_module_authorized(module_ids::cms_security::RAILS_SCANNER)
&& detected_technologies.iter().any(|t| {
let t_lower = t.to_lowercase();
t_lower.contains("rails") || t_lower.contains("ruby")
})
{
info!(" - Testing Ruby on Rails Security");
let (vulns, tests) = engine.rails_scanner.scan(target, scan_config).await?;
all_vulnerabilities.extend(vulns);
total_tests += tests as u64;
}
if scan_token.is_module_authorized(module_ids::cms_security::SPRING_SCANNER)
&& (is_java_stack
|| detected_technologies.iter().any(|t| {
let t_lower = t.to_lowercase();
t_lower.contains("spring") || t_lower.contains("java")
}))
{
info!(" - Testing Spring Framework Security");
let (vulns, tests) = engine.spring_scanner.scan(target, scan_config).await?;
all_vulnerabilities.extend(vulns);
total_tests += tests as u64;
}
if scan_token.is_module_authorized(module_ids::cms_security::GO_FRAMEWORKS_SCANNER)
&& detected_technologies.iter().any(|t| {
let t_lower = t.to_lowercase();
t_lower.contains("go")
|| t_lower.contains("gin")
|| t_lower.contains("echo")
|| t_lower.contains("fiber")
|| t_lower.contains("chi")
})
{
info!(" - Testing Go Web Framework Security");
let (vulns, tests) = engine
.go_frameworks_scanner
.scan(target, scan_config)
.await?;
all_vulnerabilities.extend(vulns);
total_tests += tests as u64;
}
if scan_token.is_module_authorized(module_ids::advanced_scanning::TOMCAT_MISCONFIG)
&& (is_java_stack
|| detected_technologies
.iter()
.any(|t| t.to_lowercase().contains("tomcat")))
{
info!(" - Testing Tomcat Misconfigurations");
let (vulns, tests) = engine
.tomcat_misconfig_scanner
.scan(target, scan_config)
.await?;
all_vulnerabilities.extend(vulns);
total_tests += tests as u64;
}
if scan_token.is_module_authorized(module_ids::advanced_scanning::VARNISH_MISCONFIG) {
info!(" - Testing Varnish/Cache Misconfigurations");
let (vulns, tests) = engine
.varnish_misconfig_scanner
.scan(target, scan_config)
.await?;
all_vulnerabilities.extend(vulns);
total_tests += tests as u64;
}
if scan_token.is_module_authorized(module_ids::advanced_scanning::HPP_SCANNER)
&& has_real_params
&& !is_static_site
{
info!(" - Testing HTTP Parameter Pollution");
let (vulns, tests) = engine.hpp_scanner.scan(target, scan_config).await?;
all_vulnerabilities.extend(vulns);
total_tests += tests as u64;
}
if scan_token.is_module_authorized(module_ids::advanced_scanning::WAF_BYPASS) {
info!(" - Testing WAF Bypass Techniques");
let (vulns, tests) = engine.waf_bypass_scanner.scan(target, scan_config).await?;
all_vulnerabilities.extend(vulns);
total_tests += tests as u64;
}
if scan_token.is_module_authorized(module_ids::advanced_scanning::MERLIN_SCANNER) {
let has_js = baseline_response.as_ref().is_some_and(|r| {
r.body.contains("<script")
|| r.body.contains(".js\"")
|| r.body.contains(".js'")
|| r.body.contains("application/javascript")
|| r.body.contains("text/javascript")
});
if has_js || is_spa_detected || is_nodejs_stack {
info!(" - Running Merlin JS Library Vulnerability Scanner");
let (vulns, tests) = engine.merlin_scanner.scan(target, scan_config).await?;
let vuln_count = vulns.len();
all_vulnerabilities.extend(vulns);
total_tests += tests as u64;
if vuln_count > 0 {
info!(
"[SUCCESS] [Merlin] Found {} vulnerable JavaScript libraries",
vuln_count
);
}
} else {
info!(" - Skipping Merlin (no JavaScript detected)");
}
}
if scan_token.is_module_authorized(module_ids::advanced_scanning::JS_SENSITIVE_INFO)
&& is_nodejs_stack
{
info!(" - Scanning JavaScript for Sensitive Information");
let (vulns, tests) = engine
.js_sensitive_info_scanner
.scan(target, scan_config)
.await?;
all_vulnerabilities.extend(vulns);
total_tests += tests as u64;
}
if scan_token.is_module_authorized(module_ids::advanced_scanning::SOURCE_MAP_DETECTION) {
info!(" - Scanning for Exposed Source Maps");
let (vulns, tests) = engine.source_map_scanner.scan(target, scan_config).await?;
all_vulnerabilities.extend(vulns);
total_tests += tests as u64;
}
if scan_token.is_module_authorized(module_ids::advanced_scanning::FAVICON_HASH_DETECTION) {
info!(" - Scanning Favicon for Technology Fingerprinting");
let (vulns, tests) = engine
.favicon_hash_scanner
.scan(target, scan_config)
.await?;
all_vulnerabilities.extend(vulns);
total_tests += tests as u64;
}
if scan_token.is_module_authorized(module_ids::advanced_scanning::RATE_LIMITING) {
info!(" - Testing Rate Limiting");
let (vulns, tests) = engine
.rate_limiting_scanner
.scan(target, scan_config)
.await?;
all_vulnerabilities.extend(vulns);
total_tests += tests as u64;
}
if scan_token.is_module_authorized(module_ids::advanced_scanning::HTTP3_SCANNER) {
info!(" - Testing HTTP/3 (QUIC) Security");
let (vulns, tests) = engine.http3_scanner.scan(target, scan_config).await?;
all_vulnerabilities.extend(vulns);
total_tests += tests as u64;
}
if scan_token.is_module_authorized(module_ids::advanced_scanning::FIREBASE_SCANNER) {
if let Some(ref response) = baseline_response {
if detect_technology("firebase", &response.body, &response.headers) {
info!(" - Testing Firebase Security");
let (vulns, tests) = engine.firebase_scanner.scan(target, scan_config).await?;
all_vulnerabilities.extend(vulns);
total_tests += tests as u64;
} else {
info!(" - Skipping Firebase (not detected)");
}
}
}
if scan_config.enable_cloud_scanning() {
info!("Phase 8: Cloud & Container security");
if scan_token.is_module_authorized(module_ids::cloud_scanning::CLOUD_STORAGE) {
info!(" - Testing Cloud Storage Misconfigurations");
let (vulns, tests) = engine
.cloud_storage_scanner
.scan(target, scan_config)
.await?;
all_vulnerabilities.extend(vulns);
total_tests += tests as u64;
if !js_miner_results.s3_buckets.is_empty() {
let unique_s3_buckets: std::collections::HashSet<String> = js_miner_results
.s3_buckets
.iter()
.filter_map(|url| extract_s3_bucket_url(url))
.collect();
info!(
" - Scanning {} unique S3 buckets (from {} URLs)",
unique_s3_buckets.len(),
js_miner_results.s3_buckets.len()
);
for s3_bucket_url in unique_s3_buckets {
info!(" Scanning S3 bucket: {}", s3_bucket_url);
let (vulns, tests) = engine
.cloud_storage_scanner
.scan(&s3_bucket_url, scan_config)
.await?;
all_vulnerabilities.extend(vulns);
total_tests += tests as u64;
}
}
if !js_miner_results.azure_blobs.is_empty() {
let unique_azure_containers: std::collections::HashSet<String> = js_miner_results
.azure_blobs
.iter()
.filter_map(|url| extract_azure_container_url(url))
.collect();
info!(
" - Scanning {} unique Azure containers (from {} URLs)",
unique_azure_containers.len(),
js_miner_results.azure_blobs.len()
);
for azure_url in unique_azure_containers {
info!(" Scanning Azure container: {}", azure_url);
let (vulns, tests) = engine
.cloud_storage_scanner
.scan(&azure_url, scan_config)
.await?;
all_vulnerabilities.extend(vulns);
total_tests += tests as u64;
}
}
if !js_miner_results.gcs_buckets.is_empty() {
let unique_gcs_buckets: std::collections::HashSet<String> = js_miner_results
.gcs_buckets
.iter()
.filter_map(|url| extract_gcs_bucket_url(url))
.collect();
info!(
" - Scanning {} unique GCS buckets (from {} URLs)",
unique_gcs_buckets.len(),
js_miner_results.gcs_buckets.len()
);
for gcs_url in unique_gcs_buckets {
info!(" Scanning GCS bucket: {}", gcs_url);
let (vulns, tests) = engine
.cloud_storage_scanner
.scan(&gcs_url, scan_config)
.await?;
all_vulnerabilities.extend(vulns);
total_tests += tests as u64;
}
}
}
if scan_token.is_module_authorized(module_ids::cloud_scanning::CONTAINER_SCANNER) {
info!(" - Testing Container Security");
let (vulns, tests) = engine.container_scanner.scan(target, scan_config).await?;
all_vulnerabilities.extend(vulns);
total_tests += tests as u64;
}
if scan_token.is_module_authorized(module_ids::advanced_scanning::API_GATEWAY) {
info!(" - Testing API Gateway Security");
let (vulns, tests) = engine.api_gateway_scanner.scan(target, scan_config).await?;
all_vulnerabilities.extend(vulns);
total_tests += tests as u64;
}
if scan_token.is_module_authorized(module_ids::cloud_scanning::CLOUD_SECURITY) {
info!(" - Testing Cloud Security");
let (vulns, tests) = engine
.cloud_security_scanner
.scan(target, scan_config)
.await?;
all_vulnerabilities.extend(vulns);
total_tests += tests as u64;
}
}
let elapsed = start_time.elapsed();
info!("");
info!(
"Scan completed: {} vulnerabilities, {} tests, {:.2}s",
all_vulnerabilities.len(),
total_tests,
elapsed.as_secs_f64()
);
{
use lonkero_scanner::analysis::{AttackSeverity, KnownVulnerability};
for vuln in &all_vulnerabilities {
let severity = match vuln.severity {
lonkero_scanner::types::Severity::Critical => AttackSeverity::Critical,
lonkero_scanner::types::Severity::High => AttackSeverity::High,
lonkero_scanner::types::Severity::Medium => AttackSeverity::Medium,
lonkero_scanner::types::Severity::Low => AttackSeverity::Low,
lonkero_scanner::types::Severity::Info => AttackSeverity::Low,
};
let known_vuln = KnownVulnerability {
vuln_type: vuln.vuln_type.clone(),
endpoint: vuln.url.clone(),
parameter: vuln.parameter.clone(),
severity,
exploitable: vuln.verified,
payload: Some(vuln.payload.clone()),
notes: vuln.evidence.clone(),
};
attack_planner.update_state(StateUpdate::VulnFound(known_vuln));
}
let achievable_goals = attack_planner.get_achievable_goals();
if !achievable_goals.is_empty() {
info!("");
info!("[Intelligence] Attack chain analysis:");
for goal in &achievable_goals {
if let Some(plan) = attack_planner.plan_attack(goal.clone()) {
info!(
" - {:?}: {} steps (success probability: {:.0}%)",
goal,
plan.steps.len(),
plan.estimated_success * 100.0
);
for (i, step) in plan.steps.iter().enumerate() {
debug!(" {}. {}", i + 1, step.name);
}
}
}
}
let accumulated = intelligence_bus.get_accumulated().await;
if !accumulated.frameworks.is_empty() || accumulated.waf_info.is_some() {
info!("");
info!("[Intelligence] Accumulated intelligence:");
if !accumulated.frameworks.is_empty() {
let frameworks: Vec<String> = accumulated
.frameworks
.iter()
.map(|(name, ver, _)| {
if let Some(v) = ver {
format!("{} {}", name, v)
} else {
name.clone()
}
})
.collect();
info!(" - Frameworks: {}", frameworks.join(", "));
}
if let Some((waf, hints)) = &accumulated.waf_info {
info!(" - WAF detected: {} ({} bypass hints)", waf, hints.len());
}
if !accumulated.auth_types.is_empty() {
info!(
" - Auth types: {:?}",
accumulated
.auth_types
.iter()
.map(|(t, _, _)| t)
.collect::<Vec<_>>()
);
}
}
}
let original_count = all_vulnerabilities.len();
let deduplicator = VulnerabilityDeduplicator::new();
let all_vulnerabilities = deduplicator.deduplicate_aggressive(all_vulnerabilities);
let deduped_count = all_vulnerabilities.len();
if original_count != deduped_count {
info!(
"Deduplication: {} -> {} vulnerabilities ({} duplicates removed)",
original_count,
deduped_count,
original_count - deduped_count
);
}
let mut results = ScanResults {
scan_id: job.scan_id.clone(),
target: target.clone(),
tests_run: total_tests,
vulnerabilities: all_vulnerabilities,
started_at,
completed_at: chrono::Utc::now().to_rfc3339(),
duration_seconds: elapsed.as_secs_f64(),
early_terminated: false,
termination_reason: None,
scanner_version: Some(env!("CARGO_PKG_VERSION").to_string()),
license_signature: Some(license::get_license_signature()),
quantum_signature: None,
authorization_token_id: Some(scan_token.token.clone()),
};
let results_hash = signing::hash_results(&results)
.map_err(|e| anyhow::anyhow!("Failed to hash results: {}", e))?;
let findings_summary = signing::FindingsSummary::from_vulnerabilities(&results.vulnerabilities);
match signing::sign_results(
&results_hash,
&scan_token,
vec![],
Some(signing::ScanMetadata {
targets_count: Some(1),
scanner_version: Some(env!("CARGO_PKG_VERSION").to_string()),
scan_duration_ms: Some(elapsed.as_millis() as u64),
}),
Some(findings_summary),
Some(vec![target.to_string()]),
)
.await
{
Ok(signature) => {
info!(
"[SIGNED] Results signed with algorithm: {}",
signature.algorithm
);
results.quantum_signature = Some(signature);
}
Err(SigningError::ServerUnreachable(msg)) => {
error!("Failed to sign results - server unreachable: {}", msg);
error!("Results cannot be verified without server signature.");
return Err(anyhow::anyhow!("Signing server unreachable: {}", msg));
}
Err(e) => {
error!("Failed to sign results: {}", e);
return Err(anyhow::anyhow!("Failed to sign results: {}", e));
}
}
Ok(results)
}
fn print_banner() {
print!("\x1b[96m\x1b[1m");
println!(" __ __");
println!(" / / ____ ____ / /_____ _________");
println!(" / / / __ \\/ __ \\/ //_/ _ \\/ ___/ __ \\");
println!(" / /___/ /_/ / / / / ,< / __/ / / /_/ /");
println!("/_____/\\____/_/ /_/_/|_|\\___/_/ \\____/");
print!("\x1b[0m");
println!();
print!("\x1b[97m");
println!(" Wraps around your attack surface");
print!("\x1b[0m\x1b[90m");
println!(" v3.6 - Intelligent Mode - (c) 2026 Bountyy Oy");
print!("\x1b[0m");
println!();
}
fn print_vulnerability_summary(results: &ScanResults) {
use lonkero_scanner::types::Severity;
let critical = results
.vulnerabilities
.iter()
.filter(|v| v.severity == Severity::Critical)
.count();
let high = results
.vulnerabilities
.iter()
.filter(|v| v.severity == Severity::High)
.count();
let medium = results
.vulnerabilities
.iter()
.filter(|v| v.severity == Severity::Medium)
.count();
let low = results
.vulnerabilities
.iter()
.filter(|v| v.severity == Severity::Low)
.count();
let info = results
.vulnerabilities
.iter()
.filter(|v| v.severity == Severity::Info)
.count();
println!();
println!("{}", "-".repeat(60));
println!("VULNERABILITIES FOUND: {}", results.vulnerabilities.len());
println!("{}", "-".repeat(60));
if critical > 0 {
println!(" [CRITICAL] {}", critical);
}
if high > 0 {
println!(" [HIGH] {}", high);
}
if medium > 0 {
println!(" [MEDIUM] {}", medium);
}
if low > 0 {
println!(" [LOW] {}", low);
}
if info > 0 {
println!(" [INFO] {}", info);
}
for vuln in &results.vulnerabilities {
let severity_str = match vuln.severity {
Severity::Critical => "[CRITICAL]",
Severity::High => "[HIGH] ",
Severity::Medium => "[MEDIUM] ",
Severity::Low => "[LOW] ",
Severity::Info => "[INFO] ",
};
println!();
println!("{} {}", severity_str, vuln.vuln_type);
println!(" URL: {}", vuln.url);
if let Some(param) = &vuln.parameter {
println!(" Parameter: {}", param);
}
println!(" CWE: {}", vuln.cwe);
println!(" CVSS: {:.1}", vuln.cvss);
}
println!("{}", "-".repeat(60));
}
fn write_results(results: &[ScanResults], path: &PathBuf, format: OutputFormat) -> Result<()> {
match format {
OutputFormat::Pdf => {
let pdf_data = generate_pdf_report(results)?;
std::fs::write(path, pdf_data)?;
}
OutputFormat::Xlsx => {
let xlsx_data = generate_xlsx_report(results)?;
std::fs::write(path, xlsx_data)?;
}
_ => {
let content = match format {
OutputFormat::Json => serde_json::to_string_pretty(results)?,
OutputFormat::Html => generate_html_report(results)?,
OutputFormat::Markdown => generate_markdown_report(results)?,
OutputFormat::Sarif => generate_sarif_report(results)?,
OutputFormat::Csv => generate_csv_report(results)?,
OutputFormat::Junit => generate_junit_report(results)?,
_ => serde_json::to_string_pretty(results)?, };
std::fs::write(path, content)?;
}
}
Ok(())
}
fn html_escape(s: &str) -> String {
s.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
.replace('"', """)
.replace('\'', "'")
}
fn generate_html_report(results: &[ScanResults]) -> Result<String> {
let mut html = String::from(
r#"<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Lonkero Security Scan Report</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
:root {
--neon-green: #39ff14;
--neon-green-dim: rgba(57, 255, 20, 0.3);
--neon-glow: 0 0 20px rgba(57, 255, 20, 0.4), 0 0 40px rgba(57, 255, 20, 0.2);
--bg-dark: #0a0a0a;
--bg-darker: #050505;
--bg-card: #0f0f0f;
--bg-card-alt: #141414;
--border-color: #1a1a1a;
--border-glow: #39ff14;
--text-primary: #e0e0e0;
--text-secondary: #666666;
--critical: #ff3366;
--critical-bg: rgba(255, 51, 102, 0.15);
--high: #ff6b35;
--high-bg: rgba(255, 107, 53, 0.15);
--medium: #ffcc00;
--medium-bg: rgba(255, 204, 0, 0.15);
--low: #00b4d8;
--low-bg: rgba(0, 180, 216, 0.15);
--info: #6c757d;
--info-bg: rgba(108, 117, 125, 0.15);
}
* { box-sizing: border-box; margin: 0; padding: 0; }
html { scroll-behavior: smooth; }
body {
font-family: 'JetBrains Mono', monospace;
background: var(--bg-dark);
background-image:
radial-gradient(ellipse at top, rgba(57, 255, 20, 0.03) 0%, transparent 50%),
radial-gradient(ellipse at bottom, rgba(57, 255, 20, 0.02) 0%, transparent 50%);
color: var(--text-primary);
line-height: 1.7;
min-height: 100vh;
padding: 40px 20px;
}
.container {
max-width: 1400px;
margin: 0 auto;
}
.header {
background: linear-gradient(135deg, var(--bg-card) 0%, var(--bg-darker) 100%);
border: 1px solid var(--border-color);
border-radius: 16px;
padding: 40px 50px;
margin-bottom: 30px;
position: relative;
overflow: hidden;
}
.header::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 2px;
background: linear-gradient(90deg, transparent, var(--neon-green), transparent);
opacity: 0.8;
}
.header-content {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 20px;
}
.brand {
display: flex;
align-items: center;
gap: 20px;
}
.logo {
height: 45px;
filter: drop-shadow(0 0 8px rgba(57, 255, 20, 0.3));
}
.title-group h1 {
font-size: 2em;
font-weight: 700;
color: var(--neon-green);
text-shadow: var(--neon-glow);
letter-spacing: -0.5px;
}
.title-group .subtitle {
color: var(--text-secondary);
font-size: 0.85em;
margin-top: 4px;
}
.scan-meta {
display: flex;
gap: 30px;
font-size: 0.8em;
color: var(--text-secondary);
}
.scan-meta-item {
display: flex;
flex-direction: column;
gap: 4px;
}
.scan-meta-label { color: var(--text-secondary); font-size: 0.9em; }
.scan-meta-value { color: var(--neon-green); font-weight: 500; }
/* Stats Cards */
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 16px;
margin: 30px 0;
}
.stat-card {
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: 12px;
padding: 24px;
text-align: center;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
.stat-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
}
.stat-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 3px;
border-radius: 12px 12px 0 0;
}
.stat-card.critical::before { background: var(--critical); box-shadow: 0 0 15px var(--critical); }
.stat-card.high::before { background: var(--high); box-shadow: 0 0 15px var(--high); }
.stat-card.medium::before { background: var(--medium); box-shadow: 0 0 15px var(--medium); }
.stat-card.low::before { background: var(--low); box-shadow: 0 0 15px var(--low); }
.stat-card.info::before { background: var(--info); }
.stat-number {
font-size: 2.5em;
font-weight: 700;
display: block;
margin-bottom: 8px;
}
.stat-card.critical .stat-number { color: var(--critical); text-shadow: 0 0 20px var(--critical); }
.stat-card.high .stat-number { color: var(--high); text-shadow: 0 0 20px var(--high); }
.stat-card.medium .stat-number { color: var(--medium); text-shadow: 0 0 20px var(--medium); }
.stat-card.low .stat-number { color: var(--low); text-shadow: 0 0 20px var(--low); }
.stat-card.info .stat-number { color: var(--info); }
.stat-label {
font-size: 0.85em;
color: var(--text-secondary);
text-transform: uppercase;
letter-spacing: 1px;
}
/* Section */
.section {
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: 16px;
padding: 30px;
margin-bottom: 24px;
}
.section-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 1px solid var(--border-color);
}
.section-title {
font-size: 1.2em;
font-weight: 600;
color: var(--neon-green);
display: flex;
align-items: center;
gap: 10px;
}
.target-badge {
background: var(--bg-darker);
border: 1px solid var(--neon-green);
color: var(--neon-green);
padding: 8px 16px;
border-radius: 8px;
font-size: 0.85em;
font-weight: 500;
}
.meta-info {
display: flex;
gap: 24px;
font-size: 0.8em;
color: var(--text-secondary);
margin-bottom: 20px;
}
.meta-info span { color: var(--neon-green); }
/* Vulnerability Cards */
.vuln-list { display: flex; flex-direction: column; gap: 12px; }
.vuln-card {
background: var(--bg-darker);
border: 1px solid var(--border-color);
border-radius: 12px;
overflow: hidden;
transition: all 0.2s ease;
}
.vuln-card:hover {
border-color: var(--neon-green-dim);
box-shadow: 0 0 20px rgba(57, 255, 20, 0.08);
}
.vuln-header {
padding: 16px 20px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
cursor: pointer;
border-left: 4px solid transparent;
}
.vuln-critical .vuln-header { border-left-color: var(--critical); background: var(--critical-bg); }
.vuln-high .vuln-header { border-left-color: var(--high); background: var(--high-bg); }
.vuln-medium .vuln-header { border-left-color: var(--medium); background: var(--medium-bg); }
.vuln-low .vuln-header { border-left-color: var(--low); background: var(--low-bg); }
.vuln-info .vuln-header { border-left-color: var(--info); background: var(--info-bg); }
.vuln-title-row {
display: flex;
align-items: center;
gap: 12px;
flex: 1;
}
.severity-badge {
padding: 4px 10px;
border-radius: 6px;
font-size: 0.7em;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.vuln-critical .severity-badge { background: var(--critical); color: #fff; }
.vuln-high .severity-badge { background: var(--high); color: #fff; }
.vuln-medium .severity-badge { background: var(--medium); color: #000; }
.vuln-low .severity-badge { background: var(--low); color: #fff; }
.vuln-info .severity-badge { background: var(--info); color: #fff; }
.vuln-type {
font-weight: 600;
font-size: 0.95em;
color: var(--text-primary);
}
.vuln-body {
padding: 20px 24px;
background: var(--bg-card);
border-top: 1px solid var(--border-color);
}
.vuln-details {
display: grid;
gap: 16px;
}
.detail-row {
display: grid;
grid-template-columns: 120px 1fr;
gap: 16px;
font-size: 0.85em;
}
.detail-label {
color: var(--text-secondary);
font-weight: 500;
}
.detail-value {
color: var(--text-primary);
word-break: break-word;
}
.detail-value code {
background: var(--bg-darker);
padding: 4px 8px;
border-radius: 4px;
font-size: 0.9em;
color: var(--neon-green);
border: 1px solid var(--border-color);
}
.cvss-score {
display: inline-flex;
align-items: center;
gap: 8px;
}
.cvss-value {
background: var(--bg-darker);
padding: 4px 10px;
border-radius: 6px;
font-weight: 600;
}
/* PoC Code Blocks */
.poc-code, .evidence-code {
background: var(--bg-darker);
border: 1px solid var(--border-color);
border-left: 3px solid var(--neon-green);
border-radius: 6px;
padding: 16px;
font-family: 'JetBrains Mono', monospace;
font-size: 0.85em;
color: var(--neon-green);
overflow-x: auto;
white-space: pre-wrap;
word-wrap: break-word;
margin: 0;
max-height: 300px;
overflow-y: auto;
}
.evidence-code {
border-left-color: var(--medium);
color: var(--text-primary);
}
/* Footer */
footer {
text-align: center;
padding: 40px 20px;
color: var(--text-secondary);
font-size: 0.85em;
}
footer a {
color: var(--neon-green);
text-decoration: none;
font-weight: 500;
transition: text-shadow 0.2s ease;
}
footer a:hover {
text-shadow: 0 0 10px var(--neon-green);
}
footer .footer-brand {
font-size: 1.1em;
margin-bottom: 8px;
}
/* Animations */
@keyframes glow-pulse {
0%, 100% { opacity: 0.8; }
50% { opacity: 1; }
}
/* Responsive */
@media (max-width: 768px) {
body { padding: 20px 10px; }
.header { padding: 24px; }
.header-content { flex-direction: column; align-items: flex-start; }
.title-group h1 { font-size: 1.5em; }
.stats-grid { grid-template-columns: repeat(2, 1fr); }
.detail-row { grid-template-columns: 1fr; gap: 4px; }
.scan-meta { flex-wrap: wrap; gap: 16px; }
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<div class="header-content">
<div class="brand">
<img src="https://bountyyfi.s3.eu-north-1.amazonaws.com/bountyy.fi-2.png" alt="Bountyy" class="logo">
<div class="title-group">
<h1>Lonkero</h1>
<div class="subtitle">Security Scan Report</div>
</div>
</div>
</div>
</div>
"#,
);
for result in results {
let critical = result
.vulnerabilities
.iter()
.filter(|v| v.severity == lonkero_scanner::types::Severity::Critical)
.count();
let high = result
.vulnerabilities
.iter()
.filter(|v| v.severity == lonkero_scanner::types::Severity::High)
.count();
let medium = result
.vulnerabilities
.iter()
.filter(|v| v.severity == lonkero_scanner::types::Severity::Medium)
.count();
let low = result
.vulnerabilities
.iter()
.filter(|v| v.severity == lonkero_scanner::types::Severity::Low)
.count();
let info = result
.vulnerabilities
.iter()
.filter(|v| v.severity == lonkero_scanner::types::Severity::Info)
.count();
html.push_str(&format!(r#"
<div class="section">
<div class="section-header">
<h2 class="section-title">Target</h2>
<span class="target-badge">{}</span>
</div>
<div class="stats-grid">
<div class="stat-card critical">
<span class="stat-number">{}</span>
<span class="stat-label">Critical</span>
</div>
<div class="stat-card high">
<span class="stat-number">{}</span>
<span class="stat-label">High</span>
</div>
<div class="stat-card medium">
<span class="stat-number">{}</span>
<span class="stat-label">Medium</span>
</div>
<div class="stat-card low">
<span class="stat-number">{}</span>
<span class="stat-label">Low</span>
</div>
<div class="stat-card info">
<span class="stat-number">{}</span>
<span class="stat-label">Info</span>
</div>
</div>
<div class="meta-info">
Tests: <span>{}</span> | Duration: <span>{:.2}s</span> | Completed: <span>{}</span>
</div>
<div class="vuln-list">
"#, html_escape(&result.target), critical, high, medium, low, info, result.tests_run, result.duration_seconds, html_escape(&result.completed_at)));
for vuln in &result.vulnerabilities {
let severity_class = match vuln.severity {
lonkero_scanner::types::Severity::Critical => "vuln-critical",
lonkero_scanner::types::Severity::High => "vuln-high",
lonkero_scanner::types::Severity::Medium => "vuln-medium",
lonkero_scanner::types::Severity::Low => "vuln-low",
lonkero_scanner::types::Severity::Info => "vuln-info",
};
let severity_label = match vuln.severity {
lonkero_scanner::types::Severity::Critical => "Critical",
lonkero_scanner::types::Severity::High => "High",
lonkero_scanner::types::Severity::Medium => "Medium",
lonkero_scanner::types::Severity::Low => "Low",
lonkero_scanner::types::Severity::Info => "Info",
};
let poc_section = if !vuln.payload.is_empty() {
format!(
r#"
<div class="detail-row">
<span class="detail-label">PoC Payload</span>
<span class="detail-value"><pre class="poc-code">{}</pre></span>
</div>"#,
html_escape(&vuln.payload)
)
} else {
String::new()
};
let evidence_section = if let Some(ref evidence) = vuln.evidence {
if !evidence.is_empty() {
format!(
r#"
<div class="detail-row">
<span class="detail-label">Evidence</span>
<span class="detail-value"><pre class="evidence-code">{}</pre></span>
</div>"#,
html_escape(evidence)
)
} else {
String::new()
}
} else {
String::new()
};
html.push_str(&format!(r#"
<div class="vuln-card {}">
<div class="vuln-header">
<div class="vuln-title-row">
<span class="severity-badge">{}</span>
<span class="vuln-type">{}</span>
</div>
</div>
<div class="vuln-body">
<div class="vuln-details">
<div class="detail-row">
<span class="detail-label">URL</span>
<span class="detail-value"><code>{}</code></span>
</div>
<div class="detail-row">
<span class="detail-label">Parameter</span>
<span class="detail-value">{}</span>
</div>
<div class="detail-row">
<span class="detail-label">CWE</span>
<span class="detail-value">{}</span>
</div>
<div class="detail-row">
<span class="detail-label">CVSS</span>
<span class="detail-value cvss-score"><span class="cvss-value">{:.1}</span></span>
</div>{}{}
<div class="detail-row">
<span class="detail-label">Description</span>
<span class="detail-value">{}</span>
</div>
<div class="detail-row">
<span class="detail-label">Remediation</span>
<span class="detail-value">{}</span>
</div>
</div>
</div>
</div>
"#, severity_class, severity_label, html_escape(&vuln.vuln_type), html_escape(&vuln.url), html_escape(vuln.parameter.as_deref().unwrap_or("-")), html_escape(&vuln.cwe), vuln.cvss, poc_section, evidence_section, html_escape(&vuln.description), html_escape(&vuln.remediation)));
}
html.push_str(
r#"
</div>
</div>
"#,
);
}
let current_year = chrono::Utc::now().format("%Y");
html.push_str(&format!(r#"
<footer>
<div class="footer-brand">
Generated by <a href="https://lonkero.bountyy.fi/en" target="_blank"><strong>Lonkero</strong></a> - Wraps around your attack surface
</div>
<div>© {} <a href="https://bountyy.fi" target="_blank">Bountyy Oy</a> | All rights reserved</div>
</footer>
</div>
</body>
</html>"#, current_year));
Ok(html)
}
fn generate_markdown_report(results: &[ScanResults]) -> Result<String> {
let mut md = String::from("# Lonkero Security Scan Report\n\n");
for result in results {
md.push_str(&format!("## Target: {}\n\n", result.target));
md.push_str(&format!("- **Tests Run:** {}\n", result.tests_run));
md.push_str(&format!(
"- **Duration:** {:.2}s\n",
result.duration_seconds
));
md.push_str(&format!(
"- **Vulnerabilities Found:** {}\n\n",
result.vulnerabilities.len()
));
if !result.vulnerabilities.is_empty() {
md.push_str("### Vulnerabilities\n\n");
md.push_str("| Severity | Type | URL | CWE | CVSS |\n");
md.push_str("|----------|------|-----|-----|------|\n");
for vuln in &result.vulnerabilities {
md.push_str(&format!(
"| {:?} | {} | {} | {} | {:.1} |\n",
vuln.severity, vuln.vuln_type, vuln.url, vuln.cwe, vuln.cvss
));
}
md.push_str("\n---\n\n");
for vuln in &result.vulnerabilities {
md.push_str(&format!(
"#### {} - {:?}\n\n",
vuln.vuln_type, vuln.severity
));
md.push_str(&format!("- **URL:** `{}`\n", vuln.url));
if let Some(param) = &vuln.parameter {
md.push_str(&format!("- **Parameter:** `{}`\n", param));
}
md.push_str(&format!("- **CWE:** {}\n", vuln.cwe));
md.push_str(&format!("- **CVSS:** {:.1}\n\n", vuln.cvss));
md.push_str(&format!("**Description:** {}\n\n", vuln.description));
md.push_str(&format!("**Remediation:** {}\n\n", vuln.remediation));
md.push_str("---\n\n");
}
}
}
let current_year = chrono::Utc::now().format("%Y");
md.push_str(&format!(
"\n---\n*Generated by Lonkero v3.5.0 | (c) {} Bountyy Oy*\n",
current_year
));
Ok(md)
}
fn generate_sarif_report(results: &[ScanResults]) -> Result<String> {
let sarif = serde_json::json!({
"$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json",
"version": "2.1.0",
"runs": results.iter().map(|r| {
serde_json::json!({
"tool": {
"driver": {
"name": "Lonkero",
"version": "3.5.0",
"informationUri": "https://github.com/bountyyfi/lonkero"
}
},
"results": r.vulnerabilities.iter().map(|v| {
serde_json::json!({
"ruleId": v.cwe.clone(),
"level": match v.severity {
lonkero_scanner::types::Severity::Critical | lonkero_scanner::types::Severity::High => "error",
lonkero_scanner::types::Severity::Medium => "warning",
_ => "note"
},
"message": {
"text": v.description.clone()
},
"locations": [{
"physicalLocation": {
"artifactLocation": {
"uri": v.url.clone()
}
}
}]
})
}).collect::<Vec<_>>()
})
}).collect::<Vec<_>>()
});
Ok(serde_json::to_string_pretty(&sarif)?)
}
fn generate_csv_report(results: &[ScanResults]) -> Result<String> {
let mut csv =
String::from("Target,Vulnerability Type,Severity,URL,Parameter,CWE,CVSS,Description\n");
for result in results {
for vuln in &result.vulnerabilities {
csv.push_str(&format!(
"\"{}\",\"{}\",\"{:?}\",\"{}\",\"{}\",\"{}\",\"{:.1}\",\"{}\"\n",
result.target,
vuln.vuln_type,
vuln.severity,
vuln.url,
vuln.parameter.as_deref().unwrap_or(""),
vuln.cwe,
vuln.cvss,
vuln.description.replace("\"", "\"\"")
));
}
}
Ok(csv)
}
fn generate_pdf_report(results: &[ScanResults]) -> Result<Vec<u8>> {
use lonkero_scanner::reporting::formats::pdf::PdfReportGenerator;
use lonkero_scanner::reporting::types::{
BrandingConfig, ComplianceMapping, EnhancedReport, ExecutiveSummary, RiskAssessment,
VulnerabilityBreakdown,
};
use std::collections::HashMap;
let mut all_vulns = Vec::new();
let mut target = String::new();
let mut scan_id = String::new();
for result in results {
if target.is_empty() {
target = result.target.clone();
scan_id = result.scan_id.clone();
}
all_vulns.extend(result.vulnerabilities.clone());
}
let critical_count = all_vulns
.iter()
.filter(|v| matches!(v.severity, lonkero_scanner::types::Severity::Critical))
.count();
let high_count = all_vulns
.iter()
.filter(|v| matches!(v.severity, lonkero_scanner::types::Severity::High))
.count();
let medium_count = all_vulns
.iter()
.filter(|v| matches!(v.severity, lonkero_scanner::types::Severity::Medium))
.count();
let low_count = all_vulns
.iter()
.filter(|v| matches!(v.severity, lonkero_scanner::types::Severity::Low))
.count();
let info_count = all_vulns
.iter()
.filter(|v| matches!(v.severity, lonkero_scanner::types::Severity::Info))
.count();
let risk_score = (critical_count as f64 * 10.0
+ high_count as f64 * 7.0
+ medium_count as f64 * 4.0
+ low_count as f64 * 1.0)
/ 10.0;
let risk_level = if critical_count > 0 {
"Critical"
} else if high_count > 0 {
"High"
} else if medium_count > 0 {
"Medium"
} else if low_count > 0 {
"Low"
} else {
"Info"
};
let scan_results = lonkero_scanner::types::ScanResults {
scan_id: scan_id.clone(),
target: target.clone(),
tests_run: results.iter().map(|r| r.tests_run).sum(),
vulnerabilities: all_vulns,
started_at: results
.first()
.map(|r| r.started_at.clone())
.unwrap_or_default(),
completed_at: results
.last()
.map(|r| r.completed_at.clone())
.unwrap_or_default(),
duration_seconds: results.iter().map(|r| r.duration_seconds).sum(),
early_terminated: false,
termination_reason: None,
scanner_version: Some("3.5.0".to_string()),
license_signature: Some(String::new()),
quantum_signature: None,
authorization_token_id: None,
};
let enhanced_report = EnhancedReport {
scan_results,
executive_summary: ExecutiveSummary {
target: target.clone(),
scan_date: chrono::Utc::now().to_rfc3339(),
total_vulnerabilities: critical_count
+ high_count
+ medium_count
+ low_count
+ info_count,
critical_count,
high_count,
medium_count,
low_count,
info_count,
risk_score,
risk_level: risk_level.to_string(),
key_findings: Vec::new(),
recommendations: Vec::new(),
duration_seconds: results.iter().map(|r| r.duration_seconds).sum(),
},
vulnerability_breakdown: VulnerabilityBreakdown {
by_severity: HashMap::new(),
by_category: HashMap::new(),
by_confidence: HashMap::new(),
verified_count: 0,
unverified_count: 0,
},
owasp_mapping: HashMap::new(),
cwe_mapping: HashMap::new(),
compliance_mapping: ComplianceMapping {
pci_dss: HashMap::new(),
hipaa: HashMap::new(),
soc2: HashMap::new(),
iso27001: HashMap::new(),
gdpr: HashMap::new(),
nist_csf: HashMap::new(),
dora: HashMap::new(),
nis2: HashMap::new(),
},
risk_assessment: RiskAssessment {
overall_risk_score: risk_score,
risk_level: risk_level.to_string(),
risk_matrix: Vec::new(),
attack_surface_score: 0.0,
exploitability_score: 0.0,
business_impact_score: 0.0,
},
trends: None,
generated_at: chrono::Utc::now().to_rfc3339(),
report_version: "1.0".to_string(),
};
let branding = BrandingConfig::default();
let pdf_generator = PdfReportGenerator::new();
let pdf_data = tokio::task::block_in_place(|| {
tokio::runtime::Handle::current()
.block_on(pdf_generator.generate(&enhanced_report, &branding))
})?;
Ok(pdf_data)
}
fn generate_xlsx_report(results: &[ScanResults]) -> Result<Vec<u8>> {
use rust_xlsxwriter::*;
let mut workbook = Workbook::new();
let worksheet = workbook.add_worksheet();
worksheet.set_name("Vulnerabilities")?;
let header_format = Format::new()
.set_bold()
.set_background_color(Color::RGB(0x2563eb));
worksheet.write_with_format(0, 0, "Target", &header_format)?;
worksheet.write_with_format(0, 1, "Type", &header_format)?;
worksheet.write_with_format(0, 2, "Severity", &header_format)?;
worksheet.write_with_format(0, 3, "URL", &header_format)?;
worksheet.write_with_format(0, 4, "Parameter", &header_format)?;
worksheet.write_with_format(0, 5, "Payload", &header_format)?;
worksheet.write_with_format(0, 6, "CWE", &header_format)?;
worksheet.write_with_format(0, 7, "CVSS", &header_format)?;
worksheet.write_with_format(0, 8, "Description", &header_format)?;
worksheet.write_with_format(0, 9, "Remediation", &header_format)?;
let mut row = 1u32;
for result in results {
for vuln in &result.vulnerabilities {
worksheet.write(row, 0, &result.target)?;
worksheet.write(row, 1, &vuln.vuln_type)?;
worksheet.write(row, 2, format!("{:?}", vuln.severity))?;
worksheet.write(row, 3, &vuln.url)?;
worksheet.write(row, 4, vuln.parameter.as_deref().unwrap_or(""))?;
worksheet.write(row, 5, &vuln.payload)?;
worksheet.write(row, 6, &vuln.cwe)?;
worksheet.write(row, 7, vuln.cvss)?;
worksheet.write(row, 8, &vuln.description)?;
worksheet.write(row, 9, &vuln.remediation)?;
row += 1;
}
}
worksheet.set_column_width(0, 30)?;
worksheet.set_column_width(1, 35)?;
worksheet.set_column_width(2, 12)?;
worksheet.set_column_width(3, 50)?;
worksheet.set_column_width(4, 20)?;
worksheet.set_column_width(5, 40)?;
worksheet.set_column_width(6, 12)?;
worksheet.set_column_width(7, 8)?;
worksheet.set_column_width(8, 60)?;
worksheet.set_column_width(9, 60)?;
let temp_path = format!("/tmp/lonkero_report_{}.xlsx", std::process::id());
workbook.save(&temp_path)?;
let data = std::fs::read(&temp_path)?;
let _ = std::fs::remove_file(&temp_path);
Ok(data)
}
fn generate_junit_report(results: &[ScanResults]) -> Result<String> {
let mut total_tests = 0usize;
let mut total_failures = 0usize;
for result in results {
total_tests += result.vulnerabilities.len().max(1);
total_failures += result.vulnerabilities.len();
}
let mut xml = format!(
r#"<?xml version="1.0" encoding="UTF-8"?>
<testsuites name="Lonkero Security Scan" tests="{}" failures="{}" time="{}">
"#,
total_tests,
total_failures,
results.iter().map(|r| r.duration_seconds).sum::<f64>()
);
for result in results {
let failures = result.vulnerabilities.len();
xml.push_str(&format!(
r#" <testsuite name="{}" tests="{}" failures="{}" time="{}">
"#,
xml_escape(&result.target),
result.vulnerabilities.len().max(1),
failures,
result.duration_seconds
));
if result.vulnerabilities.is_empty() {
xml.push_str(&format!(
r#" <testcase name="Security Scan" classname="{}" time="{}" />
"#,
xml_escape(&result.target),
result.duration_seconds
));
} else {
for vuln in &result.vulnerabilities {
xml.push_str(&format!(
r#" <testcase name="{}" classname="{}" time="0">
<failure message="{}" type="{:?}"><![CDATA[
URL: {}
CWE: {}
CVSS: {:.1}
Description: {}
Remediation: {}
]]></failure>
</testcase>
"#,
xml_escape(&vuln.vuln_type),
xml_escape(&result.target),
xml_escape(&vuln.description),
vuln.severity,
vuln.url,
vuln.cwe,
vuln.cvss,
vuln.description,
vuln.remediation
));
}
}
xml.push_str(" </testsuite>\n");
}
xml.push_str("</testsuites>\n");
Ok(xml)
}
fn xml_escape(s: &str) -> String {
s.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
.replace('"', """)
.replace('\'', "'")
}
fn extract_s3_bucket_url(url: &str) -> Option<String> {
if url.contains(".s3.") && url.contains("amazonaws.com") {
if let Some(pos) = url.find("amazonaws.com") {
let base = &url[..pos + "amazonaws.com".len()];
return Some(format!("{}/", base.trim_end_matches('\\')));
}
} else if url.contains("s3.amazonaws.com") || url.contains("s3-") {
if let Ok(parsed) = url::Url::parse(url.trim_end_matches('\\')) {
let host = parsed.host_str()?;
return Some(format!("https://{}/", host));
}
}
None
}
fn extract_azure_container_url(url: &str) -> Option<String> {
if url.contains(".blob.core.windows.net") {
if let Ok(parsed) = url::Url::parse(url.trim_end_matches('\\')) {
let host = parsed.host_str()?;
let path_segments: Vec<&str> =
parsed.path().split('/').filter(|s| !s.is_empty()).collect();
if !path_segments.is_empty() {
return Some(format!("https://{}/{}/", host, path_segments[0]));
} else {
return Some(format!("https://{}/", host));
}
}
}
None
}
fn extract_gcs_bucket_url(url: &str) -> Option<String> {
if url.contains("storage.googleapis.com") || url.contains("storage.cloud.google.com") {
if let Ok(parsed) = url::Url::parse(url.trim_end_matches('\\')) {
let host = parsed.host_str()?;
let path_segments: Vec<&str> =
parsed.path().split('/').filter(|s| !s.is_empty()).collect();
if !path_segments.is_empty() {
return Some(format!("https://{}/{}/", host, path_segments[0]));
} else {
return Some(format!("https://{}/", host));
}
}
}
None
}
fn list_scanners(verbose: bool, category: Option<String>) -> Result<()> {
let scanners = vec![
(
"xss",
"Injection",
"Cross-Site Scripting (XSS) - Reflected, Stored, DOM-based",
),
(
"sqli",
"Injection",
"SQL Injection - Error-based, Blind, Time-based",
),
("command_injection", "Injection", "OS Command Injection"),
("path_traversal", "Injection", "Path/Directory Traversal"),
("ssrf", "Injection", "Server-Side Request Forgery"),
("xxe", "Injection", "XML External Entity Injection"),
("ssti", "Injection", "Server-Side Template Injection"),
("nosql", "Injection", "NoSQL Injection (MongoDB, Redis)"),
("ldap", "Injection", "LDAP Injection"),
(
"code_injection",
"Injection",
"Code Injection (PHP, Python, Ruby)",
),
(
"crlf",
"Injection",
"CRLF Injection / HTTP Response Splitting",
),
("xpath", "Injection", "XPath Injection"),
("xml", "Injection", "XML Injection"),
("ssi", "Injection", "Server-Side Includes Injection"),
(
"security_headers",
"Configuration",
"Missing/Misconfigured Security Headers",
),
("cors", "Configuration", "CORS Misconfiguration"),
("csrf", "Configuration", "Cross-Site Request Forgery"),
(
"clickjacking",
"Configuration",
"Clickjacking / UI Redressing",
),
("jwt", "Authentication", "JWT Security Issues"),
("oauth", "Authentication", "OAuth 2.0 Vulnerabilities"),
("saml", "Authentication", "SAML Security Issues"),
("auth_bypass", "Authentication", "Authentication Bypass"),
("session", "Authentication", "Session Management Issues"),
("mfa", "Authentication", "MFA Bypass/Weaknesses"),
("idor", "Authorization", "Insecure Direct Object References"),
(
"mass_assignment",
"Authorization",
"Mass Assignment Vulnerabilities",
),
("graphql", "API", "GraphQL Security Issues"),
("api_security", "API", "API Security (REST, SOAP)"),
("grpc", "API", "gRPC Security"),
("websocket", "Protocol", "WebSocket Security"),
("http_smuggling", "Protocol", "HTTP Request Smuggling"),
("host_header", "Protocol", "Host Header Injection"),
("http3", "Protocol", "HTTP/3 and QUIC Security"),
("race_condition", "Logic", "Race Condition Vulnerabilities"),
("business_logic", "Logic", "Business Logic Flaws"),
("open_redirect", "Logic", "Open Redirect"),
("file_upload", "Files", "File Upload Vulnerabilities"),
("deserialization", "Files", "Insecure Deserialization"),
("info_disclosure", "Information", "Information Disclosure"),
("sensitive_data", "Information", "Sensitive Data Exposure"),
("js_miner", "Information", "JavaScript Secret Mining"),
("cache_poisoning", "Cache", "Web Cache Poisoning"),
("prototype_pollution", "JavaScript", "Prototype Pollution"),
(
"cloud_storage",
"Cloud",
"Cloud Storage Misconfigurations (S3, GCS, Azure Blob)",
),
("container", "Cloud", "Container Security"),
("api_gateway", "Cloud", "API Gateway Security"),
("aws_ec2", "Cloud", "AWS EC2 Security"),
("aws_s3", "Cloud", "AWS S3 Security"),
("aws_rds", "Cloud", "AWS RDS Security"),
("aws_lambda", "Cloud", "AWS Lambda Security"),
("azure_storage", "Cloud", "Azure Storage Security"),
("azure_vm", "Cloud", "Azure VM Security"),
("gcp_storage", "Cloud", "GCP Storage Security"),
("gcp_compute", "Cloud", "GCP Compute Security"),
(
"framework",
"Framework",
"Framework-Specific Vulnerabilities",
),
("webauthn", "Authentication", "WebAuthn/FIDO2 Security"),
];
println!("Available Scanner Modules ({} total)", scanners.len());
println!("{}", "=".repeat(70));
let filter_category = category.as_ref().map(|c| c.to_lowercase());
for (name, cat, desc) in &scanners {
if let Some(ref filter) = filter_category {
if !cat.to_lowercase().contains(filter) {
continue;
}
}
if verbose {
println!("\n[{}]", name);
println!(" Category: {}", cat);
println!(" Description: {}", desc);
} else {
println!("{:20} {:15} {}", name, cat, desc);
}
}
println!("\n{}", "=".repeat(70));
println!("Use --only or --skip flags to control which scanners run");
Ok(())
}
async fn validate_targets(targets: Vec<String>) -> Result<()> {
println!("Validating {} target(s)...\n", targets.len());
for target in &targets {
print!("{}: ", target);
match url::Url::parse(target) {
Ok(parsed) => {
if parsed.scheme() != "http" && parsed.scheme() != "https" {
println!("INVALID (scheme must be http or https)");
continue;
}
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(10))
.build()?;
match client.get(target).send().await {
Ok(response) => {
println!(
"OK (status: {}, server: {})",
response.status(),
response
.headers()
.get("server")
.map(|v| v.to_str().unwrap_or("unknown"))
.unwrap_or("unknown")
);
}
Err(e) => {
println!("UNREACHABLE ({})", e);
}
}
}
Err(e) => {
println!("INVALID URL ({})", e);
}
}
}
Ok(())
}
fn generate_config(output: PathBuf) -> Result<()> {
let config = r#"# Lonkero Scanner Configuration
# See: https://github.com/bountyyfi/lonkero
[scanner]
# Scan mode: fast, normal, thorough, insane
mode = "normal"
# Maximum concurrent requests
concurrency = 50
# Request timeout in seconds
timeout = 30
# Rate limit (requests per second per target)
rate_limit = 100
# Enable subdomain enumeration
subdomains = false
# Enable web crawler
crawl = false
max_depth = 3
[output]
# Output format: json, html, pdf, sarif, markdown, csv
format = "json"
# Output file path (optional)
# path = "scan-results.json"
[http]
# Custom User-Agent
# user_agent = "Lonkero/1.0"
# Follow redirects
follow_redirects = true
max_redirects = 5
# TLS verification
verify_tls = true
[authentication]
# Authentication cookie
# cookie = "session=abc123"
# Bearer token
# token = "eyJhbGciOiJIUzI1NiIs..."
# HTTP Basic Auth (user:pass)
# basic_auth = "admin:password"
[headers]
# Custom headers
# X-Custom-Header = "value"
# Authorization = "Bearer token"
[scanners]
# Enable/disable specific scanners
# skip = ["grpc", "websocket"]
# only = ["xss", "sqli", "ssrf"]
[proxy]
# Proxy URL
# url = "http://127.0.0.1:8080"
[cloud]
# AWS credentials (for cloud scanning)
# aws_region = "us-east-1"
# aws_profile = "default"
# Azure credentials
# azure_subscription_id = ""
# GCP credentials
# gcp_project_id = ""
"#;
std::fs::write(&output, config)?;
println!("Configuration file generated: {}", output.display());
println!(
"\nEdit this file and run: lonkero scan --config {}",
output.display()
);
Ok(())
}
fn show_version() -> Result<()> {
let current_year = chrono::Utc::now().format("%Y");
println!("Lonkero v3.5.0");
println!("Wraps around your attack surface");
println!();
println!("(c) {} Bountyy Oy", current_year);
println!("https://lonkero.bountyy.fi");
println!();
println!("Build info:");
println!(
" Rust version: {}",
env!("CARGO_PKG_RUST_VERSION")
.chars()
.take(10)
.collect::<String>()
);
println!(" Target: {}", std::env::consts::ARCH);
println!(" OS: {}", std::env::consts::OS);
println!();
println!("Scanner modules: 60+");
println!("Supported outputs: JSON, HTML, PDF, SARIF, Markdown, CSV, XLSX, JUnit");
Ok(())
}