wraith/src/components/tools/PortScanner.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

82 lines
3.3 KiB
Vue

<template>
<div class="flex flex-col h-full p-4 gap-3">
<div class="flex items-center gap-2">
<input v-model="target" type="text" placeholder="Target IP or hostname" class="px-3 py-1.5 text-sm rounded bg-[#161b22] border border-[#30363d] text-[#e0e0e0] outline-none focus:border-[#58a6ff] w-44" />
<input v-model="portRange" type="text" placeholder="Ports: 1-1024 or 22,80,443" class="px-3 py-1.5 text-sm rounded bg-[#161b22] border border-[#30363d] text-[#e0e0e0] outline-none focus:border-[#58a6ff] w-48" />
<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" }}
</button>
<button class="px-3 py-1.5 text-xs rounded border border-[#30363d] text-[#8b949e] hover:text-white cursor-pointer disabled:opacity-40" :disabled="scanning" @click="quickScan">Quick Scan</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 w-20">Port</th>
<th class="text-left px-3 py-2 text-[#8b949e] font-medium w-20">State</th>
<th class="text-left px-3 py-2 text-[#8b949e] font-medium">Service</th>
</tr>
</thead>
<tbody>
<tr v-for="r in results" :key="r.port" class="border-t border-[#21262d]">
<td class="px-3 py-1.5 font-mono">{{ r.port }}</td>
<td class="px-3 py-1.5" :class="r.open ? 'text-[#3fb950]' : 'text-[#484f58]'">{{ r.open ? "open" : "closed" }}</td>
<td class="px-3 py-1.5">{{ r.service }}</td>
</tr>
</tbody>
</table>
</div>
<div class="text-[10px] text-[#484f58]">{{ results.filter(r => r.open).length }} open / {{ results.length }} scanned</div>
</div>
</template>
<script setup lang="ts">
import { ref } from "vue";
import { invoke } from "@tauri-apps/api/core";
const props = defineProps<{ sessionId: string }>();
const target = ref("");
const portRange = ref("1-1024");
const results = ref<{ port: number; open: boolean; service: string }[]>([]);
const scanning = ref(false);
function parsePorts(input: string): number[] {
const ports: number[] = [];
for (const part of input.split(",")) {
const trimmed = part.trim();
if (trimmed.includes("-")) {
const [start, end] = trimmed.split("-").map(Number);
if (!isNaN(start) && !isNaN(end)) {
for (let p = start; p <= Math.min(end, 65535); p++) ports.push(p);
}
} else {
const p = Number(trimmed);
if (!isNaN(p) && p > 0 && p <= 65535) ports.push(p);
}
}
return ports;
}
async function scan(): Promise<void> {
if (!target.value) return;
scanning.value = true;
try {
const ports = parsePorts(portRange.value);
results.value = await invoke("scan_ports", { sessionId: props.sessionId, target: target.value, ports });
} catch (err) { alert(err); }
scanning.value = false;
}
async function quickScan(): Promise<void> {
if (!target.value) return;
scanning.value = true;
try {
results.value = await invoke("quick_scan", { sessionId: props.sessionId, target: target.value });
} catch (err) { alert(err); }
scanning.value = false;
}
</script>