use std::collections::HashSet;
use tracing::info;
use crate::crawler::CrawlResults;
use super::attack_surface::{
AttackSurface, DeduplicatedTargets, ParameterSource as AttackSurfaceParamSource,
};
use super::parameter_prioritizer::{
ParameterInfo, ParameterPrioritizer, ParameterRisk, ParameterSource as PrioritizerParamSource,
};
use super::registry::{PayloadIntensity, ScannerRegistry, ScannerType, TechCategory};
#[derive(Debug, Clone, Default)]
pub struct OrchestrationStats {
pub total_original: usize,
pub total_deduplicated: usize,
pub reduction_percent: f32,
pub high_risk_params: usize,
pub medium_risk_params: usize,
pub low_risk_params: usize,
pub scanners_selected: usize,
pub technologies_detected: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct PrioritizedParameter {
pub name: String,
pub risk_score: u32,
pub intensity: PayloadIntensity,
pub suggested_scanners: Vec<ScannerType>,
pub risk_factors: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct IntelligentScanPlan {
pub scanners: Vec<ScannerType>,
pub targets: DeduplicatedTargets,
pub prioritized_params: Vec<PrioritizedParameter>,
pub technologies: Vec<TechCategory>,
pub stats: OrchestrationStats,
}
pub struct IntelligentScanOrchestrator {
registry: ScannerRegistry,
prioritizer: ParameterPrioritizer,
}
impl Default for IntelligentScanOrchestrator {
fn default() -> Self {
Self::new()
}
}
impl IntelligentScanOrchestrator {
pub fn new() -> Self {
Self {
registry: ScannerRegistry::new(),
prioritizer: ParameterPrioritizer::new(),
}
}
pub fn generate_scan_plan(
&self,
crawl_results: &CrawlResults,
detected_technologies: &[TechCategory],
target_url: &str,
) -> IntelligentScanPlan {
info!("[Orchestrator] Generating intelligent scan plan");
let mut stats = OrchestrationStats::default();
let mut attack_surface = AttackSurface::new();
for endpoint in &crawl_results.api_endpoints {
let params: Vec<String> = if let Ok(parsed) = url::Url::parse(endpoint) {
parsed.query_pairs().map(|(k, _)| k.to_string()).collect()
} else {
Vec::new()
};
attack_surface.add_endpoint(endpoint, "GET", ¶ms);
}
for form in &crawl_results.forms {
attack_surface.add_form(&form.action, &form.method, &form.inputs);
}
for (endpoint, params) in &crawl_results.parameters {
for param in params {
attack_surface.add_parameter(param, AttackSurfaceParamSource::Url, endpoint);
}
}
attack_surface.add_url_parameters(target_url);
let deduplicated = attack_surface.build();
stats.total_original = deduplicated.total_original;
stats.total_deduplicated = deduplicated.total_deduplicated;
stats.reduction_percent = deduplicated.reduction_percent;
info!(
"[Orchestrator] Deduplication: {}/{} targets ({:.1}% reduction)",
stats.total_deduplicated, stats.total_original, stats.reduction_percent
);
let (scanners, _base_intensity) = self.registry.get_intelligent_scan_config(
detected_technologies,
50, );
stats.scanners_selected = scanners.len();
stats.technologies_detected = detected_technologies
.iter()
.map(|t| format!("{:?}", t))
.collect();
info!(
"[Orchestrator] Selected {} scanners for {} technologies",
stats.scanners_selected,
stats.technologies_detected.len()
);
let prioritized_params = self.prioritize_parameters(&deduplicated, detected_technologies);
for param in &prioritized_params {
match param.risk_score {
0..=33 => stats.low_risk_params += 1,
34..=66 => stats.medium_risk_params += 1,
_ => stats.high_risk_params += 1,
}
}
info!(
"[Orchestrator] Parameter risk distribution: {} high, {} medium, {} low",
stats.high_risk_params, stats.medium_risk_params, stats.low_risk_params
);
IntelligentScanPlan {
scanners,
targets: deduplicated,
prioritized_params,
technologies: detected_technologies.to_vec(),
stats,
}
}
fn prioritize_parameters(
&self,
deduplicated: &DeduplicatedTargets,
technologies: &[TechCategory],
) -> Vec<PrioritizedParameter> {
let mut prioritized = Vec::new();
for test_param in &deduplicated.unique_parameters {
let source = if test_param
.context
.sources
.contains(&AttackSurfaceParamSource::Form)
{
PrioritizerParamSource::Form
} else if test_param
.context
.sources
.contains(&AttackSurfaceParamSource::Url)
{
PrioritizerParamSource::URL
} else if test_param
.context
.sources
.contains(&AttackSurfaceParamSource::JsonBody)
{
PrioritizerParamSource::JSON
} else if test_param
.context
.sources
.contains(&AttackSurfaceParamSource::Header)
{
PrioritizerParamSource::Header
} else if test_param
.context
.sources
.contains(&AttackSurfaceParamSource::Cookie)
{
PrioritizerParamSource::Cookie
} else if test_param
.context
.sources
.contains(&AttackSurfaceParamSource::PathSegment)
{
PrioritizerParamSource::Path
} else {
PrioritizerParamSource::Unknown
};
let endpoint = test_param
.context
.endpoints_seen
.first()
.cloned()
.unwrap_or_default();
let param_info = ParameterInfo {
name: test_param.name.clone(),
value: test_param.context.sample_values.first().cloned(),
input_type: "text".to_string(),
source,
endpoint_url: endpoint,
form_context: None,
};
let risk = self.prioritizer.score_parameter(¶m_info);
let risk_score_u32 = risk.score as u32;
let intensity = PayloadIntensity::from_risk_score(risk_score_u32);
let suggested = self.get_suggested_scanners(&risk, technologies);
prioritized.push(PrioritizedParameter {
name: test_param.name.clone(),
risk_score: risk_score_u32,
intensity,
suggested_scanners: suggested,
risk_factors: risk
.risk_factors
.iter()
.map(|f| format!("{:?}", f))
.collect(),
});
}
prioritized.sort_by(|a, b| b.risk_score.cmp(&a.risk_score));
prioritized
}
fn get_suggested_scanners(
&self,
risk: &ParameterRisk,
technologies: &[TechCategory],
) -> Vec<ScannerType> {
use super::parameter_prioritizer::RiskFactor;
let mut scanners = HashSet::new();
for factor in &risk.risk_factors {
match factor {
RiskFactor::CommandParameter => {
scanners.insert(ScannerType::CommandInjection);
scanners.insert(ScannerType::CodeInjection);
}
RiskFactor::FileParameter => {
scanners.insert(ScannerType::PathTraversal);
scanners.insert(ScannerType::FileUpload);
}
RiskFactor::UrlParameter => {
scanners.insert(ScannerType::Ssrf);
scanners.insert(ScannerType::SsrfBlind);
scanners.insert(ScannerType::OpenRedirect);
}
RiskFactor::IdParameter => {
scanners.insert(ScannerType::Idor);
scanners.insert(ScannerType::Bola);
scanners.insert(ScannerType::SqlI);
}
RiskFactor::AuthRelated => {
scanners.insert(ScannerType::AuthBypass);
scanners.insert(ScannerType::Jwt);
}
RiskFactor::EmailParameter => {
scanners.insert(ScannerType::EmailHeaderInjection);
}
RiskFactor::SearchParameter => {
scanners.insert(ScannerType::Xss);
scanners.insert(ScannerType::SqlI);
}
RiskFactor::AdminIndicator => {
scanners.insert(ScannerType::AuthBypass);
scanners.insert(ScannerType::Idor);
}
RiskFactor::DebugParameter => {
scanners.insert(ScannerType::InformationDisclosure);
}
_ => {}
}
}
if technologies
.iter()
.any(|t| matches!(t, TechCategory::JavaScript(_)))
{
scanners.insert(ScannerType::PrototypePollution);
}
if risk.score > 20 {
scanners.insert(ScannerType::Xss);
}
scanners.into_iter().collect()
}
pub fn registry(&self) -> &ScannerRegistry {
&self.registry
}
pub fn prioritizer(&self) -> &ParameterPrioritizer {
&self.prioritizer
}
pub fn should_run_scanner(&self, scanner: &ScannerType, technologies: &[TechCategory]) -> bool {
if self.registry.is_universal(scanner) || self.registry.is_core(scanner) {
return true;
}
for tech in technologies {
if !self.registry.should_skip(scanner, tech) {
return true;
}
}
if technologies.is_empty()
|| technologies
.iter()
.all(|t| matches!(t, TechCategory::Unknown))
{
return self.registry.get_fallback_scanners().contains(scanner);
}
false
}
pub fn get_parameter_intensity(&self, param_name: &str) -> PayloadIntensity {
let param_info = ParameterInfo {
name: param_name.to_string(),
value: None,
input_type: "text".to_string(),
source: PrioritizerParamSource::Unknown,
endpoint_url: String::new(),
form_context: None,
};
let risk = self.prioritizer.score_parameter(¶m_info);
PayloadIntensity::from_risk_score(risk.score as u32)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_orchestrator_creation() {
let orchestrator = IntelligentScanOrchestrator::new();
assert!(!orchestrator.registry().get_universal_scanners().is_empty());
assert!(!orchestrator.registry().get_core_scanners().is_empty());
}
#[test]
fn test_should_run_scanner_universal() {
let orchestrator = IntelligentScanOrchestrator::new();
assert!(orchestrator.should_run_scanner(&ScannerType::Cors, &[]));
assert!(orchestrator.should_run_scanner(&ScannerType::SecurityHeaders, &[]));
}
#[test]
fn test_should_run_scanner_core() {
let orchestrator = IntelligentScanOrchestrator::new();
assert!(orchestrator.should_run_scanner(&ScannerType::Xss, &[]));
assert!(orchestrator.should_run_scanner(&ScannerType::SqlI, &[]));
assert!(orchestrator.should_run_scanner(&ScannerType::Ssrf, &[]));
}
#[test]
fn test_parameter_intensity_high_risk() {
let orchestrator = IntelligentScanOrchestrator::new();
let intensity = orchestrator.get_parameter_intensity("password");
assert!(matches!(
intensity,
PayloadIntensity::Extended | PayloadIntensity::Maximum
));
let intensity = orchestrator.get_parameter_intensity("cmd");
assert!(matches!(
intensity,
PayloadIntensity::Extended | PayloadIntensity::Maximum
));
}
#[test]
fn test_parameter_intensity_low_risk() {
let orchestrator = IntelligentScanOrchestrator::new();
let intensity = orchestrator.get_parameter_intensity("page");
assert!(matches!(
intensity,
PayloadIntensity::Minimal | PayloadIntensity::Standard
));
}
}