use crate::api::jobs::{JobQueue, ScanJob};
use crate::api::models::request::ScanOptions;
use crate::api::models::response::ProgressMessage;
use crate::cli::Args;
use crate::scanner::{ScanResults, Scanner};
use anyhow::Result;
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::{Semaphore, broadcast};
use tracing::{error, info, warn};
pub struct ScanExecutor {
job_queue: Arc<dyn JobQueue>,
max_concurrent: usize,
semaphore: Arc<Semaphore>,
progress_tx: broadcast::Sender<ProgressMessage>,
shutdown_tx: tokio::sync::watch::Sender<bool>,
shutdown_rx: tokio::sync::watch::Receiver<bool>,
}
impl ScanExecutor {
pub fn new(job_queue: Arc<dyn JobQueue>, max_concurrent: usize) -> Self {
let semaphore = Arc::new(Semaphore::new(max_concurrent));
let (progress_tx, _) = broadcast::channel(1000);
let (shutdown_tx, shutdown_rx) = tokio::sync::watch::channel(false);
Self {
job_queue,
max_concurrent,
semaphore,
progress_tx,
shutdown_tx,
shutdown_rx,
}
}
pub fn progress_broadcaster(&self) -> broadcast::Sender<ProgressMessage> {
self.progress_tx.clone()
}
pub fn subscribe_progress(&self) -> broadcast::Receiver<ProgressMessage> {
self.progress_tx.subscribe()
}
pub async fn start(self: Arc<Self>) -> Result<()> {
info!(
"Starting scan executor with {} concurrent slots",
self.max_concurrent
);
let shutdown_rx = self.shutdown_rx.clone();
loop {
if *shutdown_rx.borrow() {
info!("Scan executor shutting down");
break;
}
let permit = match Arc::clone(&self.semaphore).try_acquire_owned() {
Ok(p) => p,
Err(_) => {
tokio::time::sleep(Duration::from_millis(100)).await;
continue;
}
};
match self.job_queue.dequeue().await {
Ok(Some(job)) => {
let executor = Arc::clone(&self);
let queue = Arc::clone(&self.job_queue);
tokio::spawn(async move {
executor.execute_scan(queue, job).await;
drop(permit);
});
}
Ok(None) => {
drop(permit);
tokio::time::sleep(Duration::from_millis(500)).await;
}
Err(e) => {
error!("Error dequeuing job: {}", e);
drop(permit);
tokio::time::sleep(Duration::from_secs(1)).await;
}
}
}
Ok(())
}
async fn execute_scan(&self, queue: Arc<dyn JobQueue>, mut job: ScanJob) {
info!("Starting scan job {} for target {}", job.id, job.target);
job.mark_started();
if let Err(e) = queue.update_job(&job).await {
error!("Failed to update job status: {}", e);
}
self.send_progress(&job, 0, "Starting scan");
match Self::run_scan(&job, &self.progress_tx).await {
Ok(results) => {
info!("Scan job {} completed successfully", job.id);
job.mark_completed(results);
let msg = ProgressMessage::completed(&job.id);
let _ = self.progress_tx.send(msg);
}
Err(e) => {
let error_msg = e.to_string();
error!("Scan job {} failed: {}", job.id, error_msg);
let msg = ProgressMessage::failed(&job.id, &error_msg);
let _ = self.progress_tx.send(msg);
job.mark_failed(error_msg);
}
}
if let Err(e) = queue.update_job(&job).await {
error!("Failed to update job status: {}", e);
}
if let Some(webhook_url) = &job.webhook_url
&& let Err(e) = Self::send_webhook(webhook_url, &job).await
{
warn!("Failed to send webhook for job {}: {}", job.id, e);
}
}
async fn run_scan(
job: &ScanJob,
progress_tx: &broadcast::Sender<ProgressMessage>,
) -> Result<ScanResults> {
let args = Self::options_to_args(&job.target, &job.options)?;
let _ = progress_tx.send(ProgressMessage::new(&job.id, 5, "Initializing scanner"));
let scanner = Scanner::new(args)?;
let _ = progress_tx.send(ProgressMessage::new(&job.id, 10, "Resolving target"));
scanner.initialize().await?;
let _ = progress_tx.send(ProgressMessage::new(&job.id, 15, "Starting TLS scan"));
let results = scanner.run().await?;
let _ = progress_tx.send(ProgressMessage::new(&job.id, 95, "Finalizing results"));
Ok(results)
}
#[allow(clippy::field_reassign_with_default)]
fn options_to_args(target: &str, options: &ScanOptions) -> Result<Args> {
let mut args = Args::default();
args.target = Some(target.to_string());
args.output.quiet = true;
args.connection.socket_timeout = Some(options.timeout_seconds);
if options.test_protocols || options.full_scan {
args.scan.protocols = true;
}
if options.test_ciphers || options.full_scan {
args.scan.each_cipher = true;
}
if options.test_vulnerabilities || options.full_scan {
args.scan.vulnerabilities = true;
}
if options.test_http_headers || options.full_scan {
args.scan.headers = true;
}
if options.client_simulation || options.full_scan {
args.fingerprint.client_simulation = true;
}
args.starttls.protocol = options.starttls_protocol.clone();
args.network.ipv4_only = options.ipv4_only;
args.network.ipv6_only = options.ipv6_only;
args.ip = options.ip.clone();
if options.full_scan {
args.scan.all = true;
}
Ok(args)
}
fn send_progress(&self, job: &ScanJob, progress: u8, stage: &str) {
let msg = ProgressMessage::new(&job.id, progress, stage);
let _ = self.progress_tx.send(msg);
}
async fn send_webhook(webhook_url: &str, job: &ScanJob) -> Result<()> {
let client = reqwest::Client::new();
let payload = serde_json::json!({
"job_id": job.id,
"target": job.target,
"status": job.status,
"completed_at": job.completed_at,
"error": job.error,
});
let response = client
.post(webhook_url)
.json(&payload)
.timeout(Duration::from_secs(10))
.send()
.await?;
if !response.status().is_success() {
anyhow::bail!("Webhook returned status: {}", response.status());
}
Ok(())
}
pub async fn shutdown(&self) -> Result<()> {
info!("Initiating executor shutdown");
self.shutdown_tx.send(true)?;
Ok(())
}
}
impl Clone for ScanExecutor {
fn clone(&self) -> Self {
Self {
job_queue: self.job_queue.clone(),
max_concurrent: self.max_concurrent,
semaphore: self.semaphore.clone(),
progress_tx: self.progress_tx.clone(),
shutdown_tx: self.shutdown_tx.clone(),
shutdown_rx: self.shutdown_rx.clone(),
}
}
}