openhacker 0.0.0 → 0.1.1
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 +12 -1
- package/bin/openhacker +4 -0
- package/package.json +29 -3
- package/scripts/clean-template.js +8 -0
- package/scripts/sync-template.js +53 -0
- package/src/cli.js +276 -0
- package/src/index.js +1 -0
- package/src/index.ts +1 -0
- package/templates/agent/.env.example +13 -0
- package/templates/agent/README.md +34 -0
- package/templates/agent/agent/agent.ts +5 -0
- package/templates/agent/agent/channels/eve.ts +7 -0
- package/templates/agent/agent/instructions.md +11 -0
- package/templates/agent/app/globals.css +148 -0
- package/templates/agent/app/layout.tsx +15 -0
- package/templates/agent/app/page.tsx +92 -0
- package/templates/agent/next.config.ts +8 -0
- package/templates/agent/package.json +29 -0
- package/templates/agent/tsconfig.json +43 -0
package/README.md
CHANGED
|
@@ -1,3 +1,14 @@
|
|
|
1
1
|
# openhacker
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Scaffold a self-hosted OpenHacker security agent.
|
|
4
|
+
|
|
5
|
+
## Create an instance
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx openhacker my-instance
|
|
9
|
+
cd my-instance
|
|
10
|
+
pnpm install
|
|
11
|
+
pnpm dev
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Running `npx openhacker` with no arguments creates `./openhacker`.
|
package/bin/openhacker
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,32 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openhacker",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
5
|
-
"license": "UNLICENSED"
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Scaffold a self-hosted OpenHacker security agent",
|
|
5
|
+
"license": "UNLICENSED",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"openhacker": "./bin/openhacker"
|
|
9
|
+
},
|
|
10
|
+
"exports": {
|
|
11
|
+
".": "./src/index.js"
|
|
12
|
+
},
|
|
13
|
+
"main": "./src/index.js",
|
|
14
|
+
"types": "./src/index.ts",
|
|
15
|
+
"files": [
|
|
16
|
+
"bin",
|
|
17
|
+
"scripts",
|
|
18
|
+
"src",
|
|
19
|
+
"templates",
|
|
20
|
+
"README.md"
|
|
21
|
+
],
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=20"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"typescript": "6.0.2"
|
|
27
|
+
},
|
|
28
|
+
"scripts": {
|
|
29
|
+
"build": "tsc --noEmit && node --check ./bin/openhacker && node --check ./src/cli.js && node --check ./scripts/sync-template.js && node --check ./scripts/clean-template.js",
|
|
30
|
+
"sync-template": "node ./scripts/sync-template.js"
|
|
31
|
+
}
|
|
6
32
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { rm } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
const templateDir = path.resolve(here, "../templates");
|
|
7
|
+
|
|
8
|
+
await rm(templateDir, { recursive: true, force: true });
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { cp, mkdir, rm, stat } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
const EXCLUDE = new Set([
|
|
6
|
+
".env",
|
|
7
|
+
".env.local",
|
|
8
|
+
"node_modules",
|
|
9
|
+
".eve",
|
|
10
|
+
".next",
|
|
11
|
+
".output",
|
|
12
|
+
".git",
|
|
13
|
+
".vercel",
|
|
14
|
+
".turbo",
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
18
|
+
const packageRoot = path.resolve(here, "..");
|
|
19
|
+
const source = path.resolve(process.env.OPENHACKER_TEMPLATE_DIR ?? path.join(packageRoot, "../../apps/agent"));
|
|
20
|
+
const dest = path.join(packageRoot, "templates/agent");
|
|
21
|
+
|
|
22
|
+
async function exists(p) {
|
|
23
|
+
try {
|
|
24
|
+
await stat(p);
|
|
25
|
+
return true;
|
|
26
|
+
} catch {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function shouldCopyTemplatePath(src) {
|
|
32
|
+
const relative = path.relative(source, src);
|
|
33
|
+
const segments = relative.split(path.sep);
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
!segments.some((seg) => EXCLUDE.has(seg)) &&
|
|
37
|
+
!relative.endsWith("next-env.d.ts") &&
|
|
38
|
+
!relative.endsWith("tsconfig.tsbuildinfo")
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (!(await exists(path.join(source, "agent", "agent.ts")))) {
|
|
43
|
+
throw new Error(`Could not find the OpenHacker app template at ${source}`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
await rm(dest, { recursive: true, force: true });
|
|
47
|
+
await mkdir(path.dirname(dest), { recursive: true });
|
|
48
|
+
await cp(source, dest, {
|
|
49
|
+
recursive: true,
|
|
50
|
+
filter: shouldCopyTemplatePath,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
console.log(`Synced OpenHacker template from ${source}`);
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { cp, readFile, stat, writeFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
const ORANGE = "\x1b[38;5;214m";
|
|
7
|
+
const MUTED = "\x1b[0;2m";
|
|
8
|
+
const RED = "\x1b[0;31m";
|
|
9
|
+
const NC = "\x1b[0m";
|
|
10
|
+
|
|
11
|
+
const EXCLUDE = new Set([
|
|
12
|
+
".env",
|
|
13
|
+
".env.local",
|
|
14
|
+
"node_modules",
|
|
15
|
+
".eve",
|
|
16
|
+
".next",
|
|
17
|
+
".output",
|
|
18
|
+
".git",
|
|
19
|
+
".vercel",
|
|
20
|
+
".turbo",
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
// Written into scaffolded projects directly rather than shipped as a template
|
|
24
|
+
// file, because npm renames a packaged `.gitignore` to `.npmignore` on publish.
|
|
25
|
+
const GITIGNORE = `# dependencies
|
|
26
|
+
node_modules
|
|
27
|
+
|
|
28
|
+
# next.js
|
|
29
|
+
.next
|
|
30
|
+
next-env.d.ts
|
|
31
|
+
|
|
32
|
+
# eve build artifacts
|
|
33
|
+
.eve
|
|
34
|
+
.output
|
|
35
|
+
|
|
36
|
+
# vercel / turbo
|
|
37
|
+
.vercel
|
|
38
|
+
.turbo
|
|
39
|
+
|
|
40
|
+
# typescript
|
|
41
|
+
tsconfig.tsbuildinfo
|
|
42
|
+
|
|
43
|
+
# env files (keep the example)
|
|
44
|
+
.env
|
|
45
|
+
.env.*
|
|
46
|
+
!.env.example
|
|
47
|
+
|
|
48
|
+
# misc
|
|
49
|
+
.DS_Store
|
|
50
|
+
*.log
|
|
51
|
+
`;
|
|
52
|
+
|
|
53
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
54
|
+
|
|
55
|
+
async function exists(p) {
|
|
56
|
+
try {
|
|
57
|
+
await stat(p);
|
|
58
|
+
return true;
|
|
59
|
+
} catch {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function resolveTemplateDir() {
|
|
65
|
+
const candidates = [];
|
|
66
|
+
|
|
67
|
+
if (process.env.OPENHACKER_TEMPLATE_DIR) {
|
|
68
|
+
candidates.push(path.resolve(process.env.OPENHACKER_TEMPLATE_DIR));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
candidates.push(
|
|
72
|
+
// npm package layout: packages/openhacker/src -> packages/openhacker/templates/agent
|
|
73
|
+
path.resolve(here, "../templates/agent"),
|
|
74
|
+
// monorepo layout: packages/openhacker/src -> repo root -> apps/agent
|
|
75
|
+
path.resolve(here, "../../../apps/agent"),
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
for (const candidate of candidates) {
|
|
79
|
+
if (await exists(path.join(candidate, "agent", "agent.ts"))) {
|
|
80
|
+
return candidate;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return candidates[0];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function packageNameFor(dest) {
|
|
88
|
+
return path.basename(dest).replace(/[^a-z0-9-]+/gi, "-").toLowerCase() || "openhacker";
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function shouldCopyTemplatePath(src, root) {
|
|
92
|
+
const relative = path.relative(root, src);
|
|
93
|
+
const segments = relative.split(path.sep);
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
!segments.some((seg) => EXCLUDE.has(seg)) &&
|
|
97
|
+
!relative.endsWith("next-env.d.ts") &&
|
|
98
|
+
!relative.endsWith("tsconfig.tsbuildinfo")
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function runStep(command, args, cwd, { quiet = false } = {}) {
|
|
103
|
+
const result = spawnSync(command, args, {
|
|
104
|
+
cwd,
|
|
105
|
+
stdio: quiet ? "ignore" : "inherit",
|
|
106
|
+
shell: process.platform === "win32",
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
return !result.error && result.status === 0;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function hasCommand(command) {
|
|
113
|
+
const probe = process.platform === "win32" ? "where" : "which";
|
|
114
|
+
const result = spawnSync(probe, [command], { stdio: "ignore", shell: process.platform === "win32" });
|
|
115
|
+
return !result.error && result.status === 0;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function isInsideGitRepo(cwd) {
|
|
119
|
+
const result = spawnSync("git", ["rev-parse", "--is-inside-work-tree"], {
|
|
120
|
+
cwd,
|
|
121
|
+
stdio: "ignore",
|
|
122
|
+
shell: process.platform === "win32",
|
|
123
|
+
});
|
|
124
|
+
return !result.error && result.status === 0;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function installDependencies(dest) {
|
|
128
|
+
if (!hasCommand("pnpm")) {
|
|
129
|
+
console.log(`${MUTED}pnpm not found \u2014 skipping install. Run \`pnpm install\` manually.${NC}`);
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
console.log(`\n${MUTED}Installing dependencies with pnpm\u2026${NC}`);
|
|
134
|
+
// --ignore-workspace keeps the install self-contained: without it, pnpm walks
|
|
135
|
+
// up to a parent pnpm-workspace.yaml (e.g. when scaffolding inside a monorepo)
|
|
136
|
+
// and installs that workspace instead of the new project's node_modules.
|
|
137
|
+
const ok = runStep("pnpm", ["install", "--ignore-workspace"], dest);
|
|
138
|
+
if (!ok) {
|
|
139
|
+
console.log(`${RED}pnpm install failed.${NC} ${MUTED}You can re-run it inside the project.${NC}`);
|
|
140
|
+
}
|
|
141
|
+
return ok;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function initGitRepo(dest) {
|
|
145
|
+
if (!hasCommand("git")) {
|
|
146
|
+
console.log(`${MUTED}git not found \u2014 skipping repository init.${NC}`);
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (isInsideGitRepo(dest)) {
|
|
151
|
+
console.log(`${MUTED}Already inside a git repository \u2014 skipping git init.${NC}`);
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const initialized =
|
|
156
|
+
runStep("git", ["init"], dest, { quiet: true }) &&
|
|
157
|
+
runStep("git", ["add", "-A"], dest, { quiet: true }) &&
|
|
158
|
+
runStep("git", ["commit", "-m", "Initial commit from OpenHacker"], dest, { quiet: true });
|
|
159
|
+
|
|
160
|
+
if (initialized) {
|
|
161
|
+
console.log(`${MUTED}Initialized a git repository.${NC}`);
|
|
162
|
+
} else {
|
|
163
|
+
console.log(`${MUTED}Could not create the initial git commit \u2014 you can do it manually.${NC}`);
|
|
164
|
+
}
|
|
165
|
+
return initialized;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async function init(targetArg, { skipInstall = false, skipGit = false } = {}) {
|
|
169
|
+
const template = await resolveTemplateDir();
|
|
170
|
+
if (!(await exists(path.join(template, "agent", "agent.ts")))) {
|
|
171
|
+
console.error(`${RED}Could not find the instance template at ${template}.${NC}`);
|
|
172
|
+
console.error(`${MUTED}Set OPENHACKER_TEMPLATE_DIR to the template directory.${NC}`);
|
|
173
|
+
process.exit(1);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const dest = path.resolve(process.cwd(), targetArg ?? "openhacker");
|
|
177
|
+
if (await exists(dest)) {
|
|
178
|
+
console.error(`${RED}Destination already exists: ${dest}${NC}`);
|
|
179
|
+
process.exit(1);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
console.log(`\n${MUTED}Creating OpenHacker instance in ${NC}${dest}`);
|
|
183
|
+
await cp(template, dest, {
|
|
184
|
+
recursive: true,
|
|
185
|
+
filter: (src) => shouldCopyTemplatePath(src, template),
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
const pkgPath = path.join(dest, "package.json");
|
|
189
|
+
const pkg = JSON.parse(await readFile(pkgPath, "utf8"));
|
|
190
|
+
pkg.name = packageNameFor(dest);
|
|
191
|
+
pkg.private = true;
|
|
192
|
+
await writeFile(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`);
|
|
193
|
+
|
|
194
|
+
// Write before git init so the initial commit doesn't include node_modules/.env.
|
|
195
|
+
if (!(await exists(path.join(dest, ".gitignore")))) {
|
|
196
|
+
await writeFile(path.join(dest, ".gitignore"), GITIGNORE);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const installed = skipInstall ? false : await installDependencies(dest);
|
|
200
|
+
if (!skipGit) {
|
|
201
|
+
await initGitRepo(dest);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
console.log(`\n${ORANGE}\u2713${NC} OpenHacker instance ready.\n`);
|
|
205
|
+
console.log("Next steps:\n");
|
|
206
|
+
console.log(` cd ${path.relative(process.cwd(), dest) || "."}`);
|
|
207
|
+
if (skipInstall || !installed) {
|
|
208
|
+
console.log(" pnpm install");
|
|
209
|
+
}
|
|
210
|
+
console.log(" pnpm dev # run locally\n");
|
|
211
|
+
console.log(`${MUTED}Deploy: push to a git repo and import it into Vercel (deploys as one project).`);
|
|
212
|
+
console.log(`Add a Vercel KV / Upstash Redis integration to persist findings. See README.md.${NC}\n`);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function usage() {
|
|
216
|
+
console.log("OpenHacker\n");
|
|
217
|
+
console.log("Usage:");
|
|
218
|
+
console.log(" openhacker [dir] Scaffold a deployable OpenHacker instance");
|
|
219
|
+
console.log(" openhacker init [dir] Same as above");
|
|
220
|
+
console.log(" openhacker --help Show help");
|
|
221
|
+
console.log(" openhacker --version Show version\n");
|
|
222
|
+
console.log("Options:");
|
|
223
|
+
console.log(" --skip-install Don't run pnpm install");
|
|
224
|
+
console.log(" --skip-git Don't create a git repository\n");
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async function version() {
|
|
228
|
+
const pkg = JSON.parse(await readFile(path.resolve(here, "../package.json"), "utf8"));
|
|
229
|
+
console.log(pkg.version);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export async function run(args = process.argv.slice(2)) {
|
|
233
|
+
const options = { skipInstall: false, skipGit: false };
|
|
234
|
+
const positionals = [];
|
|
235
|
+
|
|
236
|
+
for (const arg of args) {
|
|
237
|
+
switch (arg) {
|
|
238
|
+
case "--skip-install":
|
|
239
|
+
options.skipInstall = true;
|
|
240
|
+
break;
|
|
241
|
+
case "--skip-git":
|
|
242
|
+
options.skipGit = true;
|
|
243
|
+
break;
|
|
244
|
+
case "-h":
|
|
245
|
+
case "--help":
|
|
246
|
+
usage();
|
|
247
|
+
return;
|
|
248
|
+
case "-v":
|
|
249
|
+
case "--version":
|
|
250
|
+
await version();
|
|
251
|
+
return;
|
|
252
|
+
default:
|
|
253
|
+
if (arg.startsWith("-")) {
|
|
254
|
+
console.error(`${RED}Unknown option: ${arg}${NC}\n`);
|
|
255
|
+
usage();
|
|
256
|
+
process.exit(1);
|
|
257
|
+
}
|
|
258
|
+
positionals.push(arg);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const [command, target, ...rest] = positionals;
|
|
263
|
+
|
|
264
|
+
if (rest.length > 0) {
|
|
265
|
+
console.error(`${RED}Too many arguments.${NC}\n`);
|
|
266
|
+
usage();
|
|
267
|
+
process.exit(1);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (command === "init") {
|
|
271
|
+
await init(target, options);
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
await init(command, options);
|
|
276
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const MESSAGE = "OpenHacker";
|
package/src/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const MESSAGE = "OpenHacker";
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# --- Model (Vercel AI Gateway) ---
|
|
2
|
+
# On Vercel, inference authenticates automatically via VERCEL_OIDC_TOKEN — no key needed.
|
|
3
|
+
# For LOCAL runs of the eve agent, provide a gateway key:
|
|
4
|
+
AI_GATEWAY_API_KEY=
|
|
5
|
+
|
|
6
|
+
# --- Persistent storage ---
|
|
7
|
+
# Add a Vercel KV / Upstash Redis integration. Without these, an in-memory store is
|
|
8
|
+
# used and data does NOT persist across restarts/deploys.
|
|
9
|
+
KV_REST_API_URL=
|
|
10
|
+
KV_REST_API_TOKEN=
|
|
11
|
+
|
|
12
|
+
# --- Optional: protect POST /api/scan for programmatic triggers ---
|
|
13
|
+
OPENHACKER_API_TOKEN=
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# OpenHacker instance
|
|
2
|
+
|
|
3
|
+
Your self-hosted OpenHacker security agent — a Next.js dashboard with an embedded
|
|
4
|
+
[eve](https://eve.dev) agent, deployable to Vercel as a single project.
|
|
5
|
+
|
|
6
|
+
## What it does
|
|
7
|
+
|
|
8
|
+
- Continuously scans the repositories you connect for vulnerable dependencies (via OSV).
|
|
9
|
+
- Records findings with computed CVSS severity and fix versions.
|
|
10
|
+
- A daily schedule (Vercel Cron) re-scans every target so newly disclosed advisories are
|
|
11
|
+
caught even when your code hasn't changed.
|
|
12
|
+
- The eve agent layers code-level analysis (reachability, injection, authz) on top.
|
|
13
|
+
|
|
14
|
+
## Deploy to Vercel
|
|
15
|
+
|
|
16
|
+
1. Push this directory to a Git repository.
|
|
17
|
+
2. Import it into Vercel (it deploys as one project — UI + agent + cron).
|
|
18
|
+
3. Add a **KV / Upstash Redis** integration from the Vercel Marketplace so findings persist
|
|
19
|
+
(`KV_REST_API_URL` / `KV_REST_API_TOKEN` are injected automatically).
|
|
20
|
+
4. Open the deployment URL and add a target repository.
|
|
21
|
+
|
|
22
|
+
Inference runs through the Vercel AI Gateway and authenticates automatically via Vercel
|
|
23
|
+
OIDC — no model API key required in production.
|
|
24
|
+
|
|
25
|
+
## Local development
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pnpm install
|
|
29
|
+
cp .env.example .env.local # set AI_GATEWAY_API_KEY for local agent runs
|
|
30
|
+
pnpm dev
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
`pnpm dev` runs the Next.js dashboard and the eve agent together. Open the printed URL,
|
|
34
|
+
add a target, and click **Scan now**.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { eveChannel } from "eve/channels/eve";
|
|
2
|
+
import { none } from "eve/channels/auth";
|
|
3
|
+
|
|
4
|
+
// Public demo: anyone can call the agent from the browser. Add real auth
|
|
5
|
+
// (e.g. vercelOidc/localDev or your own session check) before exposing
|
|
6
|
+
// anything sensitive.
|
|
7
|
+
export default eveChannel({ auth: [none()] });
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# OpenHacker
|
|
2
|
+
|
|
3
|
+
You are OpenHacker, an application security agent.
|
|
4
|
+
|
|
5
|
+
The user gives you a GitHub repository (as `owner/name` or a github.com URL). Analyze it for security vulnerabilities and report your findings.
|
|
6
|
+
|
|
7
|
+
- Briefly narrate what you are checking as you go (dependencies, auth, input handling, secrets, etc.).
|
|
8
|
+
- Use the shell to clone and explore the repo. Use `ls`/`find` to list directories; only use `read_file` on actual file paths, never on a directory.
|
|
9
|
+
- Call out concrete risks with severity and, where possible, how to fix them.
|
|
10
|
+
- If you cannot access the repository, say so plainly.
|
|
11
|
+
- Keep the final summary concise and skimmable.
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
--bg: #000000;
|
|
3
|
+
--panel: #0d0d0d;
|
|
4
|
+
--panel-2: #161616;
|
|
5
|
+
--border: #2a2a2a;
|
|
6
|
+
--text: #f5f5f5;
|
|
7
|
+
--muted: #8f8f8f;
|
|
8
|
+
--accent: #ffffff;
|
|
9
|
+
--accent-dim: #333333;
|
|
10
|
+
--crit: #ffffff;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
* {
|
|
14
|
+
box-sizing: border-box;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
html,
|
|
18
|
+
body {
|
|
19
|
+
margin: 0;
|
|
20
|
+
padding: 0;
|
|
21
|
+
background: var(--bg);
|
|
22
|
+
color: var(--text);
|
|
23
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
24
|
+
font-size: 14px;
|
|
25
|
+
line-height: 1.6;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.container {
|
|
29
|
+
max-width: 720px;
|
|
30
|
+
margin: 0 auto;
|
|
31
|
+
padding: 64px 24px 96px;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
h1 {
|
|
35
|
+
font-size: 22px;
|
|
36
|
+
font-weight: 700;
|
|
37
|
+
letter-spacing: 0.5px;
|
|
38
|
+
margin: 0 0 4px;
|
|
39
|
+
}
|
|
40
|
+
h1 span {
|
|
41
|
+
color: var(--accent);
|
|
42
|
+
}
|
|
43
|
+
.sub {
|
|
44
|
+
color: var(--muted);
|
|
45
|
+
margin: 0 0 28px;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.ask {
|
|
49
|
+
display: flex;
|
|
50
|
+
gap: 8px;
|
|
51
|
+
}
|
|
52
|
+
.ask input {
|
|
53
|
+
flex: 1;
|
|
54
|
+
background: var(--panel-2);
|
|
55
|
+
border: 1px solid var(--border);
|
|
56
|
+
color: var(--text);
|
|
57
|
+
border-radius: 8px;
|
|
58
|
+
padding: 11px 13px;
|
|
59
|
+
font: inherit;
|
|
60
|
+
}
|
|
61
|
+
.ask input:focus {
|
|
62
|
+
outline: none;
|
|
63
|
+
border-color: var(--accent);
|
|
64
|
+
}
|
|
65
|
+
.ask button {
|
|
66
|
+
background: var(--accent);
|
|
67
|
+
color: #000000;
|
|
68
|
+
border: none;
|
|
69
|
+
border-radius: 8px;
|
|
70
|
+
padding: 11px 18px;
|
|
71
|
+
font: inherit;
|
|
72
|
+
font-weight: 600;
|
|
73
|
+
cursor: pointer;
|
|
74
|
+
}
|
|
75
|
+
.ask button:hover:not(:disabled) {
|
|
76
|
+
filter: brightness(1.08);
|
|
77
|
+
}
|
|
78
|
+
.ask button:disabled {
|
|
79
|
+
opacity: 0.5;
|
|
80
|
+
cursor: not-allowed;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.reply {
|
|
84
|
+
margin-top: 28px;
|
|
85
|
+
background: var(--panel);
|
|
86
|
+
border: 1px solid var(--border);
|
|
87
|
+
border-radius: 10px;
|
|
88
|
+
padding: 18px 20px;
|
|
89
|
+
}
|
|
90
|
+
.reply .text {
|
|
91
|
+
white-space: pre-wrap;
|
|
92
|
+
margin: 0 0 12px;
|
|
93
|
+
}
|
|
94
|
+
.reply .text:last-child {
|
|
95
|
+
margin-bottom: 0;
|
|
96
|
+
}
|
|
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);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.tool {
|
|
107
|
+
display: flex;
|
|
108
|
+
align-items: center;
|
|
109
|
+
gap: 10px;
|
|
110
|
+
font-size: 12.5px;
|
|
111
|
+
color: var(--muted);
|
|
112
|
+
padding: 4px 0;
|
|
113
|
+
}
|
|
114
|
+
.tool-name {
|
|
115
|
+
color: var(--text);
|
|
116
|
+
}
|
|
117
|
+
.tool-state {
|
|
118
|
+
font-size: 11px;
|
|
119
|
+
text-transform: uppercase;
|
|
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
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.banner {
|
|
141
|
+
margin-top: 20px;
|
|
142
|
+
border: 1px solid var(--border);
|
|
143
|
+
background: rgba(255, 255, 255, 0.04);
|
|
144
|
+
color: var(--crit);
|
|
145
|
+
border-radius: 8px;
|
|
146
|
+
padding: 10px 14px;
|
|
147
|
+
font-size: 13px;
|
|
148
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Metadata } from "next";
|
|
2
|
+
import "./globals.css";
|
|
3
|
+
|
|
4
|
+
export const metadata: Metadata = {
|
|
5
|
+
title: "OpenHacker",
|
|
6
|
+
description: "Analyze a GitHub repo for vulnerabilities",
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
10
|
+
return (
|
|
11
|
+
<html lang="en">
|
|
12
|
+
<body>{children}</body>
|
|
13
|
+
</html>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { useEveAgent } from "eve/react";
|
|
5
|
+
|
|
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");
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<main className="container">
|
|
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}
|
|
84
|
+
|
|
85
|
+
{agent.status === "error" ? (
|
|
86
|
+
<div className="banner">
|
|
87
|
+
{String(agent.error ?? "Something went wrong.")}
|
|
88
|
+
</div>
|
|
89
|
+
) : null}
|
|
90
|
+
</main>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { NextConfig } from "next";
|
|
2
|
+
import { withEve } from "eve/next";
|
|
3
|
+
|
|
4
|
+
const nextConfig: NextConfig = {};
|
|
5
|
+
|
|
6
|
+
// Mounts the eve agent (agent/) and the dashboard as a single Vercel deployment.
|
|
7
|
+
// Schedules under agent/schedules become Vercel Cron Jobs automatically.
|
|
8
|
+
export default withEve(nextConfig);
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@openhacker/agent",
|
|
3
|
+
"private": true,
|
|
4
|
+
"type": "module",
|
|
5
|
+
"engines": {
|
|
6
|
+
"node": ">=24"
|
|
7
|
+
},
|
|
8
|
+
"scripts": {
|
|
9
|
+
"dev": "next dev",
|
|
10
|
+
"build": "next build",
|
|
11
|
+
"start": "next start",
|
|
12
|
+
"typecheck": "tsc --noEmit",
|
|
13
|
+
"eve:info": "eve info"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"ai": "^7.0.3",
|
|
17
|
+
"eve": "^0.16.2",
|
|
18
|
+
"next": "^16.2.9",
|
|
19
|
+
"react": "^19.2.7",
|
|
20
|
+
"react-dom": "^19.2.7"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@types/node": "25.5.2",
|
|
24
|
+
"@types/react": "19.2.14",
|
|
25
|
+
"@types/react-dom": "^19.2.3",
|
|
26
|
+
"microsandbox": "^0.6.0",
|
|
27
|
+
"typescript": "6.0.2"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"lib": [
|
|
5
|
+
"dom",
|
|
6
|
+
"dom.iterable",
|
|
7
|
+
"esnext"
|
|
8
|
+
],
|
|
9
|
+
"allowJs": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"strict": true,
|
|
12
|
+
"noEmit": true,
|
|
13
|
+
"incremental": true,
|
|
14
|
+
"module": "esnext",
|
|
15
|
+
"esModuleInterop": true,
|
|
16
|
+
"moduleResolution": "bundler",
|
|
17
|
+
"resolveJsonModule": true,
|
|
18
|
+
"isolatedModules": true,
|
|
19
|
+
"jsx": "react-jsx",
|
|
20
|
+
"plugins": [
|
|
21
|
+
{
|
|
22
|
+
"name": "next"
|
|
23
|
+
}
|
|
24
|
+
],
|
|
25
|
+
"paths": {
|
|
26
|
+
"@/*": [
|
|
27
|
+
"./*"
|
|
28
|
+
]
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"include": [
|
|
32
|
+
"next-env.d.ts",
|
|
33
|
+
".next/types/**/*.ts",
|
|
34
|
+
"**/*.ts",
|
|
35
|
+
"**/*.tsx",
|
|
36
|
+
".next/dev/types/**/*.ts"
|
|
37
|
+
],
|
|
38
|
+
"exclude": [
|
|
39
|
+
"node_modules",
|
|
40
|
+
".eve",
|
|
41
|
+
".output"
|
|
42
|
+
]
|
|
43
|
+
}
|