wifiscan 0.4.0

Wireless network scanner TUI with monitor mode, handshake capture, deauth, and evil twin
Documentation
use std::io::Write;

use serde::Serialize;

use crate::types::{ApMap, HandshakeMap, ProbeMap};

#[derive(Serialize)]
struct ApExport {
    essid: String,
    bssid: String,
    channel: u8,
    frequency: u16,
    signal_dbm: i8,
    noise_dbm: Option<i8>,
    encryption: String,
    wpa3: bool,
    pmf: bool,
    wifi_generation: Option<u8>,
    channel_width: u16,
    vendor: String,
    beacons: u64,
    data: u64,
    clients: Vec<ClientExport>,
    handshakes: u32,
    pmkid: bool,
    last_seen_secs: u64,
    first_seen_secs: u64,
    deauth_sent: u32,
    security_score: u8,
}

#[derive(Serialize)]
struct ClientExport {
    mac: String,
    signal_dbm: i8,
    vendor: String,
    data_count: u64,
    probed_ssids: Vec<String>,
    last_seen_secs: u64,
    is_randomized: bool,
}

pub fn export_aps_csv(ap_map: &ApMap, path: &str) -> Result<usize, String> {
    let map = ap_map.lock().unwrap();
    let mut file = std::fs::File::create(path).map_err(|e| format!("{}", e))?;
    writeln!(file,
        "ESSID,BSSID,Channel,Frequency,Signal_dBm,Noise_dBm,Encryption,WPA3,PMF,WiFi_Gen,Width_MHz,Vendor,Beacons,Data,Clients,Handshakes,PMKID,Security_Score,First_Seen_Secs,Last_Seen_Secs"
    ).map_err(|e| format!("{}", e))?;

    let count = map.len();
    for ap in map.values() {
        let noise = ap.noise_dbm.map(|n| n.to_string()).unwrap_or_default();
        let wifi_gen = ap.wifi_generation.map(|g| g.to_string()).unwrap_or_else(|| "-".to_string());
        writeln!(file,
            "\"{}\",{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{}",
            ap.essid.replace('"', "\"\""),
            ap.bssid, ap.channel, ap.frequency_mhz, ap.signal_dbm, noise,
            ap.encryption.display, ap.encryption.wpa3_sae, ap.encryption.pmf_required,
            wifi_gen, ap.channel_width, ap.vendor,
            ap.beacon_count, ap.data_count, ap.clients.len(),
            ap.handshakes, ap.pmkid_captured, ap.encryption.security_score(),
            ap.uptime_secs(), ap.age_secs()
        ).map_err(|e| format!("{}", e))?;
    }
    Ok(count)
}

pub fn export_aps_json(ap_map: &ApMap, path: &str) -> Result<usize, String> {
    let map = ap_map.lock().unwrap();
    let exports: Vec<ApExport> = map.values().map(|ap| {
        let clients: Vec<ClientExport> = ap.clients.values().map(|c| {
            ClientExport {
                mac: c.mac.clone(),
                signal_dbm: c.signal_dbm,
                vendor: c.vendor.clone(),
                data_count: c.data_count,
                probed_ssids: c.probed_ssids.clone(),
                last_seen_secs: c.age_secs(),
                is_randomized: c.is_randomized,
            }
        }).collect();

        ApExport {
            essid: ap.essid.clone(),
            bssid: ap.bssid.clone(),
            channel: ap.channel,
            frequency: ap.frequency_mhz,
            signal_dbm: ap.signal_dbm,
            noise_dbm: ap.noise_dbm,
            encryption: ap.encryption.display.clone(),
            wpa3: ap.encryption.wpa3_sae,
            pmf: ap.encryption.pmf_required,
            wifi_generation: ap.wifi_generation,
            channel_width: ap.channel_width,
            vendor: ap.vendor.clone(),
            beacons: ap.beacon_count,
            data: ap.data_count,
            clients,
            handshakes: ap.handshakes,
            pmkid: ap.pmkid_captured,
            last_seen_secs: ap.age_secs(),
            first_seen_secs: ap.uptime_secs(),
            deauth_sent: ap.deauth_sent,
            security_score: ap.encryption.security_score(),
        }
    }).collect();

    let count = exports.len();
    let json = serde_json::to_string_pretty(&exports).map_err(|e| format!("{}", e))?;
    std::fs::write(path, json).map_err(|e| format!("{}", e))?;
    Ok(count)
}

/// Export probe requests: which clients are looking for which networks
pub fn export_probes_csv(probe_map: &ProbeMap, path: &str) -> Result<usize, String> {
    let pm = probe_map.lock().unwrap();
    let mut file = std::fs::File::create(path).map_err(|e| format!("{}", e))?;
    writeln!(file, "Client_MAC,Probed_SSIDs").map_err(|e| format!("{}", e))?;

    let count = pm.len();
    for (mac, ssids) in pm.iter() {
        let ssid_list: Vec<&String> = ssids.iter().collect();
        writeln!(file, "{},\"{}\"", mac, ssid_list.iter().map(|s| s.as_str()).collect::<Vec<_>>().join(";"))
            .map_err(|e| format!("{}", e))?;
    }
    Ok(count)
}

/// Export captured PMKIDs in hashcat 22000 format
pub fn export_pmkid_hashcat(handshake_map: &HandshakeMap, path: &str) -> Result<usize, String> {
    let hs_map = handshake_map.lock().unwrap();
    let mut file = std::fs::File::create(path).map_err(|e| format!("{}", e))?;

    let mut count = 0;
    for (key, hs) in hs_map.iter() {
        if let Some(ref pmkid) = hs.pmkid {
            let pmkid_hex: String = pmkid.iter().map(|b| format!("{:02x}", b)).collect();
            let bssid_clean = key.bssid.replace(':', "");
            let client_clean = key.client.replace(':', "");
            let essid_hex: String = hs.essid.bytes().map(|b| format!("{:02x}", b)).collect();

            // hashcat mode 22000 format: PMKID*MAC_AP*MAC_STA*ESSID
            writeln!(file, "{}*{}*{}*{}", pmkid_hex, bssid_clean, client_clean, essid_hex)
                .map_err(|e| format!("{}", e))?;
            count += 1;
        }
    }
    Ok(count)
}

/// Export in WiGLE CSV format for upload
pub fn export_wigle_csv(ap_map: &ApMap, path: &str) -> Result<usize, String> {
    let map = ap_map.lock().unwrap();
    let mut file = std::fs::File::create(path).map_err(|e| format!("{}", e))?;

    // WiGLE header
    writeln!(file, "WigleWifi-1.4,appRelease=wifiscan,model=linux,release=0.4.0,device=wifiscan,display=TUI,board=custom,brand=wifiscan")
        .map_err(|e| format!("{}", e))?;
    writeln!(file, "MAC,SSID,AuthMode,FirstSeen,Channel,RSSI,CurrentLatitude,CurrentLongitude,AltitudeMeters,AccuracyMeters,Type")
        .map_err(|e| format!("{}", e))?;

    let count = map.len();
    let now = chrono::Local::now().format("%Y-%m-%d %H:%M:%S").to_string();
    for ap in map.values() {
        let auth = &ap.encryption.display;
        writeln!(file, "{},\"{}\",{},{},{},{},0.0,0.0,0,0,WIFI",
            ap.bssid, ap.essid.replace('"', "\"\""), auth, now, ap.channel, ap.signal_dbm)
            .map_err(|e| format!("{}", e))?;
    }
    Ok(count)
}