openhacker 0.1.3 → 0.1.4
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/package.json +8 -6
- package/templates/agent/.env.example +0 -8
- package/templates/agent/README.md +9 -2
- package/templates/agent/agent/channels/eve.ts +3 -3
- package/templates/agent/app/page.tsx +19 -9
- package/templates/agent/lib/repository.ts +81 -0
- package/templates/agent/next.config.ts +38 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openhacker",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Scaffold a self-hosted OpenHacker security agent",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"type": "module",
|
|
@@ -19,14 +19,16 @@
|
|
|
19
19
|
"templates",
|
|
20
20
|
"README.md"
|
|
21
21
|
],
|
|
22
|
+
"scripts": {
|
|
23
|
+
"build": "tsc --noEmit && node --check ./bin/openhacker && node --check ./src/index.js && node --check ./scripts/sync-template.js && node --check ./scripts/clean-template.js",
|
|
24
|
+
"sync-template": "node ./scripts/sync-template.js",
|
|
25
|
+
"prepack": "pnpm run sync-template",
|
|
26
|
+
"postpack": "node ./scripts/clean-template.js"
|
|
27
|
+
},
|
|
22
28
|
"engines": {
|
|
23
29
|
"node": ">=20"
|
|
24
30
|
},
|
|
25
31
|
"devDependencies": {
|
|
26
32
|
"typescript": "6.0.2"
|
|
27
|
-
},
|
|
28
|
-
"scripts": {
|
|
29
|
-
"build": "tsc --noEmit && node --check ./bin/openhacker && node --check ./src/index.js && node --check ./scripts/sync-template.js && node --check ./scripts/clean-template.js",
|
|
30
|
-
"sync-template": "node ./scripts/sync-template.js"
|
|
31
33
|
}
|
|
32
|
-
}
|
|
34
|
+
}
|
|
@@ -3,11 +3,3 @@
|
|
|
3
3
|
# For LOCAL runs of the eve agent, provide a gateway key:
|
|
4
4
|
AI_GATEWAY_API_KEY=
|
|
5
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=
|
|
@@ -15,13 +15,20 @@ Your self-hosted OpenHacker security agent — a Next.js dashboard with an embed
|
|
|
15
15
|
|
|
16
16
|
1. Push this directory to a Git repository.
|
|
17
17
|
2. Import it into Vercel (it deploys as one project — UI + agent + cron).
|
|
18
|
-
3.
|
|
19
|
-
|
|
18
|
+
3. Enable Vercel Deployment Protection for the project so only approved users
|
|
19
|
+
can reach the dashboard and eve routes.
|
|
20
20
|
4. Open the deployment URL and add a target repository.
|
|
21
21
|
|
|
22
22
|
Inference runs through the Vercel AI Gateway and authenticates automatically via Vercel
|
|
23
23
|
OIDC — no model API key required in production.
|
|
24
24
|
|
|
25
|
+
The main screen calls the eve channel directly. This is safe only when the
|
|
26
|
+
deployment is protected by Vercel Deployment Protection; without that gate,
|
|
27
|
+
`/eve/v1/*` is a public compute endpoint. The UI validates GitHub repository
|
|
28
|
+
names before it sends the agent message.
|
|
29
|
+
|
|
30
|
+
This starter intentionally does not configure external persistence yet.
|
|
31
|
+
|
|
25
32
|
## Local development
|
|
26
33
|
|
|
27
34
|
```bash
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { eveChannel } from "eve/channels/eve";
|
|
2
2
|
import { none } from "eve/channels/auth";
|
|
3
3
|
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
4
|
+
// This app is intended to run behind Vercel Deployment Protection. The
|
|
5
|
+
// deployment gate owns user access; the Eve channel accepts requests that reach
|
|
6
|
+
// the protected app.
|
|
7
7
|
export default eveChannel({ auth: [none()] });
|
|
@@ -1,21 +1,31 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { useState } from "react";
|
|
3
|
+
import { type SyntheticEvent, useState } from "react";
|
|
4
4
|
import { useEveAgent } from "eve/react";
|
|
5
5
|
|
|
6
|
+
import { validateGitHubRepository } from "@/lib/repository";
|
|
7
|
+
|
|
6
8
|
export default function Home() {
|
|
7
9
|
const [repo, setRepo] = useState("");
|
|
10
|
+
const [error, setError] = useState("");
|
|
8
11
|
const agent = useEveAgent();
|
|
9
12
|
|
|
10
13
|
const busy = agent.status === "submitted" || agent.status === "streaming";
|
|
11
14
|
|
|
12
|
-
function onSubmit(e:
|
|
15
|
+
function onSubmit(e: SyntheticEvent<HTMLFormElement>) {
|
|
13
16
|
e.preventDefault();
|
|
14
|
-
|
|
15
|
-
|
|
17
|
+
if (busy) return;
|
|
18
|
+
|
|
19
|
+
const validation = validateGitHubRepository(repo);
|
|
20
|
+
if (!validation.ok) {
|
|
21
|
+
setError(validation.error);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
setError("");
|
|
16
26
|
agent.reset();
|
|
17
27
|
agent.send({
|
|
18
|
-
message: `Analyze the GitHub repository
|
|
28
|
+
message: `Analyze the GitHub repository ${validation.repository} for security vulnerabilities. Reply with only the final report.`,
|
|
19
29
|
});
|
|
20
30
|
}
|
|
21
31
|
|
|
@@ -52,14 +62,14 @@ export default function Home() {
|
|
|
52
62
|
value={repo}
|
|
53
63
|
onChange={(e) => setRepo(e.target.value)}
|
|
54
64
|
placeholder="owner/name or https://github.com/owner/name"
|
|
55
|
-
|
|
65
|
+
aria-label="GitHub repository"
|
|
56
66
|
/>
|
|
57
67
|
<button type="submit" disabled={busy || !repo.trim()}>
|
|
58
68
|
{busy ? "Analyzing…" : "Analyze"}
|
|
59
69
|
</button>
|
|
60
70
|
</form>
|
|
61
71
|
|
|
62
|
-
{
|
|
72
|
+
{result || busy ? (
|
|
63
73
|
<section className="reply">
|
|
64
74
|
{result ? <p className="text">{result}</p> : null}
|
|
65
75
|
{busy && !result ? (
|
|
@@ -71,9 +81,9 @@ export default function Home() {
|
|
|
71
81
|
</section>
|
|
72
82
|
) : null}
|
|
73
83
|
|
|
74
|
-
{agent.status === "error" ? (
|
|
84
|
+
{error || agent.status === "error" ? (
|
|
75
85
|
<div className="banner">
|
|
76
|
-
{String(agent.error ?? "Something went wrong.")}
|
|
86
|
+
{error || String(agent.error ?? "Something went wrong.")}
|
|
77
87
|
</div>
|
|
78
88
|
) : null}
|
|
79
89
|
</main>
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
const OWNER_PATTERN = /^[a-z\d](?:[a-z\d-]{0,37}[a-z\d])?$/i;
|
|
2
|
+
const REPO_PATTERN = /^[a-z\d._-]{1,100}$/i;
|
|
3
|
+
|
|
4
|
+
export type RepositoryValidationResult =
|
|
5
|
+
| { ok: true; repository: string }
|
|
6
|
+
| { ok: false; error: string };
|
|
7
|
+
|
|
8
|
+
export function validateGitHubRepository(
|
|
9
|
+
input: string,
|
|
10
|
+
): RepositoryValidationResult {
|
|
11
|
+
const value = input.trim();
|
|
12
|
+
|
|
13
|
+
if (!value) {
|
|
14
|
+
return { ok: false, error: "Enter a GitHub repository." };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (value.length > 200) {
|
|
18
|
+
return { ok: false, error: "Repository input is too long." };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const path = extractRepositoryPath(value);
|
|
22
|
+
if (!path) {
|
|
23
|
+
return {
|
|
24
|
+
ok: false,
|
|
25
|
+
error: "Use owner/name or https://github.com/owner/name.",
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const normalizedPath = path.endsWith(".git") ? path.slice(0, -4) : path;
|
|
30
|
+
const parts = normalizedPath.split("/");
|
|
31
|
+
if (parts.length !== 2) {
|
|
32
|
+
return {
|
|
33
|
+
ok: false,
|
|
34
|
+
error: "Use owner/name or https://github.com/owner/name.",
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const [owner, repo] = parts;
|
|
39
|
+
if (!owner || !repo || !OWNER_PATTERN.test(owner) || !REPO_PATTERN.test(repo)) {
|
|
40
|
+
return {
|
|
41
|
+
ok: false,
|
|
42
|
+
error: "Repository must be a valid GitHub owner/name pair.",
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (repo === "." || repo === "..") {
|
|
47
|
+
return {
|
|
48
|
+
ok: false,
|
|
49
|
+
error: "Repository must be a valid GitHub owner/name pair.",
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return { ok: true, repository: `${owner}/${repo}` };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function extractRepositoryPath(value: string): string | null {
|
|
57
|
+
if (/^https?:\/\//i.test(value)) {
|
|
58
|
+
try {
|
|
59
|
+
const url = new URL(value);
|
|
60
|
+
if (
|
|
61
|
+
url.hostname.toLowerCase() !== "github.com" ||
|
|
62
|
+
url.username ||
|
|
63
|
+
url.password ||
|
|
64
|
+
url.search ||
|
|
65
|
+
url.hash
|
|
66
|
+
) {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return url.pathname.replace(/^\/|\/$/g, "");
|
|
71
|
+
} catch {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (value.includes(":") || value.includes("\\") || value.includes("?")) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return value.replace(/^\/|\/$/g, "");
|
|
81
|
+
}
|
|
@@ -1,8 +1,45 @@
|
|
|
1
1
|
import type { NextConfig } from "next";
|
|
2
2
|
import { withEve } from "eve/next";
|
|
3
3
|
|
|
4
|
-
const
|
|
4
|
+
const securityHeaders = [
|
|
5
|
+
{ key: "Content-Security-Policy", value: csp() },
|
|
6
|
+
{ key: "Referrer-Policy", value: "no-referrer" },
|
|
7
|
+
{ key: "X-Content-Type-Options", value: "nosniff" },
|
|
8
|
+
{ key: "X-Frame-Options", value: "DENY" },
|
|
9
|
+
{
|
|
10
|
+
key: "Permissions-Policy",
|
|
11
|
+
value: "camera=(), microphone=(), geolocation=(), payment=()",
|
|
12
|
+
},
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
const nextConfig: NextConfig = {
|
|
16
|
+
async headers() {
|
|
17
|
+
return [
|
|
18
|
+
{
|
|
19
|
+
headers: securityHeaders,
|
|
20
|
+
source: "/:path*",
|
|
21
|
+
},
|
|
22
|
+
];
|
|
23
|
+
},
|
|
24
|
+
};
|
|
5
25
|
|
|
6
26
|
// Mounts the eve agent (agent/) and the dashboard as a single Vercel deployment.
|
|
7
27
|
// Schedules under agent/schedules become Vercel Cron Jobs automatically.
|
|
8
28
|
export default withEve(nextConfig);
|
|
29
|
+
|
|
30
|
+
function csp() {
|
|
31
|
+
return [
|
|
32
|
+
"default-src 'self'",
|
|
33
|
+
"base-uri 'self'",
|
|
34
|
+
"connect-src 'self'",
|
|
35
|
+
"form-action 'self'",
|
|
36
|
+
"frame-ancestors 'none'",
|
|
37
|
+
"img-src 'self' data: blob:",
|
|
38
|
+
"object-src 'none'",
|
|
39
|
+
"script-src 'self' 'unsafe-inline' 'unsafe-eval'",
|
|
40
|
+
"style-src 'self' 'unsafe-inline'",
|
|
41
|
+
process.env.NODE_ENV === "production" ? "upgrade-insecure-requests" : "",
|
|
42
|
+
]
|
|
43
|
+
.filter(Boolean)
|
|
44
|
+
.join("; ");
|
|
45
|
+
}
|