openhacker 0.1.0 → 0.1.2
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/README.md +2 -3
- package/bin/openhacker +1 -1
- package/package.json +3 -3
- package/src/index.d.ts +1 -0
- package/src/index.js +305 -1
- package/templates/agent/.env.example +0 -7
- package/templates/agent/README.md +1 -2
- package/templates/agent/agent/agent.ts +1 -5
- package/templates/agent/agent/channels/eve.ts +7 -0
- package/templates/agent/agent/instructions.md +7 -45
- package/templates/agent/app/globals.css +65 -197
- package/templates/agent/app/layout.tsx +2 -22
- package/templates/agent/app/page.tsx +80 -102
- package/templates/agent/package.json +2 -3
- package/src/cli.js +0 -153
- package/src/index.ts +0 -1
- package/templates/agent/agent/lib/auth.ts +0 -23
- package/templates/agent/agent/lib/github.ts +0 -74
- package/templates/agent/agent/lib/osv.ts +0 -152
- package/templates/agent/agent/lib/scan.ts +0 -153
- package/templates/agent/agent/lib/store.ts +0 -151
- package/templates/agent/agent/lib/types.ts +0 -63
- package/templates/agent/agent/schedules/daily_audit.ts +0 -20
- package/templates/agent/agent/tools/check_advisories.ts +0 -27
- package/templates/agent/agent/tools/list_targets.ts +0 -21
- package/templates/agent/agent/tools/read_repo_file.ts +0 -31
- package/templates/agent/agent/tools/report_finding.ts +0 -59
- package/templates/agent/agent/tools/run_dependency_scan.ts +0 -16
- package/templates/agent/app/_components/ui.tsx +0 -29
- package/templates/agent/app/actions.ts +0 -120
- package/templates/agent/app/api/scan/route.ts +0 -34
- package/templates/agent/app/login/page.tsx +0 -40
- package/templates/agent/app/settings/page.tsx +0 -92
- package/templates/agent/app/targets/[id]/page.tsx +0 -127
- package/templates/agent/proxy.ts +0 -21
|
@@ -8,11 +8,6 @@
|
|
|
8
8
|
--accent: #ffffff;
|
|
9
9
|
--accent-dim: #333333;
|
|
10
10
|
--crit: #ffffff;
|
|
11
|
-
--high: #cfcfcf;
|
|
12
|
-
--med: #9a9a9a;
|
|
13
|
-
--low: #6f6f6f;
|
|
14
|
-
--info: #555555;
|
|
15
|
-
--ok: #f5f5f5;
|
|
16
11
|
}
|
|
17
12
|
|
|
18
13
|
* {
|
|
@@ -27,254 +22,127 @@ body {
|
|
|
27
22
|
color: var(--text);
|
|
28
23
|
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
29
24
|
font-size: 14px;
|
|
30
|
-
line-height: 1.
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
a {
|
|
34
|
-
color: var(--accent);
|
|
35
|
-
text-decoration: none;
|
|
36
|
-
}
|
|
37
|
-
a:hover {
|
|
38
|
-
text-decoration: underline;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
.nav {
|
|
42
|
-
display: flex;
|
|
43
|
-
align-items: center;
|
|
44
|
-
gap: 20px;
|
|
45
|
-
padding: 14px 24px;
|
|
46
|
-
border-bottom: 1px solid var(--border);
|
|
47
|
-
background: var(--panel);
|
|
48
|
-
}
|
|
49
|
-
.nav .brand {
|
|
50
|
-
font-weight: 700;
|
|
51
|
-
letter-spacing: 0.5px;
|
|
52
|
-
color: var(--text);
|
|
53
|
-
}
|
|
54
|
-
.nav .brand span {
|
|
55
|
-
color: var(--accent);
|
|
56
|
-
}
|
|
57
|
-
.nav .spacer {
|
|
58
|
-
flex: 1;
|
|
59
|
-
}
|
|
60
|
-
.nav a {
|
|
61
|
-
color: var(--muted);
|
|
62
|
-
}
|
|
63
|
-
.nav a:hover {
|
|
64
|
-
color: var(--text);
|
|
65
|
-
text-decoration: none;
|
|
25
|
+
line-height: 1.6;
|
|
66
26
|
}
|
|
67
27
|
|
|
68
28
|
.container {
|
|
69
|
-
max-width:
|
|
29
|
+
max-width: 720px;
|
|
70
30
|
margin: 0 auto;
|
|
71
|
-
padding:
|
|
31
|
+
padding: 64px 24px 96px;
|
|
72
32
|
}
|
|
73
33
|
|
|
74
34
|
h1 {
|
|
75
|
-
font-size:
|
|
35
|
+
font-size: 22px;
|
|
36
|
+
font-weight: 700;
|
|
37
|
+
letter-spacing: 0.5px;
|
|
76
38
|
margin: 0 0 4px;
|
|
77
39
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
margin: 28px 0 12px;
|
|
81
|
-
color: var(--muted);
|
|
82
|
-
text-transform: uppercase;
|
|
83
|
-
letter-spacing: 1px;
|
|
40
|
+
h1 span {
|
|
41
|
+
color: var(--accent);
|
|
84
42
|
}
|
|
85
43
|
.sub {
|
|
86
44
|
color: var(--muted);
|
|
87
|
-
margin: 0 0
|
|
45
|
+
margin: 0 0 28px;
|
|
88
46
|
}
|
|
89
47
|
|
|
90
|
-
.
|
|
91
|
-
background: var(--panel);
|
|
92
|
-
border: 1px solid var(--border);
|
|
93
|
-
border-radius: 10px;
|
|
94
|
-
padding: 18px;
|
|
95
|
-
margin-bottom: 16px;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
.card {
|
|
99
|
-
background: var(--panel);
|
|
100
|
-
border: 1px solid var(--border);
|
|
101
|
-
border-radius: 10px;
|
|
102
|
-
padding: 16px 18px;
|
|
103
|
-
margin-bottom: 12px;
|
|
48
|
+
.ask {
|
|
104
49
|
display: flex;
|
|
105
|
-
|
|
106
|
-
gap: 16px;
|
|
50
|
+
gap: 8px;
|
|
107
51
|
}
|
|
108
|
-
.
|
|
52
|
+
.ask input {
|
|
109
53
|
flex: 1;
|
|
110
|
-
min-width: 0;
|
|
111
|
-
}
|
|
112
|
-
.card .repo {
|
|
113
|
-
font-weight: 600;
|
|
114
|
-
}
|
|
115
|
-
.card .meta {
|
|
116
|
-
color: var(--muted);
|
|
117
|
-
font-size: 12px;
|
|
118
|
-
margin-top: 2px;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
label {
|
|
122
|
-
display: block;
|
|
123
|
-
font-size: 12px;
|
|
124
|
-
color: var(--muted);
|
|
125
|
-
margin-bottom: 6px;
|
|
126
|
-
}
|
|
127
|
-
input[type="text"],
|
|
128
|
-
input[type="password"],
|
|
129
|
-
select {
|
|
130
|
-
width: 100%;
|
|
131
54
|
background: var(--panel-2);
|
|
132
55
|
border: 1px solid var(--border);
|
|
133
56
|
color: var(--text);
|
|
134
57
|
border-radius: 8px;
|
|
135
|
-
padding:
|
|
58
|
+
padding: 11px 13px;
|
|
136
59
|
font: inherit;
|
|
137
60
|
}
|
|
138
|
-
input:focus
|
|
139
|
-
select:focus {
|
|
61
|
+
.ask input:focus {
|
|
140
62
|
outline: none;
|
|
141
63
|
border-color: var(--accent);
|
|
142
64
|
}
|
|
143
|
-
.
|
|
144
|
-
display: flex;
|
|
145
|
-
gap: 12px;
|
|
146
|
-
flex-wrap: wrap;
|
|
147
|
-
margin-bottom: 12px;
|
|
148
|
-
}
|
|
149
|
-
.row > div {
|
|
150
|
-
flex: 1;
|
|
151
|
-
min-width: 160px;
|
|
152
|
-
}
|
|
153
|
-
.check {
|
|
154
|
-
display: flex;
|
|
155
|
-
align-items: center;
|
|
156
|
-
gap: 8px;
|
|
157
|
-
}
|
|
158
|
-
.check label {
|
|
159
|
-
margin: 0;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
button,
|
|
163
|
-
.btn {
|
|
65
|
+
.ask button {
|
|
164
66
|
background: var(--accent);
|
|
165
67
|
color: #000000;
|
|
166
68
|
border: none;
|
|
167
69
|
border-radius: 8px;
|
|
168
|
-
padding:
|
|
70
|
+
padding: 11px 18px;
|
|
169
71
|
font: inherit;
|
|
170
72
|
font-weight: 600;
|
|
171
73
|
cursor: pointer;
|
|
172
74
|
}
|
|
173
|
-
button:hover {
|
|
75
|
+
.ask button:hover:not(:disabled) {
|
|
174
76
|
filter: brightness(1.08);
|
|
175
77
|
}
|
|
176
|
-
.
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
border: 1px solid var(--border);
|
|
180
|
-
}
|
|
181
|
-
.btn-ghost:hover {
|
|
182
|
-
color: var(--text);
|
|
183
|
-
}
|
|
184
|
-
.btn-danger {
|
|
185
|
-
background: transparent;
|
|
186
|
-
color: var(--crit);
|
|
187
|
-
border: 1px solid var(--accent-dim);
|
|
78
|
+
.ask button:disabled {
|
|
79
|
+
opacity: 0.5;
|
|
80
|
+
cursor: not-allowed;
|
|
188
81
|
}
|
|
189
82
|
|
|
190
|
-
.
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
border-radius: 20px;
|
|
194
|
-
font-size: 11px;
|
|
195
|
-
font-weight: 700;
|
|
196
|
-
text-transform: uppercase;
|
|
197
|
-
letter-spacing: 0.5px;
|
|
83
|
+
.reply {
|
|
84
|
+
margin-top: 28px;
|
|
85
|
+
background: var(--panel);
|
|
198
86
|
border: 1px solid var(--border);
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
}
|
|
202
|
-
.sev-critical {
|
|
203
|
-
background: #ffffff;
|
|
204
|
-
color: #000000;
|
|
205
|
-
border-color: #ffffff;
|
|
206
|
-
}
|
|
207
|
-
.sev-high {
|
|
208
|
-
color: #ffffff;
|
|
209
|
-
border-color: #cfcfcf;
|
|
210
|
-
background: rgba(255, 255, 255, 0.1);
|
|
87
|
+
border-radius: 10px;
|
|
88
|
+
padding: 18px 20px;
|
|
211
89
|
}
|
|
212
|
-
.
|
|
213
|
-
|
|
214
|
-
|
|
90
|
+
.reply .text {
|
|
91
|
+
white-space: pre-wrap;
|
|
92
|
+
margin: 0 0 12px;
|
|
215
93
|
}
|
|
216
|
-
.
|
|
217
|
-
|
|
218
|
-
border-color: #3a3a3a;
|
|
94
|
+
.reply .text:last-child {
|
|
95
|
+
margin-bottom: 0;
|
|
219
96
|
}
|
|
220
|
-
.
|
|
221
|
-
|
|
222
|
-
|
|
97
|
+
.reply .reasoning {
|
|
98
|
+
white-space: pre-wrap;
|
|
99
|
+
color: var(--muted);
|
|
100
|
+
font-size: 13px;
|
|
101
|
+
margin: 0 0 12px;
|
|
102
|
+
padding-left: 12px;
|
|
103
|
+
border-left: 2px solid var(--border);
|
|
223
104
|
}
|
|
224
105
|
|
|
225
|
-
.
|
|
106
|
+
.tool {
|
|
226
107
|
display: flex;
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
border-collapse: collapse;
|
|
108
|
+
align-items: center;
|
|
109
|
+
gap: 10px;
|
|
110
|
+
font-size: 12.5px;
|
|
111
|
+
color: var(--muted);
|
|
112
|
+
padding: 4px 0;
|
|
233
113
|
}
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
text-align: left;
|
|
237
|
-
padding: 10px 12px;
|
|
238
|
-
border-bottom: 1px solid var(--border);
|
|
239
|
-
vertical-align: top;
|
|
240
|
-
font-size: 13px;
|
|
114
|
+
.tool-name {
|
|
115
|
+
color: var(--text);
|
|
241
116
|
}
|
|
242
|
-
|
|
243
|
-
color: var(--muted);
|
|
117
|
+
.tool-state {
|
|
244
118
|
font-size: 11px;
|
|
245
119
|
text-transform: uppercase;
|
|
246
120
|
letter-spacing: 0.5px;
|
|
121
|
+
border: 1px solid var(--border);
|
|
122
|
+
border-radius: 20px;
|
|
123
|
+
padding: 0 8px;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.cursor {
|
|
127
|
+
display: inline-block;
|
|
128
|
+
width: 8px;
|
|
129
|
+
height: 1em;
|
|
130
|
+
background: var(--accent);
|
|
131
|
+
vertical-align: text-bottom;
|
|
132
|
+
animation: blink 1s steps(2) infinite;
|
|
133
|
+
}
|
|
134
|
+
@keyframes blink {
|
|
135
|
+
50% {
|
|
136
|
+
opacity: 0;
|
|
137
|
+
}
|
|
247
138
|
}
|
|
248
139
|
|
|
249
140
|
.banner {
|
|
141
|
+
margin-top: 20px;
|
|
250
142
|
border: 1px solid var(--border);
|
|
251
143
|
background: rgba(255, 255, 255, 0.04);
|
|
252
|
-
color: var(--
|
|
144
|
+
color: var(--crit);
|
|
253
145
|
border-radius: 8px;
|
|
254
146
|
padding: 10px 14px;
|
|
255
|
-
margin-bottom: 18px;
|
|
256
147
|
font-size: 13px;
|
|
257
148
|
}
|
|
258
|
-
.empty {
|
|
259
|
-
color: var(--muted);
|
|
260
|
-
padding: 24px;
|
|
261
|
-
text-align: center;
|
|
262
|
-
border: 1px dashed var(--border);
|
|
263
|
-
border-radius: 10px;
|
|
264
|
-
}
|
|
265
|
-
.inline {
|
|
266
|
-
display: inline;
|
|
267
|
-
}
|
|
268
|
-
.actions {
|
|
269
|
-
display: flex;
|
|
270
|
-
gap: 8px;
|
|
271
|
-
align-items: center;
|
|
272
|
-
}
|
|
273
|
-
.mono-sm {
|
|
274
|
-
font-size: 12px;
|
|
275
|
-
color: var(--muted);
|
|
276
|
-
}
|
|
277
|
-
.login-wrap {
|
|
278
|
-
max-width: 360px;
|
|
279
|
-
margin: 12vh auto 0;
|
|
280
|
-
}
|
|
@@ -1,35 +1,15 @@
|
|
|
1
1
|
import type { Metadata } from "next";
|
|
2
|
-
import Link from "next/link";
|
|
3
|
-
import { authEnabled } from "@/agent/lib/auth";
|
|
4
|
-
import { logout } from "./actions";
|
|
5
2
|
import "./globals.css";
|
|
6
3
|
|
|
7
4
|
export const metadata: Metadata = {
|
|
8
5
|
title: "OpenHacker",
|
|
9
|
-
description: "
|
|
6
|
+
description: "Analyze a GitHub repo for vulnerabilities",
|
|
10
7
|
};
|
|
11
8
|
|
|
12
9
|
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
13
10
|
return (
|
|
14
11
|
<html lang="en">
|
|
15
|
-
<body>
|
|
16
|
-
<nav className="nav">
|
|
17
|
-
<Link href="/" className="brand">
|
|
18
|
-
open<span>hacker</span>
|
|
19
|
-
</Link>
|
|
20
|
-
<div className="spacer" />
|
|
21
|
-
<Link href="/">Dashboard</Link>
|
|
22
|
-
<Link href="/settings">Settings</Link>
|
|
23
|
-
{authEnabled() ? (
|
|
24
|
-
<form action={logout} className="inline">
|
|
25
|
-
<button className="btn-ghost" type="submit">
|
|
26
|
-
Sign out
|
|
27
|
-
</button>
|
|
28
|
-
</form>
|
|
29
|
-
) : null}
|
|
30
|
-
</nav>
|
|
31
|
-
{children}
|
|
32
|
-
</body>
|
|
12
|
+
<body>{children}</body>
|
|
33
13
|
</html>
|
|
34
14
|
);
|
|
35
15
|
}
|
|
@@ -1,114 +1,92 @@
|
|
|
1
|
-
|
|
2
|
-
import { getStore, isPersistent } from "@/agent/lib/store";
|
|
3
|
-
import { SeverityCounts } from "./_components/ui";
|
|
4
|
-
import { addTarget, deleteTarget, scanTarget } from "./actions";
|
|
1
|
+
"use client";
|
|
5
2
|
|
|
6
|
-
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { useEveAgent } from "eve/react";
|
|
7
5
|
|
|
8
|
-
export default
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
6
|
+
export default function Home() {
|
|
7
|
+
const [repo, setRepo] = useState("");
|
|
8
|
+
const agent = useEveAgent();
|
|
9
|
+
|
|
10
|
+
const busy = agent.status === "submitted" || agent.status === "streaming";
|
|
11
|
+
|
|
12
|
+
function onSubmit(e: React.FormEvent) {
|
|
13
|
+
e.preventDefault();
|
|
14
|
+
const value = repo.trim();
|
|
15
|
+
if (!value || busy) return;
|
|
16
|
+
agent.reset();
|
|
17
|
+
agent.send({
|
|
18
|
+
message: `Analyze the GitHub repository "${value}" for security vulnerabilities. Walk through what you check and report what you find.`,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const reply = [...agent.data.messages]
|
|
23
|
+
.reverse()
|
|
24
|
+
.find((m) => m.role === "assistant");
|
|
21
25
|
|
|
22
26
|
return (
|
|
23
27
|
<main className="container">
|
|
24
|
-
<h1>
|
|
25
|
-
|
|
28
|
+
<h1>
|
|
29
|
+
open<span>hacker</span>
|
|
30
|
+
</h1>
|
|
31
|
+
<p className="sub">
|
|
32
|
+
Paste a GitHub repo and the agent will analyze it for vulnerabilities.
|
|
33
|
+
</p>
|
|
34
|
+
|
|
35
|
+
<form className="ask" onSubmit={onSubmit}>
|
|
36
|
+
<input
|
|
37
|
+
type="text"
|
|
38
|
+
value={repo}
|
|
39
|
+
onChange={(e) => setRepo(e.target.value)}
|
|
40
|
+
placeholder="owner/name or https://github.com/owner/name"
|
|
41
|
+
autoFocus
|
|
42
|
+
/>
|
|
43
|
+
<button type="submit" disabled={busy || !repo.trim()}>
|
|
44
|
+
{busy ? "Analyzing…" : "Analyze"}
|
|
45
|
+
</button>
|
|
46
|
+
</form>
|
|
47
|
+
|
|
48
|
+
{reply ? (
|
|
49
|
+
<section className="reply">
|
|
50
|
+
{reply.parts.map((part, i) => {
|
|
51
|
+
if (part.type === "reasoning") {
|
|
52
|
+
return (
|
|
53
|
+
<p key={i} className="reasoning">
|
|
54
|
+
{part.text}
|
|
55
|
+
</p>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
if (part.type === "text") {
|
|
59
|
+
return (
|
|
60
|
+
<p key={i} className="text">
|
|
61
|
+
{part.text}
|
|
62
|
+
</p>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
if (part.type === "dynamic-tool") {
|
|
66
|
+
return (
|
|
67
|
+
<div key={i} className="tool">
|
|
68
|
+
<span className="tool-name">{part.toolName}</span>
|
|
69
|
+
<span className="tool-state">{part.state}</span>
|
|
70
|
+
</div>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
return null;
|
|
74
|
+
})}
|
|
75
|
+
{agent.status === "streaming" ? (
|
|
76
|
+
<span className="cursor" aria-hidden />
|
|
77
|
+
) : null}
|
|
78
|
+
</section>
|
|
79
|
+
) : busy ? (
|
|
80
|
+
<section className="reply">
|
|
81
|
+
<span className="cursor" aria-hidden />
|
|
82
|
+
</section>
|
|
83
|
+
) : null}
|
|
26
84
|
|
|
27
|
-
{
|
|
85
|
+
{agent.status === "error" ? (
|
|
28
86
|
<div className="banner">
|
|
29
|
-
|
|
30
|
-
or Upstash Redis integration and set <code>KV_REST_API_URL</code> /{" "}
|
|
31
|
-
<code>KV_REST_API_TOKEN</code> to persist.
|
|
87
|
+
{String(agent.error ?? "Something went wrong.")}
|
|
32
88
|
</div>
|
|
33
89
|
) : null}
|
|
34
|
-
{error === "invalid-repo" ? (
|
|
35
|
-
<div className="banner">Enter a valid GitHub repository (owner/name or a github.com URL).</div>
|
|
36
|
-
) : null}
|
|
37
|
-
|
|
38
|
-
<div className="panel">
|
|
39
|
-
<h2 style={{ marginTop: 0 }}>Add a target</h2>
|
|
40
|
-
<form action={addTarget}>
|
|
41
|
-
<div className="row">
|
|
42
|
-
<div>
|
|
43
|
-
<label htmlFor="repo">GitHub repository</label>
|
|
44
|
-
<input id="repo" name="repo" type="text" placeholder="owner/name or URL" required />
|
|
45
|
-
</div>
|
|
46
|
-
<div>
|
|
47
|
-
<label htmlFor="branch">Branch (optional)</label>
|
|
48
|
-
<input id="branch" name="branch" type="text" placeholder="default branch" />
|
|
49
|
-
</div>
|
|
50
|
-
</div>
|
|
51
|
-
<div className="row">
|
|
52
|
-
<div>
|
|
53
|
-
<label htmlFor="name">Display name (optional)</label>
|
|
54
|
-
<input id="name" name="name" type="text" placeholder="My app" />
|
|
55
|
-
</div>
|
|
56
|
-
<div>
|
|
57
|
-
<label htmlFor="token">Access token (optional, for private repos)</label>
|
|
58
|
-
<input id="token" name="token" type="password" placeholder="ghp_..." />
|
|
59
|
-
</div>
|
|
60
|
-
</div>
|
|
61
|
-
<div className="check" style={{ marginBottom: 14 }}>
|
|
62
|
-
<input id="autoRemediate" name="autoRemediate" type="checkbox" />
|
|
63
|
-
<label htmlFor="autoRemediate">Open remediation PRs automatically</label>
|
|
64
|
-
</div>
|
|
65
|
-
<button type="submit">Add target</button>
|
|
66
|
-
</form>
|
|
67
|
-
</div>
|
|
68
|
-
|
|
69
|
-
<h2>Configured targets</h2>
|
|
70
|
-
{targets.length === 0 ? (
|
|
71
|
-
<div className="empty">No targets yet. Add a repository above to start scanning.</div>
|
|
72
|
-
) : (
|
|
73
|
-
targets.map((t) => {
|
|
74
|
-
const findings = findingsByTarget.get(t.id) ?? [];
|
|
75
|
-
return (
|
|
76
|
-
<div className="card" key={t.id}>
|
|
77
|
-
<div className="grow">
|
|
78
|
-
<div className="repo">
|
|
79
|
-
<Link href={`/targets/${t.id}`}>{t.name}</Link>{" "}
|
|
80
|
-
<span className="mono-sm">{t.repo}@{t.branch}</span>
|
|
81
|
-
</div>
|
|
82
|
-
<div className="meta">
|
|
83
|
-
{t.lastScanAt
|
|
84
|
-
? `last scan ${new Date(t.lastScanAt).toLocaleString()}${
|
|
85
|
-
t.lastScanStatus === "error" ? ` — error: ${t.lastScanError}` : ""
|
|
86
|
-
}`
|
|
87
|
-
: "never scanned"}
|
|
88
|
-
</div>
|
|
89
|
-
<div style={{ marginTop: 8 }}>
|
|
90
|
-
<SeverityCounts findings={findings} />
|
|
91
|
-
</div>
|
|
92
|
-
</div>
|
|
93
|
-
<div className="actions">
|
|
94
|
-
<form action={scanTarget} className="inline">
|
|
95
|
-
<input type="hidden" name="id" value={t.id} />
|
|
96
|
-
<button type="submit">Scan now</button>
|
|
97
|
-
</form>
|
|
98
|
-
<Link className="btn btn-ghost" href={`/targets/${t.id}`}>
|
|
99
|
-
View
|
|
100
|
-
</Link>
|
|
101
|
-
<form action={deleteTarget} className="inline">
|
|
102
|
-
<input type="hidden" name="id" value={t.id} />
|
|
103
|
-
<button type="submit" className="btn-danger">
|
|
104
|
-
Delete
|
|
105
|
-
</button>
|
|
106
|
-
</form>
|
|
107
|
-
</div>
|
|
108
|
-
</div>
|
|
109
|
-
);
|
|
110
|
-
})
|
|
111
|
-
)}
|
|
112
90
|
</main>
|
|
113
91
|
);
|
|
114
92
|
}
|
|
@@ -13,18 +13,17 @@
|
|
|
13
13
|
"eve:info": "eve info"
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"@upstash/redis": "^1.38.0",
|
|
17
16
|
"ai": "^7.0.3",
|
|
18
17
|
"eve": "^0.16.2",
|
|
19
18
|
"next": "^16.2.9",
|
|
20
19
|
"react": "^19.2.7",
|
|
21
|
-
"react-dom": "^19.2.7"
|
|
22
|
-
"zod": "^4.4.3"
|
|
20
|
+
"react-dom": "^19.2.7"
|
|
23
21
|
},
|
|
24
22
|
"devDependencies": {
|
|
25
23
|
"@types/node": "25.5.2",
|
|
26
24
|
"@types/react": "19.2.14",
|
|
27
25
|
"@types/react-dom": "^19.2.3",
|
|
26
|
+
"microsandbox": "^0.6.0",
|
|
28
27
|
"typescript": "6.0.2"
|
|
29
28
|
}
|
|
30
29
|
}
|