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.
Files changed (35) hide show
  1. package/README.md +2 -3
  2. package/bin/openhacker +1 -1
  3. package/package.json +3 -3
  4. package/src/index.d.ts +1 -0
  5. package/src/index.js +305 -1
  6. package/templates/agent/.env.example +0 -7
  7. package/templates/agent/README.md +1 -2
  8. package/templates/agent/agent/agent.ts +1 -5
  9. package/templates/agent/agent/channels/eve.ts +7 -0
  10. package/templates/agent/agent/instructions.md +7 -45
  11. package/templates/agent/app/globals.css +65 -197
  12. package/templates/agent/app/layout.tsx +2 -22
  13. package/templates/agent/app/page.tsx +80 -102
  14. package/templates/agent/package.json +2 -3
  15. package/src/cli.js +0 -153
  16. package/src/index.ts +0 -1
  17. package/templates/agent/agent/lib/auth.ts +0 -23
  18. package/templates/agent/agent/lib/github.ts +0 -74
  19. package/templates/agent/agent/lib/osv.ts +0 -152
  20. package/templates/agent/agent/lib/scan.ts +0 -153
  21. package/templates/agent/agent/lib/store.ts +0 -151
  22. package/templates/agent/agent/lib/types.ts +0 -63
  23. package/templates/agent/agent/schedules/daily_audit.ts +0 -20
  24. package/templates/agent/agent/tools/check_advisories.ts +0 -27
  25. package/templates/agent/agent/tools/list_targets.ts +0 -21
  26. package/templates/agent/agent/tools/read_repo_file.ts +0 -31
  27. package/templates/agent/agent/tools/report_finding.ts +0 -59
  28. package/templates/agent/agent/tools/run_dependency_scan.ts +0 -16
  29. package/templates/agent/app/_components/ui.tsx +0 -29
  30. package/templates/agent/app/actions.ts +0 -120
  31. package/templates/agent/app/api/scan/route.ts +0 -34
  32. package/templates/agent/app/login/page.tsx +0 -40
  33. package/templates/agent/app/settings/page.tsx +0 -92
  34. package/templates/agent/app/targets/[id]/page.tsx +0 -127
  35. package/templates/agent/proxy.ts +0 -21
@@ -1,151 +0,0 @@
1
- import { Redis } from "@upstash/redis";
2
- import { DEFAULT_SETTINGS, type Finding, type Settings, type Target } from "./types";
3
-
4
- export interface Store {
5
- listTargets(): Promise<Target[]>;
6
- getTarget(id: string): Promise<Target | null>;
7
- saveTarget(target: Target): Promise<void>;
8
- deleteTarget(id: string): Promise<void>;
9
-
10
- getTargetToken(id: string): Promise<string | null>;
11
- setTargetToken(id: string, token: string | null): Promise<void>;
12
-
13
- listFindings(targetId?: string): Promise<Finding[]>;
14
- replaceTargetFindings(targetId: string, findings: Finding[]): Promise<void>;
15
- upsertFinding(finding: Finding): Promise<void>;
16
-
17
- getSettings(): Promise<Settings>;
18
- saveSettings(settings: Settings): Promise<void>;
19
- }
20
-
21
- const K = {
22
- targets: "oh:targets",
23
- token: (id: string) => `oh:token:${id}`,
24
- findings: (id: string) => `oh:findings:${id}`,
25
- settings: "oh:settings",
26
- };
27
-
28
- class RedisStore implements Store {
29
- constructor(private redis: Redis) {}
30
-
31
- async listTargets(): Promise<Target[]> {
32
- const all = (await this.redis.hgetall<Record<string, Target>>(K.targets)) ?? {};
33
- return Object.values(all).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
34
- }
35
- async getTarget(id: string): Promise<Target | null> {
36
- return (await this.redis.hget<Target>(K.targets, id)) ?? null;
37
- }
38
- async saveTarget(target: Target): Promise<void> {
39
- await this.redis.hset(K.targets, { [target.id]: target });
40
- }
41
- async deleteTarget(id: string): Promise<void> {
42
- await this.redis.hdel(K.targets, id);
43
- await this.redis.del(K.token(id), K.findings(id));
44
- }
45
- async getTargetToken(id: string): Promise<string | null> {
46
- return (await this.redis.get<string>(K.token(id))) ?? null;
47
- }
48
- async setTargetToken(id: string, token: string | null): Promise<void> {
49
- if (token) await this.redis.set(K.token(id), token);
50
- else await this.redis.del(K.token(id));
51
- }
52
- async listFindings(targetId?: string): Promise<Finding[]> {
53
- if (targetId) return (await this.redis.get<Finding[]>(K.findings(targetId))) ?? [];
54
- const targets = await this.listTargets();
55
- const lists = await Promise.all(targets.map((t) => this.listFindings(t.id)));
56
- return lists.flat();
57
- }
58
- async replaceTargetFindings(targetId: string, findings: Finding[]): Promise<void> {
59
- await this.redis.set(K.findings(targetId), findings);
60
- }
61
- async upsertFinding(finding: Finding): Promise<void> {
62
- const existing = await this.listFindings(finding.targetId);
63
- const next = existing.filter((f) => f.id !== finding.id);
64
- next.push(finding);
65
- await this.replaceTargetFindings(finding.targetId, next);
66
- }
67
- async getSettings(): Promise<Settings> {
68
- return (await this.redis.get<Settings>(K.settings)) ?? DEFAULT_SETTINGS;
69
- }
70
- async saveSettings(settings: Settings): Promise<void> {
71
- await this.redis.set(K.settings, settings);
72
- }
73
- }
74
-
75
- // Process-local fallback so the app boots and is testable without a KV store.
76
- // Not durable across serverless invocations — configure Redis/KV for production.
77
- const mem = {
78
- targets: new Map<string, Target>(),
79
- tokens: new Map<string, string>(),
80
- findings: new Map<string, Finding[]>(),
81
- settings: null as Settings | null,
82
- };
83
-
84
- class MemoryStore implements Store {
85
- async listTargets(): Promise<Target[]> {
86
- return [...mem.targets.values()].sort((a, b) => a.createdAt.localeCompare(b.createdAt));
87
- }
88
- async getTarget(id: string): Promise<Target | null> {
89
- return mem.targets.get(id) ?? null;
90
- }
91
- async saveTarget(target: Target): Promise<void> {
92
- mem.targets.set(target.id, target);
93
- }
94
- async deleteTarget(id: string): Promise<void> {
95
- mem.targets.delete(id);
96
- mem.tokens.delete(id);
97
- mem.findings.delete(id);
98
- }
99
- async getTargetToken(id: string): Promise<string | null> {
100
- return mem.tokens.get(id) ?? null;
101
- }
102
- async setTargetToken(id: string, token: string | null): Promise<void> {
103
- if (token) mem.tokens.set(id, token);
104
- else mem.tokens.delete(id);
105
- }
106
- async listFindings(targetId?: string): Promise<Finding[]> {
107
- if (targetId) return mem.findings.get(targetId) ?? [];
108
- return [...mem.findings.values()].flat();
109
- }
110
- async replaceTargetFindings(targetId: string, findings: Finding[]): Promise<void> {
111
- mem.findings.set(targetId, findings);
112
- }
113
- async upsertFinding(finding: Finding): Promise<void> {
114
- const existing = mem.findings.get(finding.targetId) ?? [];
115
- mem.findings.set(finding.targetId, [...existing.filter((f) => f.id !== finding.id), finding]);
116
- }
117
- async getSettings(): Promise<Settings> {
118
- return mem.settings ?? DEFAULT_SETTINGS;
119
- }
120
- async saveSettings(settings: Settings): Promise<void> {
121
- mem.settings = settings;
122
- }
123
- }
124
-
125
- let store: Store | null = null;
126
-
127
- export function getStore(): Store {
128
- if (store) return store;
129
-
130
- const url = process.env.KV_REST_API_URL ?? process.env.UPSTASH_REDIS_REST_URL;
131
- const token = process.env.KV_REST_API_TOKEN ?? process.env.UPSTASH_REDIS_REST_TOKEN;
132
-
133
- if (url && token) {
134
- store = new RedisStore(new Redis({ url, token }));
135
- } else {
136
- if (process.env.NODE_ENV === "production") {
137
- console.warn(
138
- "[openhacker] No KV/Redis env configured — using in-memory store. " +
139
- "Data will NOT persist. Add a Vercel KV / Upstash Redis integration.",
140
- );
141
- }
142
- store = new MemoryStore();
143
- }
144
- return store;
145
- }
146
-
147
- export function isPersistent(): boolean {
148
- const url = process.env.KV_REST_API_URL ?? process.env.UPSTASH_REDIS_REST_URL;
149
- const token = process.env.KV_REST_API_TOKEN ?? process.env.UPSTASH_REDIS_REST_TOKEN;
150
- return Boolean(url && token);
151
- }
@@ -1,63 +0,0 @@
1
- export type Severity = "critical" | "high" | "medium" | "low" | "info";
2
-
3
- export type FindingCategory =
4
- | "dependency"
5
- | "injection"
6
- | "authz"
7
- | "ssrf"
8
- | "secrets"
9
- | "xss"
10
- | "deserialization"
11
- | "other";
12
-
13
- export type Target = {
14
- id: string;
15
- name: string;
16
- /** "owner/name" on GitHub. */
17
- repo: string;
18
- branch: string;
19
- provider: "github";
20
- hasToken: boolean;
21
- autoRemediate: boolean;
22
- createdAt: string;
23
- lastScanAt?: string;
24
- lastScanStatus?: "ok" | "error";
25
- lastScanError?: string;
26
- };
27
-
28
- export type Finding = {
29
- id: string;
30
- targetId: string;
31
- title: string;
32
- severity: Severity;
33
- category: FindingCategory;
34
- packageName?: string;
35
- installedVersion?: string;
36
- advisoryIds?: string[];
37
- location?: { file: string; startLine?: number; endLine?: number; symbol?: string };
38
- proof: { status: "proven" | "likely" | "unconfirmed"; evidence?: string; poc?: string };
39
- remediation?: { summary: string; fixedVersion?: string; prUrl?: string };
40
- status: "open" | "triaged" | "fixed" | "ignored" | "false_positive";
41
- references?: string[];
42
- firstSeen: string;
43
- lastSeen: string;
44
- };
45
-
46
- export type Settings = {
47
- /** Gateway model id used by the eve agent for deep analysis. */
48
- model: string;
49
- autoRemediate: boolean;
50
- integrations: {
51
- github: { connected: boolean };
52
- hackerone: { connected: boolean; handle?: string };
53
- };
54
- };
55
-
56
- export const DEFAULT_SETTINGS: Settings = {
57
- model: "anthropic/claude-sonnet-4.6",
58
- autoRemediate: false,
59
- integrations: {
60
- github: { connected: false },
61
- hackerone: { connected: false },
62
- },
63
- };
@@ -1,20 +0,0 @@
1
- import { defineSchedule } from "eve/schedules";
2
- import { runScan } from "../lib/scan";
3
- import { getStore } from "../lib/store";
4
-
5
- // Becomes a Vercel Cron Job on deploy. Vercel evaluates cron in UTC.
6
- // Deterministically re-scans every configured target's dependencies against OSV,
7
- // so newly disclosed advisories are caught even when the code has not changed.
8
- export default defineSchedule({
9
- cron: "0 6 * * *",
10
- run({ waitUntil }) {
11
- waitUntil(
12
- (async () => {
13
- const targets = await getStore().listTargets();
14
- for (const target of targets) {
15
- await runScan(target.id);
16
- }
17
- })(),
18
- );
19
- },
20
- });
@@ -1,27 +0,0 @@
1
- import { defineTool } from "eve/tools";
2
- import { z } from "zod";
3
- import { queryOsv } from "../lib/osv";
4
-
5
- export default defineTool({
6
- description:
7
- "Check a package against the OSV.dev vulnerability database. Returns known " +
8
- "advisories (CVE/GHSA) affecting the given version. Use this to confirm whether " +
9
- "a dependency's INSTALLED version is actually vulnerable before reporting it.",
10
- inputSchema: z.object({
11
- name: z.string().min(1).describe("Package name, e.g. 'next' or 'lodash'."),
12
- version: z.string().optional().describe("Installed version, e.g. '14.1.0'."),
13
- ecosystem: z
14
- .enum(["npm", "PyPI", "Go", "crates.io", "Maven", "RubyGems", "NuGet"])
15
- .default("npm"),
16
- }),
17
- async execute({ name, version, ecosystem }) {
18
- const advisories = await queryOsv(name, version, ecosystem);
19
- return {
20
- package: name,
21
- version: version ?? null,
22
- ecosystem,
23
- advisoryCount: advisories.length,
24
- advisories,
25
- };
26
- },
27
- });
@@ -1,21 +0,0 @@
1
- import { defineTool } from "eve/tools";
2
- import { z } from "zod";
3
- import { getStore } from "../lib/store";
4
-
5
- export default defineTool({
6
- description: "List the repositories (targets) configured for scanning in this OpenHacker instance.",
7
- inputSchema: z.object({}),
8
- async execute() {
9
- const targets = await getStore().listTargets();
10
- return {
11
- targets: targets.map((t) => ({
12
- id: t.id,
13
- name: t.name,
14
- repo: t.repo,
15
- branch: t.branch,
16
- autoRemediate: t.autoRemediate,
17
- lastScanAt: t.lastScanAt ?? null,
18
- })),
19
- };
20
- },
21
- });
@@ -1,31 +0,0 @@
1
- import { defineTool } from "eve/tools";
2
- import { z } from "zod";
3
- import { getFile, listDir } from "../lib/github";
4
- import { getStore } from "../lib/store";
5
-
6
- export default defineTool({
7
- description:
8
- "Read a file or list a directory from a target's repository, for code-level " +
9
- "vulnerability analysis. Use directory listings to find route handlers, server " +
10
- "actions, and other trust boundaries, then read those files.",
11
- inputSchema: z.object({
12
- targetId: z.string(),
13
- path: z.string().default("").describe("Repo-relative path. Empty string lists the repo root."),
14
- mode: z.enum(["file", "list"]).default("file"),
15
- }),
16
- async execute({ targetId, path, mode }) {
17
- const store = getStore();
18
- const target = await store.getTarget(targetId);
19
- if (!target) return { ok: false as const, error: "Target not found" };
20
- const token = await store.getTargetToken(targetId);
21
-
22
- if (mode === "list") {
23
- const entries = await listDir(target.repo, path, target.branch, token);
24
- return { ok: true as const, mode, path, entries };
25
- }
26
-
27
- const content = await getFile(target.repo, path, target.branch, token);
28
- if (content == null) return { ok: false as const, error: `File not found: ${path}` };
29
- return { ok: true as const, mode, path, content: content.slice(0, 60_000) };
30
- },
31
- });
@@ -1,59 +0,0 @@
1
- import { createHash } from "node:crypto";
2
- import { defineTool } from "eve/tools";
3
- import { z } from "zod";
4
- import { getStore } from "../lib/store";
5
- import type { Finding } from "../lib/types";
6
-
7
- export default defineTool({
8
- description:
9
- "Persist a confirmed code-level vulnerability for a target. Only call this when " +
10
- "you have evidence the issue applies to the target's code. Be honest about proof status. " +
11
- "Dependency advisories are recorded by run_dependency_scan; use this for code findings.",
12
- inputSchema: z.object({
13
- targetId: z.string().describe("The target this finding belongs to."),
14
- title: z.string().min(1),
15
- severity: z.enum(["critical", "high", "medium", "low", "info"]),
16
- category: z.enum(["injection", "authz", "ssrf", "secrets", "xss", "deserialization", "other"]),
17
- location: z
18
- .object({
19
- file: z.string(),
20
- startLine: z.number().int().optional(),
21
- endLine: z.number().int().optional(),
22
- symbol: z.string().optional(),
23
- })
24
- .optional(),
25
- proof: z.object({
26
- status: z.enum(["proven", "likely", "unconfirmed"]),
27
- poc: z.string().optional(),
28
- evidence: z.string().optional(),
29
- }),
30
- remediation: z.object({ summary: z.string(), fixedVersion: z.string().optional() }).optional(),
31
- }),
32
- async execute(input) {
33
- const id = createHash("sha256")
34
- .update(`${input.targetId}::${input.category}::${input.location?.file ?? ""}::${input.title}`.toLowerCase())
35
- .digest("hex")
36
- .slice(0, 16);
37
-
38
- const now = new Date().toISOString();
39
- const store = getStore();
40
- const existing = (await store.listFindings(input.targetId)).find((f) => f.id === id);
41
-
42
- const finding: Finding = {
43
- id,
44
- targetId: input.targetId,
45
- title: input.title,
46
- severity: input.severity,
47
- category: input.category,
48
- location: input.location,
49
- proof: input.proof,
50
- remediation: input.remediation,
51
- status: existing?.status ?? "open",
52
- firstSeen: existing?.firstSeen ?? now,
53
- lastSeen: now,
54
- };
55
-
56
- await store.upsertFinding(finding);
57
- return { id, recorded: true };
58
- },
59
- });
@@ -1,16 +0,0 @@
1
- import { defineTool } from "eve/tools";
2
- import { z } from "zod";
3
- import { runScan } from "../lib/scan";
4
-
5
- export default defineTool({
6
- description:
7
- "Run the deterministic dependency vulnerability scan for a target: fetches its " +
8
- "manifest/lockfile, checks every dependency against OSV, and persists findings. " +
9
- "Run this first, then reason about which findings are actually reachable.",
10
- inputSchema: z.object({
11
- targetId: z.string().describe("The target to scan."),
12
- }),
13
- async execute({ targetId }) {
14
- return runScan(targetId);
15
- },
16
- });
@@ -1,29 +0,0 @@
1
- import type { Finding, Severity } from "@/agent/lib/types";
2
-
3
- const ORDER: Severity[] = ["critical", "high", "medium", "low", "info"];
4
-
5
- export function SeverityBadge({ severity }: { severity: Severity }) {
6
- return <span className={`badge sev-${severity}`}>{severity}</span>;
7
- }
8
-
9
- export function SeverityCounts({ findings }: { findings: Finding[] }) {
10
- const open = findings.filter((f) => f.status === "open" || f.status === "triaged");
11
- const counts = ORDER.map((sev) => ({
12
- sev,
13
- n: open.filter((f) => f.severity === sev).length,
14
- })).filter((c) => c.n > 0);
15
-
16
- if (counts.length === 0) {
17
- return <span className="mono-sm">no open findings</span>;
18
- }
19
-
20
- return (
21
- <span className="counts">
22
- {counts.map((c) => (
23
- <span key={c.sev} className={`badge sev-${c.sev}`}>
24
- {c.n} {c.sev}
25
- </span>
26
- ))}
27
- </span>
28
- );
29
- }
@@ -1,120 +0,0 @@
1
- "use server";
2
-
3
- import { cookies } from "next/headers";
4
- import { redirect } from "next/navigation";
5
- import { revalidatePath } from "next/cache";
6
- import { SESSION_COOKIE, adminPassword, sessionToken } from "@/agent/lib/auth";
7
- import { checkRepoAccess } from "@/agent/lib/github";
8
- import { runScan } from "@/agent/lib/scan";
9
- import { getStore } from "@/agent/lib/store";
10
- import { DEFAULT_SETTINGS, type Finding, type Target } from "@/agent/lib/types";
11
-
12
- function parseRepo(input: string): string | null {
13
- const trimmed = input.trim().replace(/\.git$/, "");
14
- const url = trimmed.match(/github\.com[/:]([^/]+\/[^/]+)/);
15
- const candidate = url ? url[1] : trimmed;
16
- return /^[^/\s]+\/[^/\s]+$/.test(candidate) ? candidate : null;
17
- }
18
-
19
- export async function login(formData: FormData): Promise<void> {
20
- const password = String(formData.get("password") ?? "");
21
- const next = String(formData.get("next") ?? "/") || "/";
22
- const expected = adminPassword();
23
-
24
- if (!expected || password !== expected) {
25
- redirect(`/login?error=1&next=${encodeURIComponent(next)}`);
26
- }
27
-
28
- const jar = await cookies();
29
- jar.set(SESSION_COOKIE, await sessionToken(password), {
30
- httpOnly: true,
31
- sameSite: "lax",
32
- secure: process.env.NODE_ENV === "production",
33
- path: "/",
34
- maxAge: 60 * 60 * 24 * 30,
35
- });
36
- redirect(next);
37
- }
38
-
39
- export async function logout(): Promise<void> {
40
- const jar = await cookies();
41
- jar.delete(SESSION_COOKIE);
42
- redirect("/login");
43
- }
44
-
45
- export async function addTarget(formData: FormData): Promise<void> {
46
- const repo = parseRepo(String(formData.get("repo") ?? ""));
47
- if (!repo) redirect("/?error=invalid-repo");
48
-
49
- const name = String(formData.get("name") ?? "").trim() || repo.split("/")[1];
50
- const branchInput = String(formData.get("branch") ?? "").trim();
51
- const token = String(formData.get("token") ?? "").trim();
52
- const autoRemediate = formData.get("autoRemediate") === "on";
53
-
54
- const access = await checkRepoAccess(repo, token || null);
55
- const branch = branchInput || access.defaultBranch || "main";
56
-
57
- const target: Target = {
58
- id: crypto.randomUUID(),
59
- name,
60
- repo,
61
- branch,
62
- provider: "github",
63
- hasToken: Boolean(token),
64
- autoRemediate,
65
- createdAt: new Date().toISOString(),
66
- };
67
-
68
- const store = getStore();
69
- await store.saveTarget(target);
70
- if (token) await store.setTargetToken(target.id, token);
71
-
72
- revalidatePath("/");
73
- redirect(`/targets/${target.id}`);
74
- }
75
-
76
- export async function deleteTarget(formData: FormData): Promise<void> {
77
- const id = String(formData.get("id") ?? "");
78
- if (id) await getStore().deleteTarget(id);
79
- revalidatePath("/");
80
- redirect("/");
81
- }
82
-
83
- export async function scanTarget(formData: FormData): Promise<void> {
84
- const id = String(formData.get("id") ?? "");
85
- if (id) await runScan(id);
86
- revalidatePath("/");
87
- revalidatePath(`/targets/${id}`);
88
- }
89
-
90
- export async function setFindingStatus(formData: FormData): Promise<void> {
91
- const targetId = String(formData.get("targetId") ?? "");
92
- const findingId = String(formData.get("findingId") ?? "");
93
- const status = String(formData.get("status") ?? "open") as Finding["status"];
94
-
95
- const store = getStore();
96
- const findings = await store.listFindings(targetId);
97
- const match = findings.find((f) => f.id === findingId);
98
- if (match) await store.upsertFinding({ ...match, status });
99
-
100
- revalidatePath(`/targets/${targetId}`);
101
- }
102
-
103
- export async function saveSettings(formData: FormData): Promise<void> {
104
- const store = getStore();
105
- const current = await store.getSettings();
106
- await store.saveSettings({
107
- ...DEFAULT_SETTINGS,
108
- ...current,
109
- model: String(formData.get("model") ?? current.model) || DEFAULT_SETTINGS.model,
110
- autoRemediate: formData.get("autoRemediate") === "on",
111
- integrations: {
112
- github: { connected: formData.get("githubConnected") === "on" },
113
- hackerone: {
114
- connected: formData.get("hackeroneConnected") === "on",
115
- handle: String(formData.get("hackeroneHandle") ?? "").trim() || undefined,
116
- },
117
- },
118
- });
119
- revalidatePath("/settings");
120
- }
@@ -1,34 +0,0 @@
1
- import { runScan } from "@/agent/lib/scan";
2
- import { getStore } from "@/agent/lib/store";
3
-
4
- export const runtime = "nodejs";
5
-
6
- function authorized(req: Request): boolean {
7
- const token = process.env.OPENHACKER_API_TOKEN;
8
- if (!token) return true; // no API token configured — allow (dev)
9
- const header = req.headers.get("authorization") ?? "";
10
- return header === `Bearer ${token}`;
11
- }
12
-
13
- /** Trigger a scan programmatically. Body: { targetId?: string } — omit to scan all. */
14
- export async function POST(req: Request): Promise<Response> {
15
- if (!authorized(req)) {
16
- return Response.json({ error: "unauthorized" }, { status: 401 });
17
- }
18
-
19
- let targetId: string | undefined;
20
- try {
21
- const body = (await req.json()) as { targetId?: string };
22
- targetId = body.targetId;
23
- } catch {
24
- // no body — scan all
25
- }
26
-
27
- const store = getStore();
28
- const targets = targetId ? [targetId] : (await store.listTargets()).map((t) => t.id);
29
- const results = await Promise.all(
30
- targets.map(async (id) => ({ targetId: id, ...(await runScan(id)) })),
31
- );
32
-
33
- return Response.json({ scanned: results.length, results });
34
- }
@@ -1,40 +0,0 @@
1
- import { authEnabled } from "@/agent/lib/auth";
2
- import { login } from "../actions";
3
-
4
- export const dynamic = "force-dynamic";
5
-
6
- export default async function LoginPage({
7
- searchParams,
8
- }: {
9
- searchParams: Promise<{ error?: string; next?: string }>;
10
- }) {
11
- const { error, next } = await searchParams;
12
-
13
- return (
14
- <main className="container">
15
- <div className="login-wrap">
16
- <h1>
17
- open<span style={{ color: "var(--accent)" }}>hacker</span>
18
- </h1>
19
- <p className="sub">Sign in to your instance.</p>
20
-
21
- {!authEnabled() ? (
22
- <div className="banner">
23
- No admin password is set. The dashboard is currently open — set{" "}
24
- <code>OPENHACKER_ADMIN_PASSWORD</code> to require sign-in.
25
- </div>
26
- ) : null}
27
- {error ? <div className="banner">Incorrect password.</div> : null}
28
-
29
- <form action={login} className="panel">
30
- <input type="hidden" name="next" value={next ?? "/"} />
31
- <label htmlFor="password">Password</label>
32
- <input id="password" name="password" type="password" autoFocus required />
33
- <button type="submit" style={{ marginTop: 14 }}>
34
- Sign in
35
- </button>
36
- </form>
37
- </div>
38
- </main>
39
- );
40
- }