codeprobe-scanner 1.0.0
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/.claude/settings.local.json +19 -0
- package/.dockerignore +17 -0
- package/.env.development +8 -0
- package/.env.example +20 -0
- package/.env.setup +214 -0
- package/.github/workflows/codeprobe-scan.yml +137 -0
- package/.github/workflows/codeprobe.yml +84 -0
- package/.github/workflows/scan-schedule.yml +28 -0
- package/ANALYSIS_SUMMARY.md +365 -0
- package/API_INTEGRATIONS.md +469 -0
- package/BUILD_PLAYBOOK.md +349 -0
- package/CLAUDE.md +106 -0
- package/DEPLOY.md +452 -0
- package/DEPLOYMENT_STATUS.md +240 -0
- package/DEPLOY_CHECKLIST.md +316 -0
- package/Dockerfile +24 -0
- package/EXECUTION_PLAN.html +1086 -0
- package/IMPLEMENTATION_COMPLETE.md +288 -0
- package/IMPLEMENTATION_SUMMARY.md +443 -0
- package/INTERACTIVE_FIX_FLOW.md +308 -0
- package/MIGRATION_COMPLETE.md +327 -0
- package/ORCHESTRATOR_SYNTHESIS.json +80 -0
- package/PENDING_WORK.md +308 -0
- package/PREFLIGHT_PLAN.md +182 -0
- package/QUICKSTART.md +305 -0
- package/README.md +15 -0
- package/STAGE_1_SETUP_ENGINE.md +245 -0
- package/STAGE_2_ARCHITECTURE.md +714 -0
- package/STAGE_2_CLI_VERIFICATION.md +269 -0
- package/STAGE_2_COMPLETE.md +332 -0
- package/STAGE_2_IMPLEMENTATION_PLAN.md +679 -0
- package/STAGE_3_COMPLETE.md +246 -0
- package/STAGE_3_DASHBOARD_POLISH.md +371 -0
- package/STAGE_3_SETUP.md +155 -0
- package/VIDEODB_INTEGRATION.md +237 -0
- package/archived/DASHBOARD_UI_WALKTHROUGH.md +392 -0
- package/archived/FRONTEND_SETUP.md +236 -0
- package/archived/auth.ts +40 -0
- package/archived/dashboard/components/BusinessImpactCard.tsx +48 -0
- package/archived/dashboard/components/CVETable.tsx +104 -0
- package/archived/dashboard/components/ErrorBoundary.tsx +48 -0
- package/archived/dashboard/components/PatchDiffViewer.tsx +43 -0
- package/archived/dashboard/components/RiskGauge.tsx +64 -0
- package/archived/dashboard/frontend.tsx +104 -0
- package/archived/dashboard/hooks/useAuth.ts +32 -0
- package/archived/dashboard/hooks/useScan.ts +65 -0
- package/archived/dashboard/index.html +15 -0
- package/archived/dashboard/pages/LoginPage.tsx +28 -0
- package/archived/dashboard/pages/ScanDetailPage.tsx +143 -0
- package/archived/dashboard/pages/ScansListPage.tsx +160 -0
- package/bin/install-and-run.sh +91 -0
- package/bun.lock +603 -0
- package/codeprobe-prd.md +674 -0
- package/cve-cache.json +25 -0
- package/demo-vulnerable-app/.github/workflows/codeprobe.yml +32 -0
- package/demo-vulnerable-app/README.md +70 -0
- package/demo-vulnerable-app/package-lock.json +27 -0
- package/demo-vulnerable-app/package.json +15 -0
- package/demo-vulnerable-app/server.js +34 -0
- package/demo.sh +45 -0
- package/index.ts +19 -0
- package/package.json +28 -0
- package/patches.json +12 -0
- package/serve-dashboard.ts +23 -0
- package/src/api/server-cli.ts +270 -0
- package/src/api/server.ts +293 -0
- package/src/bot/server.ts +113 -0
- package/src/cli/commands/report.ts +92 -0
- package/src/cli/commands/scan-with-fix.ts +123 -0
- package/src/cli/commands/scan.ts +137 -0
- package/src/cli/config.ts +188 -0
- package/src/cli/errors.ts +120 -0
- package/src/cli/index.ts +137 -0
- package/src/cli/progress.ts +119 -0
- package/src/cli-server.ts +523 -0
- package/src/engine/index.ts +90 -0
- package/src/engine/matcher.ts +115 -0
- package/src/engine/parser.ts +91 -0
- package/src/engine/patcher.ts +280 -0
- package/src/engine/report.ts +137 -0
- package/src/engine/sandbox.ts +222 -0
- package/src/engine/scraper.ts +122 -0
- package/src/integrations/videodb.ts +153 -0
- package/src/mcp/server.ts +149 -0
- package/src/scraper-cron.ts +103 -0
- package/src/shared/constants.ts +88 -0
- package/src/shared/types.ts +123 -0
- package/src/shared/utils.ts +80 -0
- package/src/test/cli.test.ts +211 -0
- package/src/test/dashboard.test.ts +38 -0
- package/src/test/demo-scan.json +32 -0
- package/src/test/engine.test.ts +157 -0
- package/tailwind.config.js +11 -0
- package/tsconfig.json +30 -0
- package/verify-dashboard.ts +87 -0
- package/verify-env.sh +98 -0
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
import { exchangeGitHubToken, validateGitHubToken } from "./auth.ts";
|
|
2
|
+
import { createEngine } from "../engine/index.ts";
|
|
3
|
+
import { writeFile, mkdir } from "fs/promises";
|
|
4
|
+
import { existsSync } from "fs";
|
|
5
|
+
import path from "path";
|
|
6
|
+
import { PATHS, FILE_PERMISSIONS } from "../shared/constants.ts";
|
|
7
|
+
import { generateScanId } from "../shared/utils.ts";
|
|
8
|
+
|
|
9
|
+
const PORT = parseInt(process.env.PORT || "3000", 10);
|
|
10
|
+
const GITHUB_CLIENT_ID = process.env.GITHUB_CLIENT_ID || "";
|
|
11
|
+
const GITHUB_CLIENT_SECRET = process.env.GITHUB_CLIENT_SECRET || "";
|
|
12
|
+
|
|
13
|
+
interface AuthState {
|
|
14
|
+
tokens: Map<string, string>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const auth: AuthState = {
|
|
18
|
+
tokens: new Map(),
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
async function getScans() {
|
|
22
|
+
const home = process.env.HOME || process.env.USERPROFILE;
|
|
23
|
+
const scansDir = `${home}/.codeprobe/scans`;
|
|
24
|
+
try {
|
|
25
|
+
const fs = await import("fs");
|
|
26
|
+
const fileNames = fs.readdirSync(scansDir);
|
|
27
|
+
const scans = [];
|
|
28
|
+
|
|
29
|
+
for (const f of fileNames) {
|
|
30
|
+
// Skip non-JSON files, hidden files, and latest.json
|
|
31
|
+
if (!f.endsWith(".json") || f.startsWith(".") || f === "latest.json") {
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const path = `${scansDir}/${f}`;
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
// Skip broken symlinks
|
|
39
|
+
if (!fs.existsSync(path)) {
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const content = await Bun.file(path).text();
|
|
44
|
+
const parsed = JSON.parse(content);
|
|
45
|
+
|
|
46
|
+
// Verify it has the expected structure
|
|
47
|
+
if (parsed.scan && parsed.summary) {
|
|
48
|
+
scans.push(parsed);
|
|
49
|
+
}
|
|
50
|
+
} catch (err) {
|
|
51
|
+
console.warn(`Skipping ${f}:`, err instanceof Error ? err.message : String(err));
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Sort by timestamp (ISO strings compare correctly)
|
|
56
|
+
return scans.sort((a, b) =>
|
|
57
|
+
new Date(b.scan.timestamp).getTime() -
|
|
58
|
+
new Date(a.scan.timestamp).getTime()
|
|
59
|
+
);
|
|
60
|
+
} catch (e) {
|
|
61
|
+
console.error("Error reading scans:", e);
|
|
62
|
+
return [];
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function getScan(scanId: string) {
|
|
67
|
+
const filePath = `${process.env.HOME || process.env.USERPROFILE}/.codeprobe/scans/${scanId}.json`;
|
|
68
|
+
try {
|
|
69
|
+
const content = await Bun.file(filePath).text();
|
|
70
|
+
return JSON.parse(content);
|
|
71
|
+
} catch {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function saveReport(report: any): Promise<string> {
|
|
77
|
+
// Ensure directory exists
|
|
78
|
+
if (!existsSync(PATHS.SCANS_DIR)) {
|
|
79
|
+
await mkdir(PATHS.SCANS_DIR, { mode: FILE_PERMISSIONS.DIR, recursive: true });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const scanPath = path.join(PATHS.SCANS_DIR, `${report.scan.id}.json`);
|
|
83
|
+
const content = JSON.stringify(report, null, 2);
|
|
84
|
+
|
|
85
|
+
await writeFile(scanPath, content, "utf-8");
|
|
86
|
+
|
|
87
|
+
// Also update latest.json (copy, not symlink, for portability)
|
|
88
|
+
const latestPath = path.join(PATHS.SCANS_DIR, "latest.json");
|
|
89
|
+
await writeFile(latestPath, content, "utf-8");
|
|
90
|
+
|
|
91
|
+
return scanPath;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function requireAuth(req: Request): boolean {
|
|
95
|
+
const authHeader = req.headers.get("Authorization") || "";
|
|
96
|
+
const token = authHeader.replace("Bearer ", "");
|
|
97
|
+
|
|
98
|
+
// In dev mode, allow any Bearer token
|
|
99
|
+
if (process.env.NODE_ENV === "development") {
|
|
100
|
+
return token.length > 0;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return auth.tokens.has(token);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export default Bun.serve({
|
|
107
|
+
port: PORT,
|
|
108
|
+
development: process.env.NODE_ENV !== "production",
|
|
109
|
+
async fetch(req) {
|
|
110
|
+
const url = new URL(req.url);
|
|
111
|
+
const path = url.pathname;
|
|
112
|
+
|
|
113
|
+
// CORS preflight
|
|
114
|
+
if (req.method === "OPTIONS") {
|
|
115
|
+
return new Response(null, {
|
|
116
|
+
headers: {
|
|
117
|
+
"Access-Control-Allow-Origin": "http://localhost:5173",
|
|
118
|
+
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
|
119
|
+
"Access-Control-Allow-Headers": "Content-Type, Authorization",
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const corsHeaders = {
|
|
125
|
+
"Access-Control-Allow-Origin": "http://localhost:5173",
|
|
126
|
+
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
|
127
|
+
"Access-Control-Allow-Headers": "Content-Type, Authorization",
|
|
128
|
+
"Content-Type": "application/json",
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
// OAuth callback
|
|
132
|
+
if (path === "/api/auth/github" && req.method === "GET") {
|
|
133
|
+
const code = url.searchParams.get("code");
|
|
134
|
+
if (!code) {
|
|
135
|
+
return new Response(JSON.stringify({ error: "No code provided" }), {
|
|
136
|
+
status: 400,
|
|
137
|
+
headers: corsHeaders,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
const token = await exchangeGitHubToken(code, GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET);
|
|
143
|
+
if (!token) {
|
|
144
|
+
return new Response(JSON.stringify({ error: "Auth failed" }), {
|
|
145
|
+
status: 401,
|
|
146
|
+
headers: corsHeaders,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const sessionToken = `session_${Date.now()}`;
|
|
151
|
+
auth.tokens.set(sessionToken, token);
|
|
152
|
+
|
|
153
|
+
return new Response(JSON.stringify({ token: sessionToken }), {
|
|
154
|
+
status: 200,
|
|
155
|
+
headers: corsHeaders,
|
|
156
|
+
});
|
|
157
|
+
} catch (e) {
|
|
158
|
+
console.error("OAuth error:", e);
|
|
159
|
+
return new Response(JSON.stringify({ error: "Auth failed" }), {
|
|
160
|
+
status: 500,
|
|
161
|
+
headers: corsHeaders,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Logout
|
|
167
|
+
if (path === "/api/auth/logout" && req.method === "GET") {
|
|
168
|
+
const authHeader = req.headers.get("Authorization") || "";
|
|
169
|
+
const token = authHeader.replace("Bearer ", "");
|
|
170
|
+
auth.tokens.delete(token);
|
|
171
|
+
return new Response(JSON.stringify({ ok: true }), {
|
|
172
|
+
status: 200,
|
|
173
|
+
headers: corsHeaders,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Trigger a new scan
|
|
178
|
+
if (path === "/api/scan" && req.method === "POST") {
|
|
179
|
+
try {
|
|
180
|
+
const body = await req.json();
|
|
181
|
+
const repoPath = body.repoPath || ".";
|
|
182
|
+
|
|
183
|
+
console.log(`[API] Triggering scan for ${repoPath}`);
|
|
184
|
+
|
|
185
|
+
// Run scan in background and return immediately
|
|
186
|
+
const engine = createEngine();
|
|
187
|
+
const report = await engine.scan(repoPath);
|
|
188
|
+
await saveReport(report);
|
|
189
|
+
|
|
190
|
+
return new Response(JSON.stringify({
|
|
191
|
+
ok: true,
|
|
192
|
+
scanId: report.scan.id,
|
|
193
|
+
message: "Scan completed successfully"
|
|
194
|
+
}), {
|
|
195
|
+
status: 200,
|
|
196
|
+
headers: corsHeaders,
|
|
197
|
+
});
|
|
198
|
+
} catch (error) {
|
|
199
|
+
console.error("[API] Scan error:", error);
|
|
200
|
+
return new Response(JSON.stringify({
|
|
201
|
+
error: error instanceof Error ? error.message : "Scan failed"
|
|
202
|
+
}), {
|
|
203
|
+
status: 500,
|
|
204
|
+
headers: corsHeaders,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// List scans
|
|
210
|
+
if (path === "/api/scans" && req.method === "GET") {
|
|
211
|
+
if (!requireAuth(req)) {
|
|
212
|
+
return new Response(JSON.stringify({ error: "Unauthorized" }), {
|
|
213
|
+
status: 401,
|
|
214
|
+
headers: corsHeaders,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const scans = await getScans();
|
|
219
|
+
return new Response(JSON.stringify(scans), {
|
|
220
|
+
status: 200,
|
|
221
|
+
headers: corsHeaders,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Get scan detail
|
|
226
|
+
if (path.match(/^\/api\/scans\/[^/]+$/) && req.method === "GET") {
|
|
227
|
+
if (!requireAuth(req)) {
|
|
228
|
+
return new Response(JSON.stringify({ error: "Unauthorized" }), {
|
|
229
|
+
status: 401,
|
|
230
|
+
headers: corsHeaders,
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const scanId = path.split("/").pop()!;
|
|
235
|
+
const scan = await getScan(scanId);
|
|
236
|
+
|
|
237
|
+
if (!scan) {
|
|
238
|
+
return new Response(JSON.stringify({ error: "Not found" }), {
|
|
239
|
+
status: 404,
|
|
240
|
+
headers: corsHeaders,
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return new Response(JSON.stringify(scan), {
|
|
245
|
+
status: 200,
|
|
246
|
+
headers: corsHeaders,
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Serve dashboard (root path)
|
|
251
|
+
if (path === "/" || path === "") {
|
|
252
|
+
const dashboardHTML = await Bun.file(
|
|
253
|
+
`${import.meta.dir}/../dashboard/index.html`
|
|
254
|
+
).text();
|
|
255
|
+
return new Response(dashboardHTML, {
|
|
256
|
+
status: 200,
|
|
257
|
+
headers: { "Content-Type": "text/html" },
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Serve dashboard assets (frontend.tsx, etc.)
|
|
262
|
+
if (path.startsWith("/") && !path.startsWith("/api")) {
|
|
263
|
+
const dashboardPath = `${import.meta.dir}/../dashboard${path}`;
|
|
264
|
+
try {
|
|
265
|
+
const file = await Bun.file(dashboardPath).blob();
|
|
266
|
+
const contentType =
|
|
267
|
+
path.endsWith(".tsx") || path.endsWith(".ts")
|
|
268
|
+
? "application/typescript"
|
|
269
|
+
: path.endsWith(".css")
|
|
270
|
+
? "text/css"
|
|
271
|
+
: path.endsWith(".js")
|
|
272
|
+
? "application/javascript"
|
|
273
|
+
: "application/octet-stream";
|
|
274
|
+
return new Response(file, {
|
|
275
|
+
status: 200,
|
|
276
|
+
headers: { "Content-Type": contentType },
|
|
277
|
+
});
|
|
278
|
+
} catch {
|
|
279
|
+
// File not found, return 404
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Not found
|
|
284
|
+
return new Response(JSON.stringify({ error: "Not found" }), {
|
|
285
|
+
status: 404,
|
|
286
|
+
headers: corsHeaders,
|
|
287
|
+
});
|
|
288
|
+
},
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
console.log(`🚀 API server listening on http://localhost:${PORT}`);
|
|
292
|
+
console.log(`📊 Dashboard: http://localhost:${PORT}`);
|
|
293
|
+
console.log(`🔌 API: http://localhost:${PORT}/api/`);
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { createEngine } from "../engine/index.js";
|
|
2
|
+
import { exchangeGitHubToken, validateGitHubToken } from "../api/auth.js";
|
|
3
|
+
import { PATHS } from "../shared/constants.js";
|
|
4
|
+
|
|
5
|
+
const BOT_PORT = parseInt(process.env.BOT_PORT || "4000");
|
|
6
|
+
const GITHUB_WEBHOOK_SECRET = process.env.GITHUB_WEBHOOK_SECRET || "dev-secret";
|
|
7
|
+
|
|
8
|
+
interface WebhookPayload {
|
|
9
|
+
action?: string;
|
|
10
|
+
pull_request?: {
|
|
11
|
+
number: number;
|
|
12
|
+
head: { sha: string; ref: string };
|
|
13
|
+
base: { ref: string };
|
|
14
|
+
title: string;
|
|
15
|
+
body: string;
|
|
16
|
+
draft: boolean;
|
|
17
|
+
};
|
|
18
|
+
repository?: {
|
|
19
|
+
owner: { login: string };
|
|
20
|
+
name: string;
|
|
21
|
+
full_name: string;
|
|
22
|
+
html_url: string;
|
|
23
|
+
clone_url: string;
|
|
24
|
+
};
|
|
25
|
+
issue?: {
|
|
26
|
+
number: number;
|
|
27
|
+
};
|
|
28
|
+
comment?: {
|
|
29
|
+
body: string;
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function handlePullRequestEvent(payload: WebhookPayload) {
|
|
34
|
+
if (!payload.pull_request || !payload.repository) return;
|
|
35
|
+
|
|
36
|
+
const { number, head, base } = payload.pull_request;
|
|
37
|
+
const { owner, name, clone_url } = payload.repository;
|
|
38
|
+
|
|
39
|
+
console.log(`\n🔍 CodeProbe bot triggered for PR #${number} in ${owner.login}/${name}`);
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
// Clone repo to temp dir
|
|
43
|
+
const tempDir = `/tmp/codeprobe-pr-${number}-${Date.now()}`;
|
|
44
|
+
const { $, file } = await import("bun");
|
|
45
|
+
|
|
46
|
+
console.log(`📥 Cloning ${clone_url}...`);
|
|
47
|
+
// Clone would require shell execution - for MVP, we'll post a comment that scan is starting
|
|
48
|
+
|
|
49
|
+
// Post initial comment
|
|
50
|
+
const commentUrl = `https://api.github.com/repos/${owner.login}/${name}/issues/${number}/comments`;
|
|
51
|
+
const headers = {
|
|
52
|
+
Authorization: `Bearer ${process.env.GITHUB_TOKEN || ""}`,
|
|
53
|
+
Accept: "application/vnd.github.v3+json",
|
|
54
|
+
"Content-Type": "application/json",
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const initialComment = {
|
|
58
|
+
body: `## ⚡ CodeProbe Security Scan\n\n**Status:** ⏳ Running scan...\n**Powered by:** Bright Data | Daytona | Nosana\n\nAnalyzing PR for exploitable vulnerabilities...`,
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const response = await fetch(commentUrl, {
|
|
62
|
+
method: "POST",
|
|
63
|
+
headers,
|
|
64
|
+
body: JSON.stringify(initialComment),
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
if (!response.ok) {
|
|
68
|
+
console.error(`Failed to post comment: ${response.statusText}`);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
console.log(`✅ Posted initial comment to PR #${number}`);
|
|
73
|
+
|
|
74
|
+
// TODO: In production:
|
|
75
|
+
// 1. Clone repo
|
|
76
|
+
// 2. Run engine.scan()
|
|
77
|
+
// 3. Update comment with results
|
|
78
|
+
// 4. Create auto-fix PR if patches available
|
|
79
|
+
} catch (error) {
|
|
80
|
+
console.error("Bot error:", error instanceof Error ? error.message : String(error));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function handleRequest(req: Request): Promise<Response> {
|
|
85
|
+
const url = new URL(req.url);
|
|
86
|
+
|
|
87
|
+
// Health check
|
|
88
|
+
if (req.method === "GET" && url.pathname === "/health") {
|
|
89
|
+
return new Response(JSON.stringify({ status: "ok" }), { status: 200 });
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// GitHub webhook
|
|
93
|
+
if (req.method === "POST" && url.pathname === "/webhook") {
|
|
94
|
+
const payload: WebhookPayload = await req.json();
|
|
95
|
+
|
|
96
|
+
// Verify webhook signature (simplified - in production, verify X-Hub-Signature-256)
|
|
97
|
+
if (payload.action === "opened" || payload.action === "synchronize") {
|
|
98
|
+
await handlePullRequestEvent(payload);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return new Response(JSON.stringify({ status: "processed" }), { status: 200 });
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return new Response(JSON.stringify({ error: "Not Found" }), { status: 404 });
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const server = Bun.serve({
|
|
108
|
+
port: BOT_PORT,
|
|
109
|
+
fetch: handleRequest,
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
console.log(`🤖 CodeProbe Bot listening on http://localhost:${BOT_PORT}`);
|
|
113
|
+
console.log(`📝 Webhook URL: http://localhost:${BOT_PORT}/webhook`);
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { readFile } from 'fs/promises';
|
|
3
|
+
import { existsSync } from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { PATHS, EXIT_CODES } from '../../shared/constants.js';
|
|
6
|
+
import { Report } from '../../shared/types.js';
|
|
7
|
+
import { formatRiskScore, formatSeverity, formatExploitable } from '../../shared/utils.js';
|
|
8
|
+
import { ProgressLogger } from '../progress.js';
|
|
9
|
+
import { handleError, CodeProbeError } from '../errors.js';
|
|
10
|
+
|
|
11
|
+
async function loadLatestReport(): Promise<Report> {
|
|
12
|
+
if (!existsSync(PATHS.LATEST_SCAN)) {
|
|
13
|
+
throw new CodeProbeError(
|
|
14
|
+
'NO_REPORT',
|
|
15
|
+
'No scan report found',
|
|
16
|
+
`Run 'codeprobe scan' first to generate a report`
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const content = await readFile(PATHS.LATEST_SCAN, 'utf-8');
|
|
21
|
+
return JSON.parse(content);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function displayReportTable(report: Report): void {
|
|
25
|
+
const logger = new ProgressLogger();
|
|
26
|
+
logger.printSeparator();
|
|
27
|
+
|
|
28
|
+
console.log(chalk.bold('Last Scan Report'));
|
|
29
|
+
console.log(`ID: ${chalk.cyan(report.scan.id)}`);
|
|
30
|
+
console.log(`Time: ${report.scan.timestamp}`);
|
|
31
|
+
console.log(`Risk Score: ${formatRiskScore(report.scan.risk_score)}`);
|
|
32
|
+
console.log('');
|
|
33
|
+
|
|
34
|
+
if (report.scan.cves.length === 0) {
|
|
35
|
+
console.log(chalk.green('✓ No vulnerabilities found!'));
|
|
36
|
+
logger.printSeparator();
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Display table header
|
|
41
|
+
console.log(
|
|
42
|
+
chalk.bold(
|
|
43
|
+
'CVE ID'.padEnd(20) +
|
|
44
|
+
'Package'.padEnd(20) +
|
|
45
|
+
'Severity'.padEnd(12) +
|
|
46
|
+
'Exploitable'.padEnd(15) +
|
|
47
|
+
'Patch'
|
|
48
|
+
)
|
|
49
|
+
);
|
|
50
|
+
console.log(chalk.gray('─'.repeat(80)));
|
|
51
|
+
|
|
52
|
+
// Display each CVE
|
|
53
|
+
report.scan.cves.forEach((cve) => {
|
|
54
|
+
const cveId = cve.id.padEnd(20);
|
|
55
|
+
const pkg = `${cve.package}@${cve.version_vulnerable}`.padEnd(20);
|
|
56
|
+
const severity = formatSeverity(cve.severity).padEnd(12);
|
|
57
|
+
const exploitable = formatExploitable(cve.exploitable).padEnd(15);
|
|
58
|
+
const patch = cve.patch_version ? chalk.green(`→ ${cve.patch_version}`) : chalk.gray('none');
|
|
59
|
+
|
|
60
|
+
console.log(`${cveId}${pkg}${severity}${exploitable}${patch}`);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
logger.printSeparator();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function displayReportJSON(report: Report): void {
|
|
67
|
+
console.log(JSON.stringify(report, null, 2));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export async function reportCommand(args: string[]): Promise<void> {
|
|
71
|
+
const logger = new ProgressLogger();
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
const exportFormat = args.includes('--json') ? 'json' : 'text';
|
|
75
|
+
|
|
76
|
+
logger.logPhaseStart('report', 'Loading latest scan');
|
|
77
|
+
|
|
78
|
+
const report = await loadLatestReport();
|
|
79
|
+
|
|
80
|
+
logger.logPhaseComplete('report', 'Report loaded');
|
|
81
|
+
|
|
82
|
+
if (exportFormat === 'json') {
|
|
83
|
+
displayReportJSON(report);
|
|
84
|
+
} else {
|
|
85
|
+
displayReportTable(report);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
process.exit(EXIT_CODES.SUCCESS);
|
|
89
|
+
} catch (error) {
|
|
90
|
+
handleError(error, logger, true);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { execSync } from 'child_process';
|
|
3
|
+
import dayjs from 'dayjs';
|
|
4
|
+
import { Report } from '../../shared/types.js';
|
|
5
|
+
import { ProgressLogger } from '../progress.js';
|
|
6
|
+
import { GitError, handleError } from '../errors.js';
|
|
7
|
+
import { EXIT_CODES } from '../../shared/constants.js';
|
|
8
|
+
|
|
9
|
+
function getGitStatus(): string {
|
|
10
|
+
try {
|
|
11
|
+
return execSync('git status --porcelain', { encoding: 'utf-8' });
|
|
12
|
+
} catch {
|
|
13
|
+
throw new GitError('Not a git repository');
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function checkGitRepo(): boolean {
|
|
18
|
+
try {
|
|
19
|
+
execSync('git rev-parse --git-dir', { encoding: 'utf-8' });
|
|
20
|
+
return true;
|
|
21
|
+
} catch {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function isGitDirty(): boolean {
|
|
27
|
+
const status = getGitStatus();
|
|
28
|
+
return status.trim().length > 0;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function createBranch(name: string): void {
|
|
32
|
+
try {
|
|
33
|
+
execSync(`git checkout -b ${name}`, { encoding: 'utf-8' });
|
|
34
|
+
} catch (error) {
|
|
35
|
+
throw new GitError(`Failed to create branch: ${name}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function commitChanges(message: string): void {
|
|
40
|
+
try {
|
|
41
|
+
execSync('git add .', { encoding: 'utf-8' });
|
|
42
|
+
execSync(`git commit -m "${message}"`, { encoding: 'utf-8' });
|
|
43
|
+
} catch (error) {
|
|
44
|
+
throw new GitError('Failed to commit changes');
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function scanWithFixCommand(
|
|
49
|
+
args: string[],
|
|
50
|
+
report: Report,
|
|
51
|
+
logger: ProgressLogger
|
|
52
|
+
): Promise<void> {
|
|
53
|
+
console.log('');
|
|
54
|
+
logger.printSeparator();
|
|
55
|
+
logger.logPhaseStart('git', 'Preparing to apply patches');
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
// Check git repo exists
|
|
59
|
+
if (!checkGitRepo()) {
|
|
60
|
+
throw new GitError('Not a git repository. Run: git init');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Check git status
|
|
64
|
+
if (isGitDirty()) {
|
|
65
|
+
logger.logWarning('Git repository has uncommitted changes', 'Commit or stash first');
|
|
66
|
+
throw new GitError('Repository is dirty. Commit changes before applying patches.');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Filter for exploitable CVEs only
|
|
70
|
+
const exploitableCVEs = report.scan.cves.filter((cve) => cve.exploitable);
|
|
71
|
+
|
|
72
|
+
if (exploitableCVEs.length === 0) {
|
|
73
|
+
logger.logWarning('No exploitable CVEs found', 'Nothing to patch');
|
|
74
|
+
process.exit(EXIT_CODES.SUCCESS);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Create feature branch
|
|
78
|
+
const timestamp = dayjs().format('YYYY-MM-DD-HHmmss');
|
|
79
|
+
const branchName = `codeprobe-fix-${timestamp}`;
|
|
80
|
+
|
|
81
|
+
logger.logPhaseStart('git', `Creating branch: ${branchName}`);
|
|
82
|
+
createBranch(branchName);
|
|
83
|
+
logger.logPhaseComplete('git', `Branch created: ${branchName}`);
|
|
84
|
+
|
|
85
|
+
// Apply patches
|
|
86
|
+
for (const cve of exploitableCVEs) {
|
|
87
|
+
if (!cve.patch_diff) continue;
|
|
88
|
+
|
|
89
|
+
logger.logPhaseStart('patch', `Applying patch for ${cve.id}`);
|
|
90
|
+
|
|
91
|
+
// Mock: just log the patch
|
|
92
|
+
console.log(chalk.gray(` Patch preview:\n${cve.patch_diff.split('\n').slice(0, 5).join('\n')}`));
|
|
93
|
+
|
|
94
|
+
// In production: apply patch using git apply or manual file updates
|
|
95
|
+
// For now: mock success
|
|
96
|
+
logger.logPhaseComplete('patch', `Patched ${cve.package}: ${cve.version_vulnerable} → ${cve.patch_version}`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Commit with detailed message
|
|
100
|
+
const commitMsg =
|
|
101
|
+
`[CodeProbe] Fix ${exploitableCVEs.length} exploitable CVE(s)\n\n` +
|
|
102
|
+
exploitableCVEs
|
|
103
|
+
.map((cve) => `- ${cve.id} (${cve.package} ${cve.version_vulnerable} → ${cve.patch_version})`)
|
|
104
|
+
.join('\n');
|
|
105
|
+
|
|
106
|
+
logger.logPhaseStart('git', 'Committing patches');
|
|
107
|
+
commitChanges(commitMsg);
|
|
108
|
+
logger.logPhaseComplete('git', 'Changes committed');
|
|
109
|
+
|
|
110
|
+
// Show what to do next
|
|
111
|
+
console.log('');
|
|
112
|
+
console.log(chalk.green('✓ Patches applied successfully!'));
|
|
113
|
+
console.log(chalk.cyan(`\nNext steps:`));
|
|
114
|
+
console.log(chalk.cyan(` 1. Review changes: git diff main`));
|
|
115
|
+
console.log(chalk.cyan(` 2. Push branch: git push -u origin ${branchName}`));
|
|
116
|
+
console.log(chalk.cyan(` 3. Create PR on GitHub`));
|
|
117
|
+
|
|
118
|
+
logger.printSeparator();
|
|
119
|
+
process.exit(EXIT_CODES.SUCCESS);
|
|
120
|
+
} catch (error) {
|
|
121
|
+
handleError(error, logger, true);
|
|
122
|
+
}
|
|
123
|
+
}
|