@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.
@@ -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: { color: 'bg-red-600/15 text-red-500 border-red-500/20', dot: 'bg-red-500', label: 'Critical' },
9
- high: { color: 'bg-orange-500/15 text-orange-500 border-orange-500/20', dot: 'bg-orange-500', label: 'High' },
10
- medium: { color: 'bg-yellow-500/15 text-yellow-500 border-yellow-500/20', dot: 'bg-yellow-500', label: 'Medium' },
11
- low: { color: 'bg-blue-500/15 text-blue-500 border-blue-500/20', dot: 'bg-blue-500', label: 'Low' },
12
- info: { color: 'bg-muted text-muted-foreground border-border', dot: 'bg-muted-foreground', label: '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-muted text-muted-foreground',
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-4 lg:grid-cols-6 gap-3 mb-6">
42
- {[
43
- { label: 'Total', value: counts.total, color: 'text-foreground' },
44
- { label: 'Critical', value: counts.critical, color: 'text-red-500' },
45
- { label: 'High', value: counts.high, color: 'text-orange-500' },
46
- { label: 'Confirmed', value: counts.confirmed, color: 'text-green-500' },
47
- { label: 'Reported', value: counts.reported, color: 'text-purple-500' },
48
- { label: 'Bounty', value: counts.totalBounty > 0 ? `$${counts.totalBounty.toLocaleString()}` : '$0', color: 'text-emerald-500' },
49
- ].map(s => (
50
- <div key={s.label} className="rounded-lg border bg-card p-3">
51
- <p className="text-[10px] text-muted-foreground uppercase tracking-wide">{s.label}</p>
52
- <p className={`text-xl font-semibold mt-0.5 ${s.color}`}>{s.value}</p>
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 className={`rounded-lg border bg-card ${sev.color.split(' ')[0].replace('/15', '/5').replace('/10', '/5')}`}>
65
- <button onClick={() => setExpanded(!expanded)} className="flex items-center gap-3 w-full text-left p-4 hover:bg-accent/30 rounded-lg">
66
- <div className={`shrink-0 w-2 h-2 rounded-full ${sev.dot}`} />
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.color}`}>{sev.label}</span>
71
- <span className="inline-flex rounded-full bg-muted px-2 py-0.5 text-[10px] font-medium text-muted-foreground">{finding.type}</span>
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-0 cursor-pointer ${STATUS_COLORS[finding.status] || ''}`}
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
- {expanded && (
90
- <div className="border-t px-4 py-3 flex flex-col gap-3">
91
- {finding.description && (
92
- <div>
93
- <p className="text-xs font-medium text-muted-foreground mb-1">Description</p>
94
- <p className="text-xs whitespace-pre-wrap">{finding.description}</p>
95
- </div>
96
- )}
97
- {finding.stepsToReproduce && (
98
- <div>
99
- <p className="text-xs font-medium text-muted-foreground mb-1">Steps to Reproduce</p>
100
- <pre className="text-xs bg-muted rounded-md p-3 whitespace-pre-wrap break-words font-mono overflow-auto max-h-48">{finding.stepsToReproduce}</pre>
101
- </div>
102
- )}
103
- {finding.impact && (
104
- <div>
105
- <p className="text-xs font-medium text-muted-foreground mb-1">Impact</p>
106
- <p className="text-xs">{finding.impact}</p>
107
- </div>
108
- )}
109
- {finding.rawOutput && (
110
- <div>
111
- <p className="text-xs font-medium text-muted-foreground mb-1">Raw Output</p>
112
- <pre className="text-xs bg-muted rounded-md p-3 whitespace-pre-wrap break-words font-mono overflow-auto max-h-32">{finding.rawOutput}</pre>
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
- <div className="flex items-center gap-2">
116
- {finding.bountyAmount > 0 && <span className="inline-flex rounded-full bg-emerald-500/10 text-emerald-500 px-2 py-0.5 text-[10px] font-medium">${finding.bountyAmount}</span>}
117
- {finding.reportUrl && <a href={finding.reportUrl} target="_blank" rel="noopener" className="text-[10px] text-blue-500 hover:underline">View Report</a>}
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/50" />)}</div>;
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-sm text-muted-foreground mt-1">Vulnerability discoveries across all agents and tools</p>
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 onClick={() => setShowAdd(!showAdd)} className="inline-flex items-center gap-1.5 rounded-md px-3 py-1.5 text-xs font-medium bg-foreground text-background hover:opacity-90">
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
- {showAdd && (
175
- <div className="rounded-lg border bg-card p-4 mb-4 flex flex-col gap-3">
176
- <div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
177
- <input placeholder="Title" value={newFinding.title} onChange={e => setNewFinding({ ...newFinding, title: e.target.value })} className="text-sm border rounded-md px-3 py-2 bg-background col-span-1 sm:col-span-3" />
178
- <select value={newFinding.severity} onChange={e => setNewFinding({ ...newFinding, severity: e.target.value })} className="text-sm border rounded-md px-3 py-2 bg-background">
179
- {Object.keys(SEVERITY_CONFIG).map(s => <option key={s} value={s}>{s}</option>)}
180
- </select>
181
- <select value={newFinding.type} onChange={e => setNewFinding({ ...newFinding, type: e.target.value })} className="text-sm border rounded-md px-3 py-2 bg-background">
182
- {FINDING_TYPES.map(t => <option key={t} value={t}>{t}</option>)}
183
- </select>
184
- </div>
185
- <textarea placeholder="Description..." value={newFinding.description} onChange={e => setNewFinding({ ...newFinding, description: e.target.value })} className="text-sm border rounded-md px-3 py-2 bg-background min-h-[80px]" />
186
- <div className="flex gap-2">
187
- <button onClick={handleCreate} className="inline-flex items-center gap-1 rounded-md px-3 py-1.5 text-xs font-medium bg-foreground text-background hover:opacity-90">Save</button>
188
- <button onClick={() => setShowAdd(false)} className="text-xs text-muted-foreground hover:text-foreground px-3 py-1.5">Cancel</button>
189
- </div>
190
- </div>
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
- <button key={f} onClick={() => setFilter(f)} className={`px-3 py-1 rounded-full text-xs font-medium transition-colors ${filter === f ? 'bg-foreground text-background' : 'bg-muted text-muted-foreground hover:text-foreground'}`}>
197
- {f === 'all' ? `All (${findings_.length})` : `${SEVERITY_CONFIG[f].label} (${findings_.filter(x => x.severity === f).length})`}
198
- </button>
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-muted p-4 mb-4"><ShieldIcon size={24} /></div>
206
- <p className="text-sm font-medium mb-1">No findings yet</p>
207
- <p className="text-xs text-muted-foreground max-w-sm">Findings will appear here as your agents discover vulnerabilities, or add them manually.</p>
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">{filtered.map(f => <FindingCard key={f.id} finding={f} onUpdate={handleUpdate} onDelete={handleDelete} />)}</div>
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';