@harbinger-ai/harbinger 0.1.3 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/bounty/actions.js +45 -0
- package/lib/chat/actions.js +501 -20
- package/lib/chat/components/agents-page.js +942 -159
- package/lib/chat/components/agents-page.jsx +907 -233
- package/lib/chat/components/icons.js +105 -0
- package/lib/chat/components/icons.jsx +129 -0
- package/lib/chat/components/page-layout.js +41 -2
- package/lib/chat/components/page-layout.jsx +40 -2
- package/lib/chat/components/settings-providers-page.js +647 -112
- package/lib/chat/components/settings-providers-page.jsx +641 -134
- package/lib/chat/components/targets-page.js +554 -96
- package/lib/chat/components/targets-page.jsx +464 -114
- package/package.json +1 -1
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { useState, useEffect } from "react";
|
|
3
|
+
import { useState, useEffect, useRef, useCallback } from "react";
|
|
4
4
|
import { motion, AnimatePresence } from "framer-motion";
|
|
5
|
-
import { CrosshairIcon, PlusIcon, TrashIcon, ChevronDownIcon, GlobeIcon, SpinnerIcon, DownloadIcon, CheckIcon } from "./icons.js";
|
|
6
|
-
import { getPrograms, createProgram, deleteProgram, getTargets, createTarget, deleteTarget, updateTarget, syncTargetsFromPlatform, syncAllTargets, getSyncStatus } from "../../bounty/actions.js";
|
|
5
|
+
import { CrosshairIcon, PlusIcon, TrashIcon, ChevronDownIcon, GlobeIcon, SpinnerIcon, DownloadIcon, CheckIcon, SearchIcon, CopyIcon, UploadIcon, FilterIcon } from "./icons.js";
|
|
6
|
+
import { getPrograms, createProgram, deleteProgram, getTargets, createTarget, deleteTarget, updateTarget, syncTargetsFromPlatform, syncAllTargets, getSyncStatus, getProgramTargetCounts, bulkImportTargets, exportTargets } from "../../bounty/actions.js";
|
|
7
7
|
const PLATFORMS = [
|
|
8
8
|
{ id: "hackerone", label: "HackerOne", bg: "bg-purple-500/10", text: "text-purple-500", border: "border-purple-500/20" },
|
|
9
9
|
{ id: "bugcrowd", label: "Bugcrowd", bg: "bg-orange-500/10", text: "text-orange-500", border: "border-orange-500/20" },
|
|
@@ -13,6 +13,7 @@ const PLATFORMS = [
|
|
|
13
13
|
{ id: "custom", label: "Custom", bg: "bg-white/5", text: "text-muted-foreground", border: "border-white/10" }
|
|
14
14
|
];
|
|
15
15
|
const TARGET_TYPES = ["domain", "wildcard", "ip", "cidr", "url", "api", "mobile"];
|
|
16
|
+
const TYPE_ICONS = { domain: "\u{1F310}", wildcard: "*", ip: "\u{1F4CD}", cidr: "\u{1F5A7}", url: "\u{1F517}", api: "\u2699\uFE0F", mobile: "\u{1F4F1}" };
|
|
16
17
|
const STATUS_COLORS = {
|
|
17
18
|
in_scope: { bg: "bg-green-500/10", text: "text-green-500", border: "border-green-500/20", dot: "bg-green-500" },
|
|
18
19
|
out_of_scope: { bg: "bg-red-500/10", text: "text-red-500", border: "border-red-500/20", dot: "bg-red-500" },
|
|
@@ -31,6 +32,18 @@ function timeAgo(ts) {
|
|
|
31
32
|
function getPlatStyle(platformId) {
|
|
32
33
|
return PLATFORMS.find((p) => p.id === platformId) || PLATFORMS[5];
|
|
33
34
|
}
|
|
35
|
+
function CopyBtn({ text }) {
|
|
36
|
+
const [copied, setCopied] = useState(false);
|
|
37
|
+
const handleCopy = async () => {
|
|
38
|
+
try {
|
|
39
|
+
await navigator.clipboard.writeText(text);
|
|
40
|
+
} catch {
|
|
41
|
+
}
|
|
42
|
+
setCopied(true);
|
|
43
|
+
setTimeout(() => setCopied(false), 1500);
|
|
44
|
+
};
|
|
45
|
+
return /* @__PURE__ */ jsx("button", { onClick: handleCopy, className: "shrink-0 p-1 text-muted-foreground hover:text-[--cyan] transition-colors", title: "Copy", children: copied ? /* @__PURE__ */ jsx(CheckIcon, { size: 12 }) : /* @__PURE__ */ jsx(CopyIcon, { size: 12 }) });
|
|
46
|
+
}
|
|
34
47
|
function SyncPanel({ onSync, syncStatus }) {
|
|
35
48
|
const [syncing, setSyncing] = useState(null);
|
|
36
49
|
const [results, setResults] = useState(null);
|
|
@@ -120,40 +133,31 @@ function SyncPanel({ onSync, syncStatus }) {
|
|
|
120
133
|
),
|
|
121
134
|
/* @__PURE__ */ jsx("span", { className: "text-[10px] text-muted-foreground font-mono", children: "(0 = unlimited)" })
|
|
122
135
|
] }),
|
|
123
|
-
/* @__PURE__ */ jsx(AnimatePresence, { children: results && /* @__PURE__ */ jsx(
|
|
124
|
-
|
|
125
|
-
{
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
" skipped"
|
|
149
|
-
] }),
|
|
150
|
-
stats.errors?.length > 0 && /* @__PURE__ */ jsxs("span", { className: "text-[--destructive]", children: [
|
|
151
|
-
stats.errors.length,
|
|
152
|
-
" errors"
|
|
153
|
-
] })
|
|
154
|
-
] }, platform)) }) })
|
|
155
|
-
}
|
|
156
|
-
) }),
|
|
136
|
+
/* @__PURE__ */ jsx(AnimatePresence, { children: results && /* @__PURE__ */ jsx(motion.div, { initial: { opacity: 0, height: 0 }, animate: { opacity: 1, height: "auto" }, exit: { opacity: 0, height: 0 }, className: "overflow-hidden", children: /* @__PURE__ */ jsx("div", { className: "rounded-md bg-black/30 border border-white/[0.04] p-3 mt-2", children: results.error ? /* @__PURE__ */ jsx("p", { className: "text-xs font-mono text-[--destructive]", children: results.error }) : /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-1", children: Object.entries(results).map(([platform, stats]) => /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 text-xs font-mono", children: [
|
|
137
|
+
/* @__PURE__ */ jsx("span", { className: "font-medium w-24 text-foreground/80", children: platform }),
|
|
138
|
+
/* @__PURE__ */ jsxs("span", { className: "text-green-500", children: [
|
|
139
|
+
"+",
|
|
140
|
+
stats.programsAdded,
|
|
141
|
+
" programs"
|
|
142
|
+
] }),
|
|
143
|
+
/* @__PURE__ */ jsxs("span", { className: "text-blue-500", children: [
|
|
144
|
+
stats.programsUpdated,
|
|
145
|
+
" updated"
|
|
146
|
+
] }),
|
|
147
|
+
/* @__PURE__ */ jsxs("span", { className: "text-green-500", children: [
|
|
148
|
+
"+",
|
|
149
|
+
stats.targetsAdded,
|
|
150
|
+
" targets"
|
|
151
|
+
] }),
|
|
152
|
+
/* @__PURE__ */ jsxs("span", { className: "text-muted-foreground", children: [
|
|
153
|
+
stats.targetsSkipped,
|
|
154
|
+
" skipped"
|
|
155
|
+
] }),
|
|
156
|
+
stats.errors?.length > 0 && /* @__PURE__ */ jsxs("span", { className: "text-[--destructive]", children: [
|
|
157
|
+
stats.errors.length,
|
|
158
|
+
" errors"
|
|
159
|
+
] })
|
|
160
|
+
] }, platform)) }) }) }) }),
|
|
157
161
|
syncStatus && syncStatus.totalSyncedPrograms > 0 && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 mt-3 pt-3 border-t border-white/[0.06]", children: [
|
|
158
162
|
/* @__PURE__ */ jsxs("span", { className: "text-[10px] text-muted-foreground font-mono", children: [
|
|
159
163
|
syncStatus.totalSyncedPrograms,
|
|
@@ -173,8 +177,10 @@ function SyncPanel({ onSync, syncStatus }) {
|
|
|
173
177
|
}
|
|
174
178
|
);
|
|
175
179
|
}
|
|
176
|
-
function ProgramCard({ program, onSelect, selected, onDelete, index }) {
|
|
180
|
+
function ProgramCard({ program, onSelect, selected, onDelete, index, targetCount }) {
|
|
177
181
|
const plat = getPlatStyle(program.platform);
|
|
182
|
+
const tc = targetCount || { total: 0, inScope: 0, outOfScope: 0 };
|
|
183
|
+
const hasTargets = tc.total > 0;
|
|
178
184
|
return /* @__PURE__ */ jsxs(
|
|
179
185
|
motion.button,
|
|
180
186
|
{
|
|
@@ -184,11 +190,7 @@ function ProgramCard({ program, onSelect, selected, onDelete, index }) {
|
|
|
184
190
|
onClick: () => onSelect(program.id),
|
|
185
191
|
className: `flex items-center gap-3 w-full text-left p-3 rounded-lg border transition-all ${selected ? "border-[--cyan]/40 bg-[--cyan]/5 glow-cyan" : "border-white/[0.06] bg-[--card] hover:border-[--cyan]/20"}`,
|
|
186
192
|
children: [
|
|
187
|
-
/* @__PURE__ */
|
|
188
|
-
/* @__PURE__ */ jsx("div", { className: "w-2 h-2 rounded-full bg-[#ff5f57]" }),
|
|
189
|
-
/* @__PURE__ */ jsx("div", { className: "w-2 h-2 rounded-full bg-[#febc2e]" }),
|
|
190
|
-
/* @__PURE__ */ jsx("div", { className: "w-2 h-2 rounded-full bg-[#28c840]" })
|
|
191
|
-
] }),
|
|
193
|
+
/* @__PURE__ */ jsx("div", { className: `shrink-0 w-2 h-2 rounded-full ${hasTargets ? "bg-green-500" : "bg-muted-foreground/30"}` }),
|
|
192
194
|
/* @__PURE__ */ jsxs("div", { className: "flex-1 min-w-0", children: [
|
|
193
195
|
/* @__PURE__ */ jsx("p", { className: "text-sm font-mono font-medium truncate", children: program.name }),
|
|
194
196
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 mt-0.5 flex-wrap", children: [
|
|
@@ -200,48 +202,275 @@ function ProgramCard({ program, onSelect, selected, onDelete, index }) {
|
|
|
200
202
|
program.syncHandle && /* @__PURE__ */ jsx("span", { className: "inline-flex rounded-full bg-[--cyan]/10 border border-[--cyan]/20 px-1.5 py-0.5 text-[9px] font-mono text-[--cyan]", children: "synced" })
|
|
201
203
|
] })
|
|
202
204
|
] }),
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
205
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 shrink-0", children: [
|
|
206
|
+
hasTargets && /* @__PURE__ */ jsx("span", { className: "inline-flex items-center gap-1 rounded-full bg-green-500/10 text-green-500 border border-green-500/20 px-2 py-0.5 text-[9px] font-mono font-medium", children: tc.total }),
|
|
207
|
+
!program.syncHandle && /* @__PURE__ */ jsx(
|
|
208
|
+
"button",
|
|
209
|
+
{
|
|
210
|
+
onClick: (e) => {
|
|
211
|
+
e.stopPropagation();
|
|
212
|
+
onDelete(program.id);
|
|
213
|
+
},
|
|
214
|
+
className: "shrink-0 p-1 text-muted-foreground hover:text-[--destructive] rounded transition-colors",
|
|
215
|
+
children: /* @__PURE__ */ jsx(TrashIcon, { size: 12 })
|
|
216
|
+
}
|
|
217
|
+
)
|
|
218
|
+
] })
|
|
207
219
|
]
|
|
208
220
|
}
|
|
209
221
|
);
|
|
210
222
|
}
|
|
211
|
-
function TargetRow({ target, onDelete, onStatusChange, index }) {
|
|
223
|
+
function TargetRow({ target, onDelete, onStatusChange, onUpdate, index }) {
|
|
224
|
+
const [expanded, setExpanded] = useState(false);
|
|
225
|
+
const [notes, setNotes] = useState(target.notes || "");
|
|
226
|
+
const [savingNotes, setSavingNotes] = useState(false);
|
|
212
227
|
const st = STATUS_COLORS[target.status] || STATUS_COLORS.in_scope;
|
|
228
|
+
const icon = TYPE_ICONS[target.type] || "\u{1F310}";
|
|
229
|
+
async function handleSaveNotes() {
|
|
230
|
+
setSavingNotes(true);
|
|
231
|
+
await onUpdate(target.id, { notes });
|
|
232
|
+
setSavingNotes(false);
|
|
233
|
+
}
|
|
234
|
+
let techs = [];
|
|
235
|
+
try {
|
|
236
|
+
if (target.technologies) techs = JSON.parse(target.technologies);
|
|
237
|
+
} catch {
|
|
238
|
+
}
|
|
213
239
|
return /* @__PURE__ */ jsxs(
|
|
214
240
|
motion.div,
|
|
215
241
|
{
|
|
216
242
|
initial: { opacity: 0, y: 6 },
|
|
217
243
|
animate: { opacity: 1, y: 0 },
|
|
218
|
-
transition: { duration: 0.2, delay: index * 0.
|
|
219
|
-
className: "
|
|
244
|
+
transition: { duration: 0.2, delay: Math.min(index * 0.015, 0.3) },
|
|
245
|
+
className: "rounded-lg border border-white/[0.06] bg-[--card] hover:border-[--cyan]/20 transition-colors",
|
|
220
246
|
children: [
|
|
221
|
-
/* @__PURE__ */
|
|
222
|
-
|
|
223
|
-
/* @__PURE__ */ jsx("
|
|
224
|
-
/* @__PURE__ */ jsxs("div", { className: "flex
|
|
225
|
-
/* @__PURE__ */ jsx("
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
247
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 p-3 cursor-pointer", onClick: () => setExpanded(!expanded), children: [
|
|
248
|
+
/* @__PURE__ */ jsx("div", { className: `shrink-0 w-2 h-2 rounded-full ${st.dot}` }),
|
|
249
|
+
/* @__PURE__ */ jsx("span", { className: "text-sm shrink-0 w-6 text-center", children: icon }),
|
|
250
|
+
/* @__PURE__ */ jsxs("div", { className: "flex-1 min-w-0", children: [
|
|
251
|
+
/* @__PURE__ */ jsx("p", { className: "text-sm font-mono font-medium truncate select-all", children: target.value }),
|
|
252
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 mt-0.5 flex-wrap", children: [
|
|
253
|
+
/* @__PURE__ */ jsx("span", { className: "inline-flex rounded-full bg-white/5 border border-white/10 px-2 py-0.5 text-[10px] font-mono font-medium text-muted-foreground", children: target.type }),
|
|
254
|
+
target.syncSource && /* @__PURE__ */ jsx("span", { className: "inline-flex rounded-full bg-[--cyan]/10 border border-[--cyan]/20 px-1.5 py-0.5 text-[9px] font-mono text-[--cyan]", children: target.syncSource }),
|
|
255
|
+
techs.length > 0 && /* @__PURE__ */ jsx("span", { className: "text-[10px] font-mono text-muted-foreground truncate max-w-[200px]", children: techs.join(", ") }),
|
|
256
|
+
target.lastScannedAt && /* @__PURE__ */ jsxs("span", { className: "text-[9px] font-mono text-muted-foreground", children: [
|
|
257
|
+
"scanned ",
|
|
258
|
+
timeAgo(target.lastScannedAt)
|
|
259
|
+
] })
|
|
260
|
+
] })
|
|
261
|
+
] }),
|
|
262
|
+
/* @__PURE__ */ jsx(CopyBtn, { text: target.value }),
|
|
263
|
+
/* @__PURE__ */ jsxs(
|
|
264
|
+
"select",
|
|
265
|
+
{
|
|
266
|
+
value: target.status,
|
|
267
|
+
onClick: (e) => e.stopPropagation(),
|
|
268
|
+
onChange: (e) => {
|
|
269
|
+
e.stopPropagation();
|
|
270
|
+
onStatusChange(target.id, e.target.value);
|
|
271
|
+
},
|
|
272
|
+
className: `text-[10px] font-mono font-medium rounded-full px-2 py-0.5 border cursor-pointer bg-transparent ${st.bg} ${st.text} ${st.border} focus:outline-none`,
|
|
273
|
+
children: [
|
|
274
|
+
/* @__PURE__ */ jsx("option", { value: "in_scope", children: "in scope" }),
|
|
275
|
+
/* @__PURE__ */ jsx("option", { value: "testing", children: "testing" }),
|
|
276
|
+
/* @__PURE__ */ jsx("option", { value: "completed", children: "completed" }),
|
|
277
|
+
/* @__PURE__ */ jsx("option", { value: "out_of_scope", children: "out of scope" })
|
|
278
|
+
]
|
|
279
|
+
}
|
|
280
|
+
),
|
|
281
|
+
/* @__PURE__ */ jsx("span", { className: `transition-transform shrink-0 text-muted-foreground ${expanded ? "rotate-180" : ""}`, children: /* @__PURE__ */ jsx(ChevronDownIcon, { size: 14 }) }),
|
|
282
|
+
/* @__PURE__ */ jsx(
|
|
283
|
+
"button",
|
|
284
|
+
{
|
|
285
|
+
onClick: (e) => {
|
|
286
|
+
e.stopPropagation();
|
|
287
|
+
onDelete(target.id);
|
|
288
|
+
},
|
|
289
|
+
className: "shrink-0 p-1 text-muted-foreground hover:text-[--destructive] rounded transition-colors",
|
|
290
|
+
children: /* @__PURE__ */ jsx(TrashIcon, { size: 12 })
|
|
291
|
+
}
|
|
292
|
+
)
|
|
229
293
|
] }),
|
|
230
|
-
/* @__PURE__ */
|
|
231
|
-
|
|
294
|
+
/* @__PURE__ */ jsx(AnimatePresence, { children: expanded && /* @__PURE__ */ jsx(
|
|
295
|
+
motion.div,
|
|
232
296
|
{
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
/* @__PURE__ */
|
|
240
|
-
|
|
241
|
-
|
|
297
|
+
initial: { height: 0, opacity: 0 },
|
|
298
|
+
animate: { height: "auto", opacity: 1 },
|
|
299
|
+
exit: { height: 0, opacity: 0 },
|
|
300
|
+
transition: { duration: 0.2 },
|
|
301
|
+
className: "overflow-hidden",
|
|
302
|
+
children: /* @__PURE__ */ jsxs("div", { className: "border-t border-white/[0.06] px-4 py-3 flex flex-col gap-3", children: [
|
|
303
|
+
/* @__PURE__ */ jsxs("div", { className: "grid grid-cols-2 sm:grid-cols-4 gap-3", children: [
|
|
304
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
305
|
+
/* @__PURE__ */ jsx("span", { className: "text-[10px] font-mono text-muted-foreground uppercase tracking-wider", children: "Type" }),
|
|
306
|
+
/* @__PURE__ */ jsxs("p", { className: "text-xs font-mono mt-0.5", children: [
|
|
307
|
+
icon,
|
|
308
|
+
" ",
|
|
309
|
+
target.type
|
|
310
|
+
] })
|
|
311
|
+
] }),
|
|
312
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
313
|
+
/* @__PURE__ */ jsx("span", { className: "text-[10px] font-mono text-muted-foreground uppercase tracking-wider", children: "Status" }),
|
|
314
|
+
/* @__PURE__ */ jsx("p", { className: "text-xs font-mono mt-0.5", children: target.status.replace("_", " ") })
|
|
315
|
+
] }),
|
|
316
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
317
|
+
/* @__PURE__ */ jsx("span", { className: "text-[10px] font-mono text-muted-foreground uppercase tracking-wider", children: "Added" }),
|
|
318
|
+
/* @__PURE__ */ jsx("p", { className: "text-xs font-mono mt-0.5", children: timeAgo(target.createdAt) })
|
|
319
|
+
] }),
|
|
320
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
321
|
+
/* @__PURE__ */ jsx("span", { className: "text-[10px] font-mono text-muted-foreground uppercase tracking-wider", children: "Last Scan" }),
|
|
322
|
+
/* @__PURE__ */ jsx("p", { className: "text-xs font-mono mt-0.5", children: target.lastScannedAt ? timeAgo(target.lastScannedAt) : "\u2014" })
|
|
323
|
+
] })
|
|
324
|
+
] }),
|
|
325
|
+
techs.length > 0 && /* @__PURE__ */ jsxs("div", { children: [
|
|
326
|
+
/* @__PURE__ */ jsx("span", { className: "text-[10px] font-mono text-muted-foreground uppercase tracking-wider", children: "Technologies" }),
|
|
327
|
+
/* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-1 mt-1", children: techs.map((t) => /* @__PURE__ */ jsx("span", { className: "inline-flex rounded-full bg-purple-500/10 text-purple-500 border border-purple-500/20 px-2 py-0.5 text-[9px] font-mono", children: t }, t)) })
|
|
328
|
+
] }),
|
|
329
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
330
|
+
/* @__PURE__ */ jsx("span", { className: "text-[10px] font-mono text-muted-foreground uppercase tracking-wider", children: "Notes" }),
|
|
331
|
+
/* @__PURE__ */ jsxs("div", { className: "flex gap-2 mt-1", children: [
|
|
332
|
+
/* @__PURE__ */ jsx(
|
|
333
|
+
"textarea",
|
|
334
|
+
{
|
|
335
|
+
value: notes,
|
|
336
|
+
onChange: (e) => setNotes(e.target.value),
|
|
337
|
+
rows: 2,
|
|
338
|
+
placeholder: "Add notes...",
|
|
339
|
+
className: "flex-1 text-[11px] border border-white/[0.06] rounded-md p-2 bg-black/20 font-mono placeholder:text-muted-foreground/50 focus:outline-none focus:border-[--cyan]/40 transition-colors resize-y"
|
|
340
|
+
}
|
|
341
|
+
),
|
|
342
|
+
/* @__PURE__ */ jsxs(
|
|
343
|
+
"button",
|
|
344
|
+
{
|
|
345
|
+
onClick: handleSaveNotes,
|
|
346
|
+
disabled: savingNotes,
|
|
347
|
+
className: "self-end inline-flex items-center gap-1 rounded-md px-2.5 py-1 text-[10px] font-mono font-medium bg-[--cyan]/10 text-[--cyan] border border-[--cyan]/20 hover:bg-[--cyan] hover:text-[--primary-foreground] transition-colors disabled:opacity-50",
|
|
348
|
+
children: [
|
|
349
|
+
savingNotes ? /* @__PURE__ */ jsx(SpinnerIcon, { size: 10 }) : /* @__PURE__ */ jsx(CheckIcon, { size: 10 }),
|
|
350
|
+
" Save"
|
|
351
|
+
]
|
|
352
|
+
}
|
|
353
|
+
)
|
|
354
|
+
] })
|
|
355
|
+
] })
|
|
356
|
+
] })
|
|
242
357
|
}
|
|
243
|
-
)
|
|
244
|
-
|
|
358
|
+
) })
|
|
359
|
+
]
|
|
360
|
+
}
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
function ImportDialog({ programId, onClose, onImported }) {
|
|
364
|
+
const [text, setText] = useState("");
|
|
365
|
+
const [importing, setImporting] = useState(false);
|
|
366
|
+
const [result, setResult] = useState(null);
|
|
367
|
+
const fileRef = useRef(null);
|
|
368
|
+
function handleFile(e) {
|
|
369
|
+
const file = e.target.files?.[0];
|
|
370
|
+
if (!file) return;
|
|
371
|
+
const reader = new FileReader();
|
|
372
|
+
reader.onload = (ev) => setText(ev.target.result);
|
|
373
|
+
reader.readAsText(file);
|
|
374
|
+
}
|
|
375
|
+
function parseTargets(raw) {
|
|
376
|
+
const lines = raw.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
377
|
+
return lines.map((line) => {
|
|
378
|
+
try {
|
|
379
|
+
const obj = JSON.parse(line);
|
|
380
|
+
if (obj.value) return obj;
|
|
381
|
+
} catch {
|
|
382
|
+
}
|
|
383
|
+
const parts = line.split(",").map((s) => s.trim());
|
|
384
|
+
const value = parts[0];
|
|
385
|
+
const type = TARGET_TYPES.includes(parts[1]) ? parts[1] : guessType(value);
|
|
386
|
+
return { value, type };
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
function guessType(v) {
|
|
390
|
+
if (/^\*\./.test(v)) return "wildcard";
|
|
391
|
+
if (/^https?:\/\//.test(v)) return "url";
|
|
392
|
+
if (/\/\d{1,2}$/.test(v)) return "cidr";
|
|
393
|
+
if (/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(v)) return "ip";
|
|
394
|
+
return "domain";
|
|
395
|
+
}
|
|
396
|
+
async function handleImport() {
|
|
397
|
+
const items = parseTargets(text);
|
|
398
|
+
if (items.length === 0) return;
|
|
399
|
+
setImporting(true);
|
|
400
|
+
const res = await bulkImportTargets(programId, items);
|
|
401
|
+
setResult(res);
|
|
402
|
+
setImporting(false);
|
|
403
|
+
if (!res.error) onImported();
|
|
404
|
+
}
|
|
405
|
+
return /* @__PURE__ */ jsxs(
|
|
406
|
+
motion.div,
|
|
407
|
+
{
|
|
408
|
+
initial: { opacity: 0, scale: 0.95 },
|
|
409
|
+
animate: { opacity: 1, scale: 1 },
|
|
410
|
+
exit: { opacity: 0, scale: 0.95 },
|
|
411
|
+
className: "fixed inset-0 z-50 flex items-center justify-center p-4",
|
|
412
|
+
children: [
|
|
413
|
+
/* @__PURE__ */ jsx("div", { className: "absolute inset-0 bg-black/60", onClick: onClose }),
|
|
414
|
+
/* @__PURE__ */ jsxs("div", { className: "relative w-full max-w-lg rounded-lg border border-[--cyan]/20 bg-[--card] shadow-2xl", children: [
|
|
415
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between p-4 border-b border-white/[0.06]", children: [
|
|
416
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
417
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
|
|
418
|
+
/* @__PURE__ */ jsx("div", { className: "w-2 h-2 rounded-full bg-[#ff5f57]" }),
|
|
419
|
+
/* @__PURE__ */ jsx("div", { className: "w-2 h-2 rounded-full bg-[#febc2e]" }),
|
|
420
|
+
/* @__PURE__ */ jsx("div", { className: "w-2 h-2 rounded-full bg-[#28c840]" })
|
|
421
|
+
] }),
|
|
422
|
+
/* @__PURE__ */ jsx("span", { className: "font-mono text-[10px] font-medium text-[--cyan] uppercase tracking-wider ml-1", children: "Import Targets" })
|
|
423
|
+
] }),
|
|
424
|
+
/* @__PURE__ */ jsx("button", { onClick: onClose, className: "text-muted-foreground hover:text-foreground", children: /* @__PURE__ */ jsx("span", { className: "text-lg", children: "\xD7" }) })
|
|
425
|
+
] }),
|
|
426
|
+
/* @__PURE__ */ jsxs("div", { className: "p-4", children: [
|
|
427
|
+
/* @__PURE__ */ jsx("p", { className: "text-xs font-mono text-muted-foreground mb-3", children: "Paste targets (one per line) or upload a file. Accepted: plain list, CSV (value,type), JSON." }),
|
|
428
|
+
/* @__PURE__ */ jsxs("div", { className: "flex gap-2 mb-3", children: [
|
|
429
|
+
/* @__PURE__ */ jsxs(
|
|
430
|
+
"button",
|
|
431
|
+
{
|
|
432
|
+
onClick: () => fileRef.current?.click(),
|
|
433
|
+
className: "inline-flex items-center gap-1.5 rounded-md px-3 py-1.5 text-xs font-mono font-medium border border-white/[0.06] hover:bg-white/[0.04] transition-colors",
|
|
434
|
+
children: [
|
|
435
|
+
/* @__PURE__ */ jsx(UploadIcon, { size: 12 }),
|
|
436
|
+
" Upload File"
|
|
437
|
+
]
|
|
438
|
+
}
|
|
439
|
+
),
|
|
440
|
+
/* @__PURE__ */ jsx("input", { ref: fileRef, type: "file", accept: ".txt,.csv,.json", className: "hidden", onChange: handleFile })
|
|
441
|
+
] }),
|
|
442
|
+
/* @__PURE__ */ jsx(
|
|
443
|
+
"textarea",
|
|
444
|
+
{
|
|
445
|
+
value: text,
|
|
446
|
+
onChange: (e) => setText(e.target.value),
|
|
447
|
+
rows: 8,
|
|
448
|
+
placeholder: "example.com\n*.api.example.com\n10.0.0.0/8,cidr\nhttps://app.example.com,url",
|
|
449
|
+
className: "w-full text-[11px] border border-white/[0.06] rounded-md p-3 bg-black/20 font-mono placeholder:text-muted-foreground/30 focus:outline-none focus:border-[--cyan]/40 focus:ring-1 focus:ring-[--cyan]/20 transition-colors resize-y"
|
|
450
|
+
}
|
|
451
|
+
),
|
|
452
|
+
text && /* @__PURE__ */ jsxs("p", { className: "text-[10px] font-mono text-muted-foreground mt-1", children: [
|
|
453
|
+
parseTargets(text).length,
|
|
454
|
+
" targets detected"
|
|
455
|
+
] }),
|
|
456
|
+
result && /* @__PURE__ */ jsx("p", { className: `text-xs font-mono mt-2 ${result.error ? "text-[--destructive]" : "text-green-500"}`, children: result.error || `${result.imported} targets imported` }),
|
|
457
|
+
/* @__PURE__ */ jsxs("div", { className: "flex justify-end gap-2 mt-3", children: [
|
|
458
|
+
/* @__PURE__ */ jsx("button", { onClick: onClose, className: "inline-flex items-center gap-1.5 rounded-md px-4 py-2 text-xs font-mono font-medium border border-white/[0.06] hover:bg-white/[0.04] transition-colors text-muted-foreground", children: "Cancel" }),
|
|
459
|
+
/* @__PURE__ */ jsxs(
|
|
460
|
+
"button",
|
|
461
|
+
{
|
|
462
|
+
onClick: handleImport,
|
|
463
|
+
disabled: importing || !text.trim(),
|
|
464
|
+
className: "inline-flex items-center gap-1.5 rounded-md px-4 py-2 text-xs font-mono font-medium bg-[--cyan]/10 text-[--cyan] border border-[--cyan]/20 hover:bg-[--cyan] hover:text-[--primary-foreground] transition-colors disabled:opacity-50",
|
|
465
|
+
children: [
|
|
466
|
+
importing ? /* @__PURE__ */ jsx(SpinnerIcon, { size: 12 }) : /* @__PURE__ */ jsx(UploadIcon, { size: 12 }),
|
|
467
|
+
" Import"
|
|
468
|
+
]
|
|
469
|
+
}
|
|
470
|
+
)
|
|
471
|
+
] })
|
|
472
|
+
] })
|
|
473
|
+
] })
|
|
245
474
|
]
|
|
246
475
|
}
|
|
247
476
|
);
|
|
@@ -262,7 +491,24 @@ function AddForm({ fields, onSubmit }) {
|
|
|
262
491
|
children: [
|
|
263
492
|
fields.map((f) => /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1", children: [
|
|
264
493
|
/* @__PURE__ */ jsx("label", { className: "text-[10px] text-muted-foreground font-mono uppercase tracking-wider", children: f.label }),
|
|
265
|
-
f.type === "select" ? /* @__PURE__ */ jsx(
|
|
494
|
+
f.type === "select" ? /* @__PURE__ */ jsx(
|
|
495
|
+
"select",
|
|
496
|
+
{
|
|
497
|
+
value: values[f.name] || "",
|
|
498
|
+
onChange: (e) => setValues({ ...values, [f.name]: e.target.value }),
|
|
499
|
+
className: "text-xs border border-white/[0.06] rounded-md px-2 py-1.5 bg-black/20 font-mono focus:outline-none focus:border-[--cyan]/40 transition-colors",
|
|
500
|
+
children: f.options.map((o) => /* @__PURE__ */ jsx("option", { value: o, children: o }, o))
|
|
501
|
+
}
|
|
502
|
+
) : /* @__PURE__ */ jsx(
|
|
503
|
+
"input",
|
|
504
|
+
{
|
|
505
|
+
type: f.type || "text",
|
|
506
|
+
placeholder: f.placeholder,
|
|
507
|
+
value: values[f.name] || "",
|
|
508
|
+
onChange: (e) => setValues({ ...values, [f.name]: e.target.value }),
|
|
509
|
+
className: "text-xs border border-white/[0.06] rounded-md px-2 py-1.5 bg-black/20 font-mono min-w-[120px] placeholder:text-muted-foreground/50 focus:outline-none focus:border-[--cyan]/40 transition-colors"
|
|
510
|
+
}
|
|
511
|
+
)
|
|
266
512
|
] }, f.name)),
|
|
267
513
|
/* @__PURE__ */ jsx("button", { onClick: () => {
|
|
268
514
|
onSubmit(values);
|
|
@@ -280,15 +526,27 @@ function AddForm({ fields, onSubmit }) {
|
|
|
280
526
|
function TargetsPage() {
|
|
281
527
|
const [programs_, setPrograms] = useState([]);
|
|
282
528
|
const [targets_, setTargets] = useState([]);
|
|
529
|
+
const [targetCounts, setTargetCounts] = useState({});
|
|
283
530
|
const [selectedProgram, setSelectedProgram] = useState(null);
|
|
284
531
|
const [loading, setLoading] = useState(true);
|
|
532
|
+
const [loadingTargets, setLoadingTargets] = useState(false);
|
|
285
533
|
const [syncStatus, setSyncStatus] = useState(null);
|
|
286
534
|
const [platformFilter, setPlatformFilter] = useState("all");
|
|
535
|
+
const [programSearch, setProgramSearch] = useState("");
|
|
536
|
+
const [targetSearch, setTargetSearch] = useState("");
|
|
537
|
+
const [typeFilter, setTypeFilter] = useState("all");
|
|
538
|
+
const [sortBy, setSortBy] = useState("default");
|
|
539
|
+
const [showImport, setShowImport] = useState(false);
|
|
540
|
+
const targetsRef = useRef(null);
|
|
287
541
|
async function load() {
|
|
288
|
-
const [p, ss] = await Promise.all([getPrograms(), getSyncStatus()]);
|
|
542
|
+
const [p, ss, tc2] = await Promise.all([getPrograms(), getSyncStatus(), getProgramTargetCounts()]);
|
|
289
543
|
setPrograms(p);
|
|
290
544
|
setSyncStatus(ss);
|
|
291
|
-
|
|
545
|
+
setTargetCounts(tc2);
|
|
546
|
+
if (p.length > 0 && !selectedProgram) {
|
|
547
|
+
const withTargets = p.find((prog) => tc2[prog.id]?.total > 0);
|
|
548
|
+
setSelectedProgram(withTargets ? withTargets.id : p[0].id);
|
|
549
|
+
}
|
|
292
550
|
setLoading(false);
|
|
293
551
|
}
|
|
294
552
|
async function loadTargets() {
|
|
@@ -296,8 +554,11 @@ function TargetsPage() {
|
|
|
296
554
|
setTargets([]);
|
|
297
555
|
return;
|
|
298
556
|
}
|
|
557
|
+
setLoadingTargets(true);
|
|
299
558
|
const t = await getTargets(selectedProgram);
|
|
300
559
|
setTargets(t);
|
|
560
|
+
setLoadingTargets(false);
|
|
561
|
+
if (targetsRef.current) targetsRef.current.scrollTop = 0;
|
|
301
562
|
}
|
|
302
563
|
useEffect(() => {
|
|
303
564
|
load();
|
|
@@ -329,17 +590,70 @@ function TargetsPage() {
|
|
|
329
590
|
async function handleAddTarget(values) {
|
|
330
591
|
await createTarget({ programId: selectedProgram, type: values.type || "domain", value: values.value || "" });
|
|
331
592
|
loadTargets();
|
|
593
|
+
load();
|
|
332
594
|
}
|
|
333
595
|
async function handleDeleteTarget(id) {
|
|
334
596
|
await deleteTarget(id);
|
|
335
597
|
loadTargets();
|
|
598
|
+
load();
|
|
336
599
|
}
|
|
337
600
|
async function handleStatusChange(id, status) {
|
|
338
601
|
await updateTarget(id, { status });
|
|
339
602
|
loadTargets();
|
|
340
603
|
}
|
|
341
|
-
|
|
342
|
-
|
|
604
|
+
async function handleUpdateTarget(id, data) {
|
|
605
|
+
await updateTarget(id, data);
|
|
606
|
+
loadTargets();
|
|
607
|
+
}
|
|
608
|
+
async function handleExport() {
|
|
609
|
+
if (!selectedProgram) return;
|
|
610
|
+
const t = await exportTargets(selectedProgram);
|
|
611
|
+
const prog = programs_.find((p) => p.id === selectedProgram);
|
|
612
|
+
const csv = ["value,type,status,notes", ...t.map((r) => `${r.value},${r.type},${r.status},${(r.notes || "").replace(/,/g, ";")}`)].join("\n");
|
|
613
|
+
const blob = new Blob([csv], { type: "text/csv" });
|
|
614
|
+
const url = URL.createObjectURL(blob);
|
|
615
|
+
const a = document.createElement("a");
|
|
616
|
+
a.href = url;
|
|
617
|
+
a.download = `${(prog?.name || "targets").replace(/\s+/g, "_").toLowerCase()}_targets.csv`;
|
|
618
|
+
a.click();
|
|
619
|
+
URL.revokeObjectURL(url);
|
|
620
|
+
}
|
|
621
|
+
const sortedPrograms = [...programs_].sort((a, b) => {
|
|
622
|
+
const aCount = targetCounts[a.id]?.total || 0;
|
|
623
|
+
const bCount = targetCounts[b.id]?.total || 0;
|
|
624
|
+
if (aCount > 0 && bCount === 0) return -1;
|
|
625
|
+
if (aCount === 0 && bCount > 0) return 1;
|
|
626
|
+
if (aCount !== bCount) return bCount - aCount;
|
|
627
|
+
return a.name.localeCompare(b.name);
|
|
628
|
+
});
|
|
629
|
+
const filteredPrograms = sortedPrograms.filter((p) => {
|
|
630
|
+
if (platformFilter !== "all" && p.platform !== platformFilter) return false;
|
|
631
|
+
if (programSearch && !p.name.toLowerCase().includes(programSearch.toLowerCase())) return false;
|
|
632
|
+
return true;
|
|
633
|
+
});
|
|
634
|
+
const selectedProg = programs_.find((p) => p.id === selectedProgram);
|
|
635
|
+
const tc = selectedProgram ? targetCounts[selectedProgram] || { total: 0, inScope: 0, outOfScope: 0 } : { total: 0, inScope: 0, outOfScope: 0 };
|
|
636
|
+
let displayTargets = [...targets_];
|
|
637
|
+
if (targetSearch) {
|
|
638
|
+
const q = targetSearch.toLowerCase();
|
|
639
|
+
displayTargets = displayTargets.filter((t) => t.value.toLowerCase().includes(q) || t.type.includes(q));
|
|
640
|
+
}
|
|
641
|
+
if (typeFilter !== "all") {
|
|
642
|
+
displayTargets = displayTargets.filter((t) => t.type === typeFilter);
|
|
643
|
+
}
|
|
644
|
+
if (sortBy === "type") {
|
|
645
|
+
displayTargets.sort((a, b) => a.type.localeCompare(b.type));
|
|
646
|
+
} else if (sortBy === "status") {
|
|
647
|
+
displayTargets.sort((a, b) => a.status.localeCompare(b.status));
|
|
648
|
+
} else if (sortBy === "alpha") {
|
|
649
|
+
displayTargets.sort((a, b) => a.value.localeCompare(b.value));
|
|
650
|
+
}
|
|
651
|
+
const typeGroups = {};
|
|
652
|
+
for (const t of displayTargets) {
|
|
653
|
+
typeGroups[t.type] = (typeGroups[t.type] || 0) + 1;
|
|
654
|
+
}
|
|
655
|
+
const totalInScope = targets_.filter((t) => t.status === "in_scope").length;
|
|
656
|
+
const totalTargets = Object.values(targetCounts).reduce((s, c) => s + c.total, 0);
|
|
343
657
|
if (loading) return /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-3", children: [...Array(3)].map((_, i) => /* @__PURE__ */ jsx("div", { className: "h-16 animate-pulse rounded-lg bg-white/[0.04] border border-white/[0.06]" }, i)) });
|
|
344
658
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
345
659
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between mb-6", children: [
|
|
@@ -353,8 +667,8 @@ function TargetsPage() {
|
|
|
353
667
|
" programs"
|
|
354
668
|
] }),
|
|
355
669
|
/* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-1.5 rounded-md px-2.5 py-1 text-[10px] font-mono font-medium bg-green-500/10 text-green-500 border border-green-500/20", children: [
|
|
356
|
-
|
|
357
|
-
"
|
|
670
|
+
totalTargets.toLocaleString(),
|
|
671
|
+
" targets"
|
|
358
672
|
] })
|
|
359
673
|
] })
|
|
360
674
|
] }),
|
|
@@ -387,8 +701,8 @@ function TargetsPage() {
|
|
|
387
701
|
p.id
|
|
388
702
|
))
|
|
389
703
|
] }),
|
|
390
|
-
/* @__PURE__ */ jsxs("div", { className: "grid grid-cols-1 lg:grid-cols-3 gap-6", children: [
|
|
391
|
-
/* @__PURE__ */ jsxs("div", { className: "lg:col-span-1", children: [
|
|
704
|
+
/* @__PURE__ */ jsxs("div", { className: "grid grid-cols-1 lg:grid-cols-3 gap-6", style: { minHeight: "60vh" }, children: [
|
|
705
|
+
/* @__PURE__ */ jsxs("div", { className: "lg:col-span-1 flex flex-col min-h-0", children: [
|
|
392
706
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between mb-3", children: [
|
|
393
707
|
/* @__PURE__ */ jsx("h2", { className: "text-[10px] font-mono font-medium uppercase tracking-wider text-muted-foreground", children: "Programs" }),
|
|
394
708
|
/* @__PURE__ */ jsx(
|
|
@@ -404,40 +718,184 @@ function TargetsPage() {
|
|
|
404
718
|
}
|
|
405
719
|
)
|
|
406
720
|
] }),
|
|
407
|
-
/* @__PURE__ */
|
|
721
|
+
/* @__PURE__ */ jsxs("div", { className: "relative mb-2", children: [
|
|
722
|
+
/* @__PURE__ */ jsx("div", { className: "absolute left-2.5 top-1/2 -translate-y-1/2 text-muted-foreground", children: /* @__PURE__ */ jsx(SearchIcon, { size: 12 }) }),
|
|
723
|
+
/* @__PURE__ */ jsx(
|
|
724
|
+
"input",
|
|
725
|
+
{
|
|
726
|
+
placeholder: "Search programs...",
|
|
727
|
+
value: programSearch,
|
|
728
|
+
onChange: (e) => setProgramSearch(e.target.value),
|
|
729
|
+
className: "w-full text-xs border border-white/[0.06] rounded-md pl-8 pr-3 py-1.5 bg-black/20 font-mono placeholder:text-muted-foreground/50 focus:outline-none focus:border-[--cyan]/40 transition-colors"
|
|
730
|
+
}
|
|
731
|
+
)
|
|
732
|
+
] }),
|
|
733
|
+
/* @__PURE__ */ jsx("div", { className: "flex flex-col gap-2 flex-1 overflow-y-auto scrollbar-thin", children: filteredPrograms.length === 0 ? /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center py-8 text-center", children: [
|
|
408
734
|
/* @__PURE__ */ jsx("div", { className: "rounded-full bg-white/[0.04] border border-white/[0.06] p-3 mb-3", children: /* @__PURE__ */ jsx(GlobeIcon, { size: 20 }) }),
|
|
409
735
|
/* @__PURE__ */ jsx("p", { className: "text-xs font-mono text-muted-foreground", children: "No programs yet" }),
|
|
410
736
|
/* @__PURE__ */ jsx("p", { className: "text-[10px] font-mono text-muted-foreground mt-1", children: "Sync from platforms or add manually" })
|
|
411
|
-
] }) : filteredPrograms.map((p, i) => /* @__PURE__ */ jsx(ProgramCard, { program: p, selected: selectedProgram === p.id, onSelect: setSelectedProgram, onDelete: handleDeleteProgram, index: i }, p.id)) })
|
|
737
|
+
] }) : filteredPrograms.map((p, i) => /* @__PURE__ */ jsx(ProgramCard, { program: p, selected: selectedProgram === p.id, onSelect: setSelectedProgram, onDelete: handleDeleteProgram, index: i, targetCount: targetCounts[p.id] }, p.id)) })
|
|
412
738
|
] }),
|
|
413
|
-
/* @__PURE__ */ jsxs("div", { className: "lg:col-span-2", children: [
|
|
739
|
+
/* @__PURE__ */ jsxs("div", { className: "lg:col-span-2 flex flex-col min-h-0", children: [
|
|
414
740
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between mb-3", children: [
|
|
415
741
|
/* @__PURE__ */ jsxs("h2", { className: "text-[10px] font-mono font-medium uppercase tracking-wider text-muted-foreground", children: [
|
|
416
742
|
"Targets ",
|
|
417
|
-
|
|
743
|
+
selectedProg && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
744
|
+
"\u2014",
|
|
745
|
+
" ",
|
|
746
|
+
selectedProg.name,
|
|
747
|
+
tc.total > 0 && /* @__PURE__ */ jsxs("span", { className: "text-[--cyan] ml-1", children: [
|
|
748
|
+
"(",
|
|
749
|
+
tc.total,
|
|
750
|
+
")"
|
|
751
|
+
] })
|
|
752
|
+
] })
|
|
418
753
|
] }),
|
|
419
|
-
selectedProgram && /* @__PURE__ */
|
|
420
|
-
|
|
754
|
+
selectedProgram && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
755
|
+
/* @__PURE__ */ jsxs(
|
|
756
|
+
"button",
|
|
757
|
+
{
|
|
758
|
+
onClick: () => setShowImport(true),
|
|
759
|
+
className: "inline-flex items-center gap-1.5 rounded-md px-2.5 py-1 text-xs font-mono font-medium border border-white/[0.06] hover:bg-white/[0.04] transition-colors text-muted-foreground hover:text-foreground",
|
|
760
|
+
children: [
|
|
761
|
+
/* @__PURE__ */ jsx(UploadIcon, { size: 12 }),
|
|
762
|
+
" Import"
|
|
763
|
+
]
|
|
764
|
+
}
|
|
765
|
+
),
|
|
766
|
+
targets_.length > 0 && /* @__PURE__ */ jsxs(
|
|
767
|
+
"button",
|
|
768
|
+
{
|
|
769
|
+
onClick: handleExport,
|
|
770
|
+
className: "inline-flex items-center gap-1.5 rounded-md px-2.5 py-1 text-xs font-mono font-medium border border-white/[0.06] hover:bg-white/[0.04] transition-colors text-muted-foreground hover:text-foreground",
|
|
771
|
+
children: [
|
|
772
|
+
/* @__PURE__ */ jsx(DownloadIcon, { size: 12 }),
|
|
773
|
+
" Export"
|
|
774
|
+
]
|
|
775
|
+
}
|
|
776
|
+
),
|
|
777
|
+
/* @__PURE__ */ jsx(
|
|
778
|
+
AddForm,
|
|
779
|
+
{
|
|
780
|
+
fields: [
|
|
781
|
+
{ name: "value", label: "Target", placeholder: "*.example.com" },
|
|
782
|
+
{ name: "type", label: "Type", type: "select", options: TARGET_TYPES }
|
|
783
|
+
],
|
|
784
|
+
onSubmit: handleAddTarget
|
|
785
|
+
}
|
|
786
|
+
)
|
|
787
|
+
] })
|
|
788
|
+
] }),
|
|
789
|
+
selectedProgram && targets_.length > 0 && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 mb-3 flex-wrap", children: [
|
|
790
|
+
/* @__PURE__ */ jsxs("div", { className: "relative flex-1 min-w-[150px]", children: [
|
|
791
|
+
/* @__PURE__ */ jsx("div", { className: "absolute left-2.5 top-1/2 -translate-y-1/2 text-muted-foreground", children: /* @__PURE__ */ jsx(SearchIcon, { size: 12 }) }),
|
|
792
|
+
/* @__PURE__ */ jsx(
|
|
793
|
+
"input",
|
|
794
|
+
{
|
|
795
|
+
placeholder: "Search targets...",
|
|
796
|
+
value: targetSearch,
|
|
797
|
+
onChange: (e) => setTargetSearch(e.target.value),
|
|
798
|
+
className: "w-full text-xs border border-white/[0.06] rounded-md pl-8 pr-3 py-1.5 bg-black/20 font-mono placeholder:text-muted-foreground/50 focus:outline-none focus:border-[--cyan]/40 transition-colors"
|
|
799
|
+
}
|
|
800
|
+
)
|
|
801
|
+
] }),
|
|
802
|
+
/* @__PURE__ */ jsxs(
|
|
803
|
+
"select",
|
|
421
804
|
{
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
805
|
+
value: typeFilter,
|
|
806
|
+
onChange: (e) => setTypeFilter(e.target.value),
|
|
807
|
+
className: "text-xs border border-white/[0.06] rounded-md px-2 py-1.5 bg-black/20 font-mono focus:outline-none focus:border-[--cyan]/40 transition-colors",
|
|
808
|
+
children: [
|
|
809
|
+
/* @__PURE__ */ jsx("option", { value: "all", children: "All types" }),
|
|
810
|
+
TARGET_TYPES.map((t) => /* @__PURE__ */ jsxs("option", { value: t, children: [
|
|
811
|
+
TYPE_ICONS[t],
|
|
812
|
+
" ",
|
|
813
|
+
t
|
|
814
|
+
] }, t))
|
|
815
|
+
]
|
|
816
|
+
}
|
|
817
|
+
),
|
|
818
|
+
/* @__PURE__ */ jsxs(
|
|
819
|
+
"select",
|
|
820
|
+
{
|
|
821
|
+
value: sortBy,
|
|
822
|
+
onChange: (e) => setSortBy(e.target.value),
|
|
823
|
+
className: "text-xs border border-white/[0.06] rounded-md px-2 py-1.5 bg-black/20 font-mono focus:outline-none focus:border-[--cyan]/40 transition-colors",
|
|
824
|
+
children: [
|
|
825
|
+
/* @__PURE__ */ jsx("option", { value: "default", children: "Recent" }),
|
|
826
|
+
/* @__PURE__ */ jsx("option", { value: "type", children: "By type" }),
|
|
827
|
+
/* @__PURE__ */ jsx("option", { value: "status", children: "By status" }),
|
|
828
|
+
/* @__PURE__ */ jsx("option", { value: "alpha", children: "A-Z" })
|
|
829
|
+
]
|
|
427
830
|
}
|
|
428
831
|
)
|
|
429
832
|
] }),
|
|
430
|
-
|
|
833
|
+
selectedProgram && Object.keys(typeGroups).length > 1 && /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-1.5 mb-3", children: Object.entries(typeGroups).sort((a, b) => b[1] - a[1]).map(([type, count]) => /* @__PURE__ */ jsxs(
|
|
834
|
+
"button",
|
|
835
|
+
{
|
|
836
|
+
onClick: () => setTypeFilter(typeFilter === type ? "all" : type),
|
|
837
|
+
className: `inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-[9px] font-mono font-medium border transition-colors cursor-pointer ${typeFilter === type ? "bg-[--cyan]/10 text-[--cyan] border-[--cyan]/20" : "bg-white/5 border-white/10 text-muted-foreground hover:text-foreground"}`,
|
|
838
|
+
children: [
|
|
839
|
+
TYPE_ICONS[type],
|
|
840
|
+
" ",
|
|
841
|
+
type,
|
|
842
|
+
" (",
|
|
843
|
+
count,
|
|
844
|
+
")"
|
|
845
|
+
]
|
|
846
|
+
},
|
|
847
|
+
type
|
|
848
|
+
)) }),
|
|
849
|
+
/* @__PURE__ */ jsx(AnimatePresence, { mode: "wait", children: !selectedProgram ? /* @__PURE__ */ jsxs(motion.div, { initial: { opacity: 0 }, animate: { opacity: 1 }, className: "flex flex-col items-center py-12 text-center flex-1", children: [
|
|
431
850
|
/* @__PURE__ */ jsx("div", { className: "rounded-full bg-white/[0.04] border border-white/[0.06] p-4 mb-4", children: /* @__PURE__ */ jsx(CrosshairIcon, { size: 24 }) }),
|
|
432
851
|
/* @__PURE__ */ jsx("p", { className: "text-sm font-mono font-medium mb-1", children: "Select a program" }),
|
|
433
852
|
/* @__PURE__ */ jsx("p", { className: "text-[11px] font-mono text-muted-foreground", children: "Choose a program to manage its targets" })
|
|
434
|
-
] }, "empty") :
|
|
853
|
+
] }, "empty") : loadingTargets ? /* @__PURE__ */ jsxs(motion.div, { initial: { opacity: 0 }, animate: { opacity: 1 }, className: "flex flex-col items-center py-12 text-center flex-1", children: [
|
|
854
|
+
/* @__PURE__ */ jsx(SpinnerIcon, { size: 24, className: "text-[--cyan] mb-3" }),
|
|
855
|
+
/* @__PURE__ */ jsx("p", { className: "text-xs font-mono text-muted-foreground", children: "Loading targets..." })
|
|
856
|
+
] }, "loading") : displayTargets.length === 0 && targets_.length === 0 ? /* @__PURE__ */ jsxs(motion.div, { initial: { opacity: 0 }, animate: { opacity: 1 }, className: "flex flex-col items-center py-12 text-center flex-1", children: [
|
|
435
857
|
/* @__PURE__ */ jsx("div", { className: "rounded-full bg-white/[0.04] border border-white/[0.06] p-4 mb-4", children: /* @__PURE__ */ jsx(CrosshairIcon, { size: 24 }) }),
|
|
436
858
|
/* @__PURE__ */ jsx("p", { className: "text-sm font-mono font-medium mb-1", children: "No targets yet" }),
|
|
437
|
-
/* @__PURE__ */ jsx("p", { className: "text-[11px] font-mono text-muted-foreground", children: "Add domains, IPs, and URLs to start hunting" })
|
|
438
|
-
|
|
859
|
+
/* @__PURE__ */ jsx("p", { className: "text-[11px] font-mono text-muted-foreground mb-4", children: "Add domains, IPs, and URLs to start hunting" }),
|
|
860
|
+
/* @__PURE__ */ jsx("div", { className: "flex gap-2", children: /* @__PURE__ */ jsxs(
|
|
861
|
+
"button",
|
|
862
|
+
{
|
|
863
|
+
onClick: () => setShowImport(true),
|
|
864
|
+
className: "inline-flex items-center gap-1.5 rounded-md px-3 py-1.5 text-xs font-mono font-medium bg-[--cyan]/10 text-[--cyan] border border-[--cyan]/20 hover:bg-[--cyan] hover:text-[--primary-foreground] transition-colors",
|
|
865
|
+
children: [
|
|
866
|
+
/* @__PURE__ */ jsx(UploadIcon, { size: 12 }),
|
|
867
|
+
" Import Targets"
|
|
868
|
+
]
|
|
869
|
+
}
|
|
870
|
+
) })
|
|
871
|
+
] }, "no-targets") : displayTargets.length === 0 ? /* @__PURE__ */ jsxs(motion.div, { initial: { opacity: 0 }, animate: { opacity: 1 }, className: "flex flex-col items-center py-12 text-center flex-1", children: [
|
|
872
|
+
/* @__PURE__ */ jsx("div", { className: "rounded-full bg-white/[0.04] border border-white/[0.06] p-4 mb-4", children: /* @__PURE__ */ jsx(SearchIcon, { size: 24 }) }),
|
|
873
|
+
/* @__PURE__ */ jsx("p", { className: "text-sm font-mono text-muted-foreground", children: "No targets match your filter" })
|
|
874
|
+
] }, "no-match") : /* @__PURE__ */ jsx(
|
|
875
|
+
motion.div,
|
|
876
|
+
{
|
|
877
|
+
initial: { opacity: 0 },
|
|
878
|
+
animate: { opacity: 1 },
|
|
879
|
+
ref: targetsRef,
|
|
880
|
+
className: "flex flex-col gap-2 flex-1 overflow-y-auto scrollbar-thin",
|
|
881
|
+
children: displayTargets.map((t, i) => /* @__PURE__ */ jsx(TargetRow, { target: t, onDelete: handleDeleteTarget, onStatusChange: handleStatusChange, onUpdate: handleUpdateTarget, index: i }, t.id))
|
|
882
|
+
},
|
|
883
|
+
"targets"
|
|
884
|
+
) })
|
|
439
885
|
] })
|
|
440
|
-
] })
|
|
886
|
+
] }),
|
|
887
|
+
/* @__PURE__ */ jsx(AnimatePresence, { children: showImport && selectedProgram && /* @__PURE__ */ jsx(
|
|
888
|
+
ImportDialog,
|
|
889
|
+
{
|
|
890
|
+
programId: selectedProgram,
|
|
891
|
+
onClose: () => setShowImport(false),
|
|
892
|
+
onImported: () => {
|
|
893
|
+
setShowImport(false);
|
|
894
|
+
loadTargets();
|
|
895
|
+
load();
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
) })
|
|
441
899
|
] });
|
|
442
900
|
}
|
|
443
901
|
export {
|