zond-cli 0.6.2

Command-Line Interface (CLI) for the Zond Engine
// Copyright (c) 2026 Erik Lening (hollowpointer) and Contributors
//
// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
// If a copy of the MPL was not distributed with this file, You can obtain one at
// https://mozilla.org/MPL/2.0/.

use std::time::Instant;

use crate::terminal::colors;
use crate::terminal::input;
use crate::terminal::print::Print;
use crate::terminal::spinner::SpinnerGuard;
use colored::*;
use dashmap::DashMap;
use std::net::IpAddr;
use std::sync::Arc;
use tracing::info_span;
use zond_engine::core::config::ZondConfig;
use zond_engine::core::models::host::Host;
use zond_engine::core::models::port::PortSet;
use zond_engine::core::parse;

pub async fn scan(
    targets: &[String],
    global_ports: PortSet,
    _cfg: &ZondConfig,
) -> anyhow::Result<()> {
    Print::header("starting scanner");

    let target_map = parse::to_target_map(
        targets,
        global_ports,
        Some(zond_engine::system::interface::resolve::resolve),
    )?;
    let start_time = Instant::now();

    let (session, scan_task) = zond_engine::scanner::scan(target_map).await?;
    let _input_guard = input::start_listener(session.handle.clone());
    let _guard: SpinnerGuard = run_spinner(session.store.clone());

    scan_task.await??;
    let mut hosts: Vec<Host> = session.store.iter().map(|kv| kv.value().clone()).collect();

    if hosts.is_empty() {
        Print::no_results();
        return Ok(());
    }

    Print::header("Network Scanner");

    hosts.sort_by_key(|host| *host.ips().iter().next().unwrap_or(&host.primary_ip()));

    Print::hosts(&hosts)?;
    Print::discovery_summary(hosts.len(), start_time.elapsed());

    Ok(())
}

fn run_spinner(store: Arc<DashMap<IpAddr, Host>>) -> SpinnerGuard {
    let span = info_span!("scan", indicatif.pb_show = true);
    let _enter = span.enter();

    SpinnerGuard::with_status(span.clone(), move || {
        let count = store.len();
        let count_str = count.to_string().green().bold();
        let label = if count == 1 { "host" } else { "hosts" };
        format!("Scanned {} {} so far...", count_str, label)
            .color(colors::TEXT_DEFAULT)
            .italic()
    })
}