wraith/src/components/tools/NetworkScanner.vue
Vantz Stockwell 875dd1a28f
Some checks failed
Build & Sign Wraith / Build Windows + Sign (push) Failing after 6s
feat: complete Tools suite — 7 tool UIs in popup windows
All 7 tool windows with full UIs:
- Network Scanner: subnet scan, ARP+DNS discovery, results table with
  Quick Scan per host, SSH/RDP connect buttons, CSV export
- Port Scanner: custom range or quick scan (24 common ports), open/closed
  results table with service names
- Ping: remote ping with count, raw output display
- Traceroute: remote traceroute, raw output display
- Wake on LAN: MAC address input, broadcasts via python3 on remote host
- SSH Key Generator: ed25519/RSA, copy public/private key, fingerprint
- Password Generator: configurable length/charset, copy button, history

Architecture:
- App.vue detects tool mode via URL hash (#/tool/name?sessionId=...)
- ToolWindow.vue routes to correct tool component
- Tools menu in toolbar opens Tauri popup windows (WebviewWindow)
- Capabilities grant tool-* windows the same permissions as main
- SFTP context menu: right-click Edit/Download/Rename/Delete

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 00:07:15 -04:00

90 lines
4.3 KiB
Vue

<template>
<div class="flex flex-col h-full p-4 gap-3">
<div class="flex items-center gap-2">
<label class="text-xs text-[#8b949e]">Subnet (first 3 octets):</label>
<input v-model="subnet" type="text" placeholder="192.168.1" class="px-3 py-1.5 text-sm rounded bg-[#161b22] border border-[#30363d] text-[#e0e0e0] outline-none focus:border-[#58a6ff] w-40" />
<button class="px-4 py-1.5 text-sm font-bold rounded bg-[#58a6ff] text-black cursor-pointer disabled:opacity-40" :disabled="scanning" @click="scan">
{{ scanning ? "Scanning..." : "Scan Network" }}
</button>
<button v-if="hosts.length" class="px-3 py-1.5 text-xs rounded border border-[#30363d] text-[#8b949e] hover:text-white cursor-pointer" @click="exportCsv">Export CSV</button>
</div>
<div class="flex-1 overflow-auto border border-[#30363d] rounded">
<table class="w-full text-xs">
<thead class="bg-[#161b22] sticky top-0">
<tr>
<th class="text-left px-3 py-2 text-[#8b949e] font-medium">IP Address</th>
<th class="text-left px-3 py-2 text-[#8b949e] font-medium">Hostname</th>
<th class="text-left px-3 py-2 text-[#8b949e] font-medium">MAC Address</th>
<th class="text-left px-3 py-2 text-[#8b949e] font-medium">Open Ports</th>
<th class="text-left px-3 py-2 text-[#8b949e] font-medium">Actions</th>
</tr>
</thead>
<tbody>
<tr v-for="host in hosts" :key="host.ip" class="border-t border-[#21262d] hover:bg-[#161b22]">
<td class="px-3 py-1.5 font-mono">{{ host.ip }}</td>
<td class="px-3 py-1.5">{{ host.hostname || "—" }}</td>
<td class="px-3 py-1.5 font-mono text-[#8b949e]">{{ host.mac || "—" }}</td>
<td class="px-3 py-1.5">
<span v-if="host.openPorts.length" class="text-[#3fb950]">{{ host.openPorts.join(", ") }}</span>
<button v-else class="text-[#58a6ff] hover:underline cursor-pointer" @click="quickScanHost(host)">scan</button>
</td>
<td class="px-3 py-1.5 flex gap-1">
<button class="px-2 py-0.5 text-[10px] rounded bg-[#238636] text-white cursor-pointer" @click="connectSsh(host)">SSH</button>
<button class="px-2 py-0.5 text-[10px] rounded bg-[#1f6feb] text-white cursor-pointer" @click="connectRdp(host)">RDP</button>
</td>
</tr>
<tr v-if="!hosts.length && !scanning">
<td colspan="5" class="px-3 py-8 text-center text-[#484f58]">Enter a subnet and click Scan</td>
</tr>
</tbody>
</table>
</div>
<div class="text-[10px] text-[#484f58]">{{ hosts.length }} hosts found • Scanning through session {{ sessionId.substring(0, 8) }}...</div>
</div>
</template>
<script setup lang="ts">
import { ref } from "vue";
import { invoke } from "@tauri-apps/api/core";
const props = defineProps<{ sessionId: string }>();
interface Host { ip: string; mac: string | null; hostname: string | null; vendor: string | null; openPorts: number[]; services: string[]; }
const subnet = ref("192.168.1");
const hosts = ref<Host[]>([]);
const scanning = ref(false);
async function scan(): Promise<void> {
scanning.value = true;
try {
hosts.value = await invoke<Host[]>("scan_network", { sessionId: props.sessionId, subnet: subnet.value });
} catch (err) { alert(err); }
scanning.value = false;
}
async function quickScanHost(host: Host): Promise<void> {
try {
const results = await invoke<{ port: number; open: boolean; service: string }[]>("quick_scan", { sessionId: props.sessionId, target: host.ip });
host.openPorts = results.filter(r => r.open).map(r => r.port);
} catch (err) { console.error(err); }
}
function connectSsh(host: Host): void { alert(`TODO: Open SSH tab to ${host.ip}`); }
function connectRdp(host: Host): void { alert(`TODO: Open RDP tab to ${host.ip}`); }
function exportCsv(): void {
const lines = ["IP,Hostname,MAC,OpenPorts"];
for (const h of hosts.value) {
lines.push(`${h.ip},"${h.hostname || ""}","${h.mac || ""}","${h.openPorts.join(";")}"`);
}
const blob = new Blob([lines.join("\n")], { type: "text/csv" });
const a = document.createElement("a");
a.href = URL.createObjectURL(blob);
a.download = `wraith-scan-${subnet.value}-${Date.now()}.csv`;
a.click();
}
</script>