wraith/src/components/tools/NetworkScanner.vue
Vantz Stockwell b86e2d68d8 refactor: extract keyboard shortcuts composable + 5 UX bug fixes
- Extract handleKeydown into useKeyboardShortcuts.ts composable; reduces
  MainLayout by ~20 lines and isolates keyboard logic cleanly
- ConnectionTree: watch groups for additions and auto-expand new entries
- MonitorBar: generation counter prevents stale event listeners on rapid
  tab switching
- NetworkScanner: revoke blob URL after CSV export click (memory leak)
- TransferProgress: implement the auto-expand/collapse watcher that was
  only commented but never wired up
- FileTree: block binary/large file uploads with clear user error rather
  than silently corrupting — backend sftp_write_file is text-only

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-29 16:53:46 -04:00

91 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();
setTimeout(() => URL.revokeObjectURL(a.href), 1000);
}
</script>