@harbinger-ai/harbinger 0.1.1 → 0.1.3
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/chat/actions.js +416 -0
- package/lib/chat/components/agents-page.js +545 -0
- package/lib/chat/components/agents-page.jsx +571 -0
- package/lib/chat/components/app-sidebar.js +33 -1
- package/lib/chat/components/app-sidebar.jsx +37 -1
- package/lib/chat/components/findings-page.js +164 -103
- package/lib/chat/components/findings-page.jsx +156 -101
- package/lib/chat/components/icons.js +62 -0
- package/lib/chat/components/icons.jsx +62 -0
- package/lib/chat/components/index.js +3 -0
- package/lib/chat/components/mcp-page.js +383 -55
- package/lib/chat/components/mcp-page.jsx +404 -101
- package/lib/chat/components/mission-control.js +490 -0
- package/lib/chat/components/mission-control.jsx +618 -0
- package/lib/chat/components/registry-page.js +267 -133
- package/lib/chat/components/registry-page.jsx +299 -138
- package/lib/chat/components/settings-layout.js +3 -2
- package/lib/chat/components/settings-layout.jsx +2 -1
- package/lib/chat/components/settings-providers-page.js +337 -0
- package/lib/chat/components/settings-providers-page.jsx +410 -0
- package/lib/chat/components/settings-secrets-page.js +91 -66
- package/lib/chat/components/settings-secrets-page.jsx +83 -72
- package/lib/chat/components/targets-page.js +269 -200
- package/lib/chat/components/targets-page.jsx +181 -111
- package/lib/mcp/actions.js +120 -0
- package/lib/mcp/registry.js +164 -0
- package/package.json +1 -1
|
@@ -1,26 +1,27 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { useState, useEffect } from 'react';
|
|
4
|
+
import { motion, AnimatePresence } from 'framer-motion';
|
|
4
5
|
import { ShieldIcon, PlusIcon, ChevronDownIcon, SpinnerIcon } from './icons.js';
|
|
5
6
|
import { getFindings, createFinding, updateFinding, deleteFinding, getFindingCounts } from '../../bounty/actions.js';
|
|
6
7
|
|
|
7
8
|
const SEVERITY_CONFIG = {
|
|
8
|
-
critical: {
|
|
9
|
-
high: {
|
|
10
|
-
medium: {
|
|
11
|
-
low: {
|
|
12
|
-
info: {
|
|
9
|
+
critical: { bg: 'bg-red-600/15', text: 'text-red-500', border: 'border-red-500/20', dot: 'bg-red-500', glow: 'shadow-[0_0_20px_oklch(0.6_0.22_25/20%)]', label: 'Critical' },
|
|
10
|
+
high: { bg: 'bg-orange-500/15', text: 'text-orange-500', border: 'border-orange-500/20', dot: 'bg-orange-500', glow: 'shadow-[0_0_20px_oklch(0.7_0.17_55/20%)]', label: 'High' },
|
|
11
|
+
medium: { bg: 'bg-yellow-500/15', text: 'text-yellow-500', border: 'border-yellow-500/20', dot: 'bg-yellow-500', glow: '', label: 'Medium' },
|
|
12
|
+
low: { bg: 'bg-blue-500/15', text: 'text-blue-500', border: 'border-blue-500/20', dot: 'bg-blue-500', glow: '', label: 'Low' },
|
|
13
|
+
info: { bg: 'bg-white/5', text: 'text-muted-foreground', border: 'border-white/10', dot: 'bg-muted-foreground', glow: '', label: 'Info' },
|
|
13
14
|
};
|
|
14
15
|
|
|
15
16
|
const STATUS_FLOW = ['new', 'triaging', 'confirmed', 'reported', 'duplicate', 'resolved', 'bounty_paid'];
|
|
16
17
|
const STATUS_COLORS = {
|
|
17
|
-
new: 'bg-blue-500/10 text-blue-500',
|
|
18
|
-
triaging: 'bg-yellow-500/10 text-yellow-500',
|
|
19
|
-
confirmed: 'bg-green-500/10 text-green-500',
|
|
20
|
-
reported: 'bg-purple-500/10 text-purple-500',
|
|
21
|
-
duplicate: 'bg-
|
|
22
|
-
resolved: 'bg-emerald-500/10 text-emerald-500',
|
|
23
|
-
bounty_paid: 'bg-green-600/10 text-green-600',
|
|
18
|
+
new: { bg: 'bg-blue-500/10', text: 'text-blue-500', border: 'border-blue-500/20' },
|
|
19
|
+
triaging: { bg: 'bg-yellow-500/10', text: 'text-yellow-500', border: 'border-yellow-500/20' },
|
|
20
|
+
confirmed: { bg: 'bg-green-500/10', text: 'text-green-500', border: 'border-green-500/20' },
|
|
21
|
+
reported: { bg: 'bg-purple-500/10', text: 'text-purple-500', border: 'border-purple-500/20' },
|
|
22
|
+
duplicate: { bg: 'bg-white/5', text: 'text-muted-foreground', border: 'border-white/10' },
|
|
23
|
+
resolved: { bg: 'bg-emerald-500/10', text: 'text-emerald-500', border: 'border-emerald-500/20' },
|
|
24
|
+
bounty_paid: { bg: 'bg-green-600/10', text: 'text-green-600', border: 'border-green-600/20' },
|
|
24
25
|
};
|
|
25
26
|
|
|
26
27
|
const FINDING_TYPES = ['xss', 'sqli', 'ssrf', 'idor', 'rce', 'lfi', 'open_redirect', 'subdomain_takeover', 'info_disclosure', 'misconfig', 'auth_bypass', 'rate_limit', 'cors', 'xxe', 'csrf', 'other'];
|
|
@@ -37,40 +38,54 @@ function timeAgo(ts) {
|
|
|
37
38
|
|
|
38
39
|
function StatsBar({ counts }) {
|
|
39
40
|
if (!counts) return null;
|
|
41
|
+
const stats = [
|
|
42
|
+
{ label: 'Total', value: counts.total, text: 'text-[--cyan]', glow: 'glow-cyan' },
|
|
43
|
+
{ label: 'Critical', value: counts.critical, text: 'text-red-500', glow: counts.critical > 0 ? 'shadow-[0_0_20px_oklch(0.6_0.22_25/15%)]' : '' },
|
|
44
|
+
{ label: 'High', value: counts.high, text: 'text-orange-500', glow: counts.high > 0 ? 'shadow-[0_0_20px_oklch(0.7_0.17_55/15%)]' : '' },
|
|
45
|
+
{ label: 'Confirmed', value: counts.confirmed, text: 'text-green-500', glow: '' },
|
|
46
|
+
{ label: 'Reported', value: counts.reported, text: 'text-purple-500', glow: '' },
|
|
47
|
+
{ label: 'Bounty', value: counts.totalBounty > 0 ? `$${counts.totalBounty.toLocaleString()}` : '$0', text: 'text-emerald-500', glow: counts.totalBounty > 0 ? 'shadow-[0_0_20px_oklch(0.7_0.17_160/15%)]' : '' },
|
|
48
|
+
];
|
|
49
|
+
|
|
40
50
|
return (
|
|
41
|
-
<div className="grid grid-cols-2 sm:grid-cols-
|
|
42
|
-
{
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
<p className=
|
|
52
|
-
|
|
53
|
-
</div>
|
|
51
|
+
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-6 gap-3 mb-6">
|
|
52
|
+
{stats.map((s, i) => (
|
|
53
|
+
<motion.div
|
|
54
|
+
key={s.label}
|
|
55
|
+
initial={{ opacity: 0, y: 8 }}
|
|
56
|
+
animate={{ opacity: 1, y: 0 }}
|
|
57
|
+
transition={{ duration: 0.25, delay: i * 0.05 }}
|
|
58
|
+
className={`rounded-lg border border-white/[0.06] bg-[--card] p-3 ${s.glow}`}
|
|
59
|
+
>
|
|
60
|
+
<p className="text-[10px] text-muted-foreground uppercase tracking-wider font-mono">{s.label}</p>
|
|
61
|
+
<p className={`text-xl font-mono font-semibold mt-0.5 ${s.text}`}>{s.value}</p>
|
|
62
|
+
</motion.div>
|
|
54
63
|
))}
|
|
55
64
|
</div>
|
|
56
65
|
);
|
|
57
66
|
}
|
|
58
67
|
|
|
59
|
-
function FindingCard({ finding, onUpdate, onDelete }) {
|
|
68
|
+
function FindingCard({ finding, onUpdate, onDelete, index }) {
|
|
60
69
|
const [expanded, setExpanded] = useState(false);
|
|
61
70
|
const sev = SEVERITY_CONFIG[finding.severity] || SEVERITY_CONFIG.info;
|
|
71
|
+
const st = STATUS_COLORS[finding.status] || STATUS_COLORS.new;
|
|
62
72
|
|
|
63
73
|
return (
|
|
64
|
-
<div
|
|
65
|
-
|
|
66
|
-
|
|
74
|
+
<motion.div
|
|
75
|
+
initial={{ opacity: 0, y: 8 }}
|
|
76
|
+
animate={{ opacity: 1, y: 0 }}
|
|
77
|
+
transition={{ duration: 0.25, delay: index * 0.03 }}
|
|
78
|
+
className={`rounded-lg border border-white/[0.06] bg-[--card] hover:border-[--cyan]/20 transition-all ${sev.glow}`}
|
|
79
|
+
>
|
|
80
|
+
<button onClick={() => setExpanded(!expanded)} className="flex items-center gap-3 w-full text-left p-4 hover:bg-white/[0.02] rounded-lg transition-colors">
|
|
81
|
+
<div className={`shrink-0 w-2.5 h-2.5 rounded-full ${sev.dot}`} />
|
|
67
82
|
<div className="flex-1 min-w-0">
|
|
68
|
-
<p className="text-sm font-medium truncate">{finding.title}</p>
|
|
83
|
+
<p className="text-sm font-mono font-medium truncate">{finding.title}</p>
|
|
69
84
|
<div className="flex items-center gap-2 mt-0.5 flex-wrap">
|
|
70
|
-
<span className={`inline-flex rounded-full px-2 py-0.5 text-[10px] font-medium border ${sev.
|
|
71
|
-
<span className="inline-flex rounded-full bg-
|
|
72
|
-
{finding.agentId && <span className="inline-flex rounded-full bg-purple-500/10 px-2 py-0.5 text-[10px] font-medium text-purple-500">{finding.agentId}</span>}
|
|
73
|
-
<span className="text-[10px] text-muted-foreground">{timeAgo(finding.createdAt)}</span>
|
|
85
|
+
<span className={`inline-flex rounded-full px-2 py-0.5 text-[10px] font-mono font-medium border ${sev.bg} ${sev.text} ${sev.border}`}>{sev.label}</span>
|
|
86
|
+
<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">{finding.type}</span>
|
|
87
|
+
{finding.agentId && <span className="inline-flex rounded-full bg-purple-500/10 border border-purple-500/20 px-2 py-0.5 text-[10px] font-mono font-medium text-purple-500">{finding.agentId}</span>}
|
|
88
|
+
<span className="text-[10px] font-mono text-muted-foreground">{timeAgo(finding.createdAt)}</span>
|
|
74
89
|
</div>
|
|
75
90
|
</div>
|
|
76
91
|
<div className="flex items-center gap-2 shrink-0">
|
|
@@ -78,7 +93,7 @@ function FindingCard({ finding, onUpdate, onDelete }) {
|
|
|
78
93
|
value={finding.status}
|
|
79
94
|
onClick={e => e.stopPropagation()}
|
|
80
95
|
onChange={e => { e.stopPropagation(); onUpdate(finding.id, { status: e.target.value }); }}
|
|
81
|
-
className={`text-[10px] font-medium rounded-full px-2 py-0.5 border
|
|
96
|
+
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`}
|
|
82
97
|
>
|
|
83
98
|
{STATUS_FLOW.map(s => <option key={s} value={s}>{s.replace('_', ' ')}</option>)}
|
|
84
99
|
</select>
|
|
@@ -86,41 +101,51 @@ function FindingCard({ finding, onUpdate, onDelete }) {
|
|
|
86
101
|
</div>
|
|
87
102
|
</button>
|
|
88
103
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
<div>
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
104
|
+
<AnimatePresence>
|
|
105
|
+
{expanded && (
|
|
106
|
+
<motion.div
|
|
107
|
+
initial={{ height: 0, opacity: 0 }}
|
|
108
|
+
animate={{ height: 'auto', opacity: 1 }}
|
|
109
|
+
exit={{ height: 0, opacity: 0 }}
|
|
110
|
+
transition={{ duration: 0.2 }}
|
|
111
|
+
className="overflow-hidden"
|
|
112
|
+
>
|
|
113
|
+
<div className="border-t border-white/[0.06] px-4 py-3 flex flex-col gap-3">
|
|
114
|
+
{finding.description && (
|
|
115
|
+
<div>
|
|
116
|
+
<p className="text-[10px] font-mono font-medium text-muted-foreground uppercase tracking-wider mb-1.5">Description</p>
|
|
117
|
+
<p className="text-xs font-mono whitespace-pre-wrap text-foreground/80">{finding.description}</p>
|
|
118
|
+
</div>
|
|
119
|
+
)}
|
|
120
|
+
{finding.stepsToReproduce && (
|
|
121
|
+
<div>
|
|
122
|
+
<p className="text-[10px] font-mono font-medium text-muted-foreground uppercase tracking-wider mb-1.5">Steps to Reproduce</p>
|
|
123
|
+
<pre className="text-[11px] bg-black/30 border border-white/[0.04] rounded-md p-3 whitespace-pre-wrap break-words font-mono overflow-auto max-h-48 text-foreground/80 scrollbar-thin">{finding.stepsToReproduce}</pre>
|
|
124
|
+
</div>
|
|
125
|
+
)}
|
|
126
|
+
{finding.impact && (
|
|
127
|
+
<div>
|
|
128
|
+
<p className="text-[10px] font-mono font-medium text-muted-foreground uppercase tracking-wider mb-1.5">Impact</p>
|
|
129
|
+
<p className="text-xs font-mono text-foreground/80">{finding.impact}</p>
|
|
130
|
+
</div>
|
|
131
|
+
)}
|
|
132
|
+
{finding.rawOutput && (
|
|
133
|
+
<div>
|
|
134
|
+
<p className="text-[10px] font-mono font-medium text-muted-foreground uppercase tracking-wider mb-1.5">Raw Output</p>
|
|
135
|
+
<pre className="text-[11px] bg-black/30 border border-white/[0.04] rounded-md p-3 whitespace-pre-wrap break-words font-mono overflow-auto max-h-32 text-foreground/80 scrollbar-thin">{finding.rawOutput}</pre>
|
|
136
|
+
</div>
|
|
137
|
+
)}
|
|
138
|
+
<div className="flex items-center gap-2 pt-1">
|
|
139
|
+
{finding.bountyAmount > 0 && <span className="inline-flex rounded-full bg-emerald-500/10 text-emerald-500 border border-emerald-500/20 px-2 py-0.5 text-[10px] font-mono font-medium">${finding.bountyAmount}</span>}
|
|
140
|
+
{finding.reportUrl && <a href={finding.reportUrl} target="_blank" rel="noopener" className="text-[10px] font-mono text-[--cyan] hover:underline">View Report</a>}
|
|
141
|
+
<div className="flex-1" />
|
|
142
|
+
<button onClick={() => onDelete(finding.id)} className="text-[10px] font-mono text-muted-foreground hover:text-[--destructive] transition-colors">Delete</button>
|
|
143
|
+
</div>
|
|
113
144
|
</div>
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
<div className="flex-1" />
|
|
119
|
-
<button onClick={() => onDelete(finding.id)} className="text-[10px] text-muted-foreground hover:text-destructive">Delete</button>
|
|
120
|
-
</div>
|
|
121
|
-
</div>
|
|
122
|
-
)}
|
|
123
|
-
</div>
|
|
145
|
+
</motion.div>
|
|
146
|
+
)}
|
|
147
|
+
</AnimatePresence>
|
|
148
|
+
</motion.div>
|
|
124
149
|
);
|
|
125
150
|
}
|
|
126
151
|
|
|
@@ -154,16 +179,20 @@ export function FindingsPage() {
|
|
|
154
179
|
|
|
155
180
|
const filtered = filter === 'all' ? findings_ : findings_.filter(f => f.severity === filter);
|
|
156
181
|
|
|
157
|
-
if (loading) return <div className="flex flex-col gap-3">{[...Array(4)].map((_, i) => <div key={i} className="h-16 animate-pulse rounded-lg bg-border/
|
|
182
|
+
if (loading) return <div className="flex flex-col gap-3">{[...Array(4)].map((_, i) => <div key={i} className="h-16 animate-pulse rounded-lg bg-white/[0.04] border border-white/[0.06]" />)}</div>;
|
|
158
183
|
|
|
159
184
|
return (
|
|
160
185
|
<>
|
|
186
|
+
{/* Header */}
|
|
161
187
|
<div className="flex items-center justify-between mb-4">
|
|
162
188
|
<div>
|
|
163
|
-
<h1 className="text-2xl font-semibold">Findings</h1>
|
|
164
|
-
<p className="text-
|
|
189
|
+
<h1 className="text-2xl font-mono font-semibold text-[--cyan] text-glow-cyan">Findings</h1>
|
|
190
|
+
<p className="text-[11px] text-muted-foreground mt-1 font-mono">Vulnerability discoveries across all agents and tools</p>
|
|
165
191
|
</div>
|
|
166
|
-
<button
|
|
192
|
+
<button
|
|
193
|
+
onClick={() => setShowAdd(!showAdd)}
|
|
194
|
+
className="inline-flex items-center gap-1.5 rounded-md px-3 py-1.5 text-xs font-mono font-medium bg-[--cyan] text-[--primary-foreground] hover:opacity-90 transition-opacity"
|
|
195
|
+
>
|
|
167
196
|
<PlusIcon size={12} /> New Finding
|
|
168
197
|
</button>
|
|
169
198
|
</div>
|
|
@@ -171,43 +200,69 @@ export function FindingsPage() {
|
|
|
171
200
|
<StatsBar counts={counts} />
|
|
172
201
|
|
|
173
202
|
{/* Add form */}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
<div
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
203
|
+
<AnimatePresence>
|
|
204
|
+
{showAdd && (
|
|
205
|
+
<motion.div
|
|
206
|
+
initial={{ opacity: 0, height: 0 }}
|
|
207
|
+
animate={{ opacity: 1, height: 'auto' }}
|
|
208
|
+
exit={{ opacity: 0, height: 0 }}
|
|
209
|
+
transition={{ duration: 0.2 }}
|
|
210
|
+
className="overflow-hidden"
|
|
211
|
+
>
|
|
212
|
+
<div className="rounded-lg border border-white/[0.06] bg-[--card] p-4 mb-4 flex flex-col gap-3">
|
|
213
|
+
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
|
|
214
|
+
<input placeholder="Title" value={newFinding.title} onChange={e => setNewFinding({ ...newFinding, title: e.target.value })} className="text-sm border border-white/[0.06] rounded-md px-3 py-2 bg-black/20 col-span-1 sm:col-span-3 font-mono placeholder:text-muted-foreground/50 focus:outline-none focus:border-[--cyan]/40 focus:ring-1 focus:ring-[--cyan]/20 transition-colors" />
|
|
215
|
+
<select value={newFinding.severity} onChange={e => setNewFinding({ ...newFinding, severity: e.target.value })} className="text-sm border border-white/[0.06] rounded-md px-3 py-2 bg-black/20 font-mono focus:outline-none focus:border-[--cyan]/40 transition-colors">
|
|
216
|
+
{Object.keys(SEVERITY_CONFIG).map(s => <option key={s} value={s}>{s}</option>)}
|
|
217
|
+
</select>
|
|
218
|
+
<select value={newFinding.type} onChange={e => setNewFinding({ ...newFinding, type: e.target.value })} className="text-sm border border-white/[0.06] rounded-md px-3 py-2 bg-black/20 font-mono focus:outline-none focus:border-[--cyan]/40 transition-colors">
|
|
219
|
+
{FINDING_TYPES.map(t => <option key={t} value={t}>{t}</option>)}
|
|
220
|
+
</select>
|
|
221
|
+
</div>
|
|
222
|
+
<textarea placeholder="Description..." value={newFinding.description} onChange={e => setNewFinding({ ...newFinding, description: e.target.value })} className="text-sm border border-white/[0.06] rounded-md px-3 py-2 bg-black/20 min-h-[80px] font-mono placeholder:text-muted-foreground/50 focus:outline-none focus:border-[--cyan]/40 focus:ring-1 focus:ring-[--cyan]/20 transition-colors" />
|
|
223
|
+
<div className="flex gap-2">
|
|
224
|
+
<button onClick={handleCreate} className="inline-flex items-center gap-1 rounded-md px-3 py-1.5 text-xs font-mono font-medium bg-[--cyan] text-[--primary-foreground] hover:opacity-90 transition-opacity">Save</button>
|
|
225
|
+
<button onClick={() => setShowAdd(false)} className="text-xs font-mono text-muted-foreground hover:text-foreground px-3 py-1.5 transition-colors">Cancel</button>
|
|
226
|
+
</div>
|
|
227
|
+
</div>
|
|
228
|
+
</motion.div>
|
|
229
|
+
)}
|
|
230
|
+
</AnimatePresence>
|
|
192
231
|
|
|
193
232
|
{/* Filter tabs */}
|
|
194
|
-
<div className="flex gap-1 mb-4 overflow-x-auto">
|
|
195
|
-
{['all', ...Object.keys(SEVERITY_CONFIG)].map(f =>
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
233
|
+
<div className="flex gap-1.5 mb-4 overflow-x-auto scrollbar-thin">
|
|
234
|
+
{['all', ...Object.keys(SEVERITY_CONFIG)].map(f => {
|
|
235
|
+
const isActive = filter === f;
|
|
236
|
+
const sev = SEVERITY_CONFIG[f];
|
|
237
|
+
return (
|
|
238
|
+
<button
|
|
239
|
+
key={f}
|
|
240
|
+
onClick={() => setFilter(f)}
|
|
241
|
+
className={`shrink-0 px-3 py-1 rounded-full text-[10px] font-mono font-medium border transition-colors ${
|
|
242
|
+
isActive
|
|
243
|
+
? f === 'all'
|
|
244
|
+
? 'bg-[--cyan]/10 text-[--cyan] border-[--cyan]/20'
|
|
245
|
+
: `${sev.bg} ${sev.text} ${sev.border}`
|
|
246
|
+
: 'border-white/[0.06] text-muted-foreground hover:text-foreground hover:border-white/[0.12]'
|
|
247
|
+
}`}
|
|
248
|
+
>
|
|
249
|
+
{f === 'all' ? `All (${findings_.length})` : `${sev.label} (${findings_.filter(x => x.severity === f).length})`}
|
|
250
|
+
</button>
|
|
251
|
+
);
|
|
252
|
+
})}
|
|
200
253
|
</div>
|
|
201
254
|
|
|
202
255
|
{/* Findings list */}
|
|
203
256
|
{filtered.length === 0 ? (
|
|
204
257
|
<div className="flex flex-col items-center justify-center py-16 text-center">
|
|
205
|
-
<div className="rounded-full bg-
|
|
206
|
-
<p className="text-sm font-medium mb-1">No findings yet</p>
|
|
207
|
-
<p className="text-
|
|
258
|
+
<div className="rounded-full bg-white/[0.04] border border-white/[0.06] p-4 mb-4"><ShieldIcon size={24} /></div>
|
|
259
|
+
<p className="text-sm font-mono font-medium mb-1">No findings yet</p>
|
|
260
|
+
<p className="text-[11px] font-mono text-muted-foreground max-w-sm">Findings will appear here as your agents discover vulnerabilities, or add them manually.</p>
|
|
208
261
|
</div>
|
|
209
262
|
) : (
|
|
210
|
-
<div className="flex flex-col gap-2">
|
|
263
|
+
<div className="flex flex-col gap-2">
|
|
264
|
+
{filtered.map((f, i) => <FindingCard key={f.id} finding={f} onUpdate={handleUpdate} onDelete={handleDelete} index={i} />)}
|
|
265
|
+
</div>
|
|
211
266
|
)}
|
|
212
267
|
</>
|
|
213
268
|
);
|
|
@@ -712,6 +712,63 @@ function PlugIcon({ size = 16 }) {
|
|
|
712
712
|
}
|
|
713
713
|
);
|
|
714
714
|
}
|
|
715
|
+
function CommandIcon({ size = 16 }) {
|
|
716
|
+
return /* @__PURE__ */ jsxs(
|
|
717
|
+
"svg",
|
|
718
|
+
{
|
|
719
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
720
|
+
viewBox: "0 0 24 24",
|
|
721
|
+
fill: "none",
|
|
722
|
+
stroke: "currentColor",
|
|
723
|
+
strokeWidth: 2,
|
|
724
|
+
strokeLinecap: "round",
|
|
725
|
+
strokeLinejoin: "round",
|
|
726
|
+
width: size,
|
|
727
|
+
height: size,
|
|
728
|
+
children: [
|
|
729
|
+
/* @__PURE__ */ jsx("path", { d: "M2 12a5 5 0 0 0 5 5 8 8 0 0 1 5 2 8 8 0 0 1 5-2 5 5 0 0 0 5-5V7h-5a8 8 0 0 0-5 2 8 8 0 0 0-5-2H2Z" }),
|
|
730
|
+
/* @__PURE__ */ jsx("path", { d: "M6 11c1.5 0 3 .5 3 2-2 0-3 0-3-2Z" }),
|
|
731
|
+
/* @__PURE__ */ jsx("path", { d: "M18 11c-1.5 0-3 .5-3 2 2 0 3 0 3-2Z" })
|
|
732
|
+
]
|
|
733
|
+
}
|
|
734
|
+
);
|
|
735
|
+
}
|
|
736
|
+
function CpuIcon({ size = 16 }) {
|
|
737
|
+
return /* @__PURE__ */ jsxs("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", width: size, height: size, children: [
|
|
738
|
+
/* @__PURE__ */ jsx("rect", { x: "4", y: "4", width: "16", height: "16", rx: "2" }),
|
|
739
|
+
/* @__PURE__ */ jsx("rect", { x: "9", y: "9", width: "6", height: "6" }),
|
|
740
|
+
/* @__PURE__ */ jsx("path", { d: "M15 2v2" }),
|
|
741
|
+
/* @__PURE__ */ jsx("path", { d: "M15 20v2" }),
|
|
742
|
+
/* @__PURE__ */ jsx("path", { d: "M2 15h2" }),
|
|
743
|
+
/* @__PURE__ */ jsx("path", { d: "M2 9h2" }),
|
|
744
|
+
/* @__PURE__ */ jsx("path", { d: "M20 15h2" }),
|
|
745
|
+
/* @__PURE__ */ jsx("path", { d: "M20 9h2" }),
|
|
746
|
+
/* @__PURE__ */ jsx("path", { d: "M9 2v2" }),
|
|
747
|
+
/* @__PURE__ */ jsx("path", { d: "M9 20v2" })
|
|
748
|
+
] });
|
|
749
|
+
}
|
|
750
|
+
function UsersIcon({ size = 16 }) {
|
|
751
|
+
return /* @__PURE__ */ jsxs("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", width: size, height: size, children: [
|
|
752
|
+
/* @__PURE__ */ jsx("path", { d: "M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" }),
|
|
753
|
+
/* @__PURE__ */ jsx("circle", { cx: "9", cy: "7", r: "4" }),
|
|
754
|
+
/* @__PURE__ */ jsx("path", { d: "M22 21v-2a4 4 0 0 0-3-3.87" }),
|
|
755
|
+
/* @__PURE__ */ jsx("path", { d: "M16 3.13a4 4 0 0 1 0 7.75" })
|
|
756
|
+
] });
|
|
757
|
+
}
|
|
758
|
+
function EyeIcon({ size = 16 }) {
|
|
759
|
+
return /* @__PURE__ */ jsxs("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", width: size, height: size, children: [
|
|
760
|
+
/* @__PURE__ */ jsx("path", { d: "M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0" }),
|
|
761
|
+
/* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "3" })
|
|
762
|
+
] });
|
|
763
|
+
}
|
|
764
|
+
function EyeOffIcon({ size = 16 }) {
|
|
765
|
+
return /* @__PURE__ */ jsxs("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", width: size, height: size, children: [
|
|
766
|
+
/* @__PURE__ */ jsx("path", { d: "M10.733 5.076a10.744 10.744 0 0 1 11.205 6.575 1 1 0 0 1 0 .696 10.747 10.747 0 0 1-1.444 2.49" }),
|
|
767
|
+
/* @__PURE__ */ jsx("path", { d: "M14.084 14.158a3 3 0 0 1-4.242-4.242" }),
|
|
768
|
+
/* @__PURE__ */ jsx("path", { d: "M17.479 17.499a10.75 10.75 0 0 1-15.417-5.151 1 1 0 0 1 0-.696 10.75 10.75 0 0 1 4.446-5.143" }),
|
|
769
|
+
/* @__PURE__ */ jsx("path", { d: "m2 2 20 20" })
|
|
770
|
+
] });
|
|
771
|
+
}
|
|
715
772
|
function LogOutIcon({ size = 16 }) {
|
|
716
773
|
return /* @__PURE__ */ jsxs(
|
|
717
774
|
"svg",
|
|
@@ -741,9 +798,13 @@ export {
|
|
|
741
798
|
ChevronDownIcon,
|
|
742
799
|
CirclePlusIcon,
|
|
743
800
|
ClockIcon,
|
|
801
|
+
CommandIcon,
|
|
744
802
|
CopyIcon,
|
|
803
|
+
CpuIcon,
|
|
745
804
|
CrosshairIcon,
|
|
746
805
|
DownloadIcon,
|
|
806
|
+
EyeIcon,
|
|
807
|
+
EyeOffIcon,
|
|
747
808
|
FileTextIcon,
|
|
748
809
|
GlobeIcon,
|
|
749
810
|
KeyIcon,
|
|
@@ -771,6 +832,7 @@ export {
|
|
|
771
832
|
SunIcon,
|
|
772
833
|
SwarmIcon,
|
|
773
834
|
TrashIcon,
|
|
835
|
+
UsersIcon,
|
|
774
836
|
WrenchIcon,
|
|
775
837
|
XIcon,
|
|
776
838
|
ZapIcon
|
|
@@ -720,6 +720,68 @@ export function PlugIcon({ size = 16 }) {
|
|
|
720
720
|
);
|
|
721
721
|
}
|
|
722
722
|
|
|
723
|
+
export function CommandIcon({ size = 16 }) {
|
|
724
|
+
return (
|
|
725
|
+
<svg
|
|
726
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
727
|
+
viewBox="0 0 24 24"
|
|
728
|
+
fill="none"
|
|
729
|
+
stroke="currentColor"
|
|
730
|
+
strokeWidth={2}
|
|
731
|
+
strokeLinecap="round"
|
|
732
|
+
strokeLinejoin="round"
|
|
733
|
+
width={size}
|
|
734
|
+
height={size}
|
|
735
|
+
>
|
|
736
|
+
<path d="M2 12a5 5 0 0 0 5 5 8 8 0 0 1 5 2 8 8 0 0 1 5-2 5 5 0 0 0 5-5V7h-5a8 8 0 0 0-5 2 8 8 0 0 0-5-2H2Z" />
|
|
737
|
+
<path d="M6 11c1.5 0 3 .5 3 2-2 0-3 0-3-2Z" />
|
|
738
|
+
<path d="M18 11c-1.5 0-3 .5-3 2 2 0 3 0 3-2Z" />
|
|
739
|
+
</svg>
|
|
740
|
+
);
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
export function CpuIcon({ size = 16 }) {
|
|
744
|
+
return (
|
|
745
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" width={size} height={size}>
|
|
746
|
+
<rect x="4" y="4" width="16" height="16" rx="2" />
|
|
747
|
+
<rect x="9" y="9" width="6" height="6" />
|
|
748
|
+
<path d="M15 2v2" /><path d="M15 20v2" /><path d="M2 15h2" /><path d="M2 9h2" />
|
|
749
|
+
<path d="M20 15h2" /><path d="M20 9h2" /><path d="M9 2v2" /><path d="M9 20v2" />
|
|
750
|
+
</svg>
|
|
751
|
+
);
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
export function UsersIcon({ size = 16 }) {
|
|
755
|
+
return (
|
|
756
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" width={size} height={size}>
|
|
757
|
+
<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" />
|
|
758
|
+
<circle cx="9" cy="7" r="4" />
|
|
759
|
+
<path d="M22 21v-2a4 4 0 0 0-3-3.87" />
|
|
760
|
+
<path d="M16 3.13a4 4 0 0 1 0 7.75" />
|
|
761
|
+
</svg>
|
|
762
|
+
);
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
export function EyeIcon({ size = 16 }) {
|
|
766
|
+
return (
|
|
767
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" width={size} height={size}>
|
|
768
|
+
<path d="M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0" />
|
|
769
|
+
<circle cx="12" cy="12" r="3" />
|
|
770
|
+
</svg>
|
|
771
|
+
);
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
export function EyeOffIcon({ size = 16 }) {
|
|
775
|
+
return (
|
|
776
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" width={size} height={size}>
|
|
777
|
+
<path d="M10.733 5.076a10.744 10.744 0 0 1 11.205 6.575 1 1 0 0 1 0 .696 10.747 10.747 0 0 1-1.444 2.49" />
|
|
778
|
+
<path d="M14.084 14.158a3 3 0 0 1-4.242-4.242" />
|
|
779
|
+
<path d="M17.479 17.499a10.75 10.75 0 0 1-15.417-5.151 1 1 0 0 1 0-.696 10.75 10.75 0 0 1 4.446-5.143" />
|
|
780
|
+
<path d="m2 2 20 20" />
|
|
781
|
+
</svg>
|
|
782
|
+
);
|
|
783
|
+
}
|
|
784
|
+
|
|
723
785
|
export function LogOutIcon({ size = 16 }) {
|
|
724
786
|
return (
|
|
725
787
|
<svg
|
|
@@ -7,10 +7,13 @@ export { TriggersPage } from './triggers-page.js';
|
|
|
7
7
|
export { PageLayout } from './page-layout.js';
|
|
8
8
|
export { SettingsLayout } from './settings-layout.js';
|
|
9
9
|
export { SettingsSecretsPage } from './settings-secrets-page.js';
|
|
10
|
+
export { SettingsProvidersPage } from './settings-providers-page.js';
|
|
10
11
|
export { McpPage } from './mcp-page.js';
|
|
12
|
+
export { AgentsPage } from './agents-page.js';
|
|
11
13
|
export { TargetsPage } from './targets-page.js';
|
|
12
14
|
export { FindingsPage } from './findings-page.js';
|
|
13
15
|
export { RegistryPage } from './registry-page.js';
|
|
16
|
+
export { MissionControlPage } from './mission-control.js';
|
|
14
17
|
export { AppSidebar } from './app-sidebar.js';
|
|
15
18
|
export { SidebarHistory } from './sidebar-history.js';
|
|
16
19
|
export { SidebarHistoryItem } from './sidebar-history-item.js';
|