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,103 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// CodeProbe Scraper Cron - Hourly dependency change detector
|
|
3
|
+
|
|
4
|
+
import { mkdir, writeFile } from "fs/promises";
|
|
5
|
+
import { existsSync } from "fs";
|
|
6
|
+
import path from "path";
|
|
7
|
+
|
|
8
|
+
const CODEPROBE_DIR = path.join(process.env.HOME || process.env.USERPROFILE || "/tmp", ".codeprobe");
|
|
9
|
+
const HASH_FILE = path.join(CODEPROBE_DIR, "last-scan-hash.txt");
|
|
10
|
+
const SERVER_URL = process.env.CODEPROBE_SERVER_URL || "http://localhost:3000";
|
|
11
|
+
|
|
12
|
+
async function computeDependencyHash(): Promise<string> {
|
|
13
|
+
const lockfiles = ["package-lock.json", "bun.lock", "yarn.lock", "pnpm-lock.yaml"];
|
|
14
|
+
let lockfileContent = "";
|
|
15
|
+
|
|
16
|
+
for (const name of lockfiles) {
|
|
17
|
+
const filePath = path.join(process.cwd(), name);
|
|
18
|
+
if (existsSync(filePath)) {
|
|
19
|
+
lockfileContent = await Bun.file(filePath).text();
|
|
20
|
+
break;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const packagePath = path.join(process.cwd(), "package.json");
|
|
25
|
+
let packageContent = "";
|
|
26
|
+
if (existsSync(packagePath)) {
|
|
27
|
+
packageContent = await Bun.file(packagePath).text();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const combined = packageContent + lockfileContent;
|
|
31
|
+
const hasher = new Bun.CryptoHasher("sha256");
|
|
32
|
+
hasher.update(combined);
|
|
33
|
+
return hasher.digest("hex");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function getStoredHash(): Promise<string | null> {
|
|
37
|
+
try {
|
|
38
|
+
if (existsSync(HASH_FILE)) {
|
|
39
|
+
return (await Bun.file(HASH_FILE).text()).trim();
|
|
40
|
+
}
|
|
41
|
+
} catch (error) {
|
|
42
|
+
console.warn("[Scraper] Failed to read hash:", error instanceof Error ? error.message : "unknown");
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function saveHash(hash: string): Promise<void> {
|
|
48
|
+
if (!existsSync(CODEPROBE_DIR)) {
|
|
49
|
+
await mkdir(CODEPROBE_DIR, { mode: 0o700, recursive: true });
|
|
50
|
+
}
|
|
51
|
+
await writeFile(HASH_FILE, hash, "utf-8");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function triggerScan(): Promise<boolean> {
|
|
55
|
+
try {
|
|
56
|
+
const response = await fetch(`${SERVER_URL}/api/scan`, {
|
|
57
|
+
method: "POST",
|
|
58
|
+
headers: { "Content-Type": "application/json" },
|
|
59
|
+
body: JSON.stringify({ repoPath: "." }),
|
|
60
|
+
signal: AbortSignal.timeout(30000),
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
if (!response.ok) {
|
|
64
|
+
console.error(`[Scraper] Server error: ${response.status}`);
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const result = await response.json();
|
|
69
|
+
console.log(`[Scraper] Scan triggered (ID: ${result.scanId})`);
|
|
70
|
+
return true;
|
|
71
|
+
} catch (error) {
|
|
72
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
73
|
+
console.error(`[Scraper] Failed: ${msg}`);
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function main(): Promise<void> {
|
|
79
|
+
try {
|
|
80
|
+
const currentHash = await computeDependencyHash();
|
|
81
|
+
const storedHash = await getStoredHash();
|
|
82
|
+
|
|
83
|
+
if (storedHash === currentHash) {
|
|
84
|
+
console.log("[Scraper] Dependencies unchanged, skipping scan");
|
|
85
|
+
process.exit(0);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
console.log("[Scraper] New packages detected, scanning...");
|
|
89
|
+
|
|
90
|
+
const success = await triggerScan();
|
|
91
|
+
if (success) {
|
|
92
|
+
await saveHash(currentHash);
|
|
93
|
+
} else {
|
|
94
|
+
console.warn("[Scraper] Trigger failed, will retry next hour");
|
|
95
|
+
process.exit(1);
|
|
96
|
+
}
|
|
97
|
+
} catch (error) {
|
|
98
|
+
console.error("[Scraper] Fatal:", error instanceof Error ? error.message : String(error));
|
|
99
|
+
process.exit(2);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
main();
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
|
|
4
|
+
// API Endpoints
|
|
5
|
+
export const API_ENDPOINTS = {
|
|
6
|
+
BRIGHT_DATA: process.env.BRIGHT_DATA_API_URL || "https://api.brightdata.com",
|
|
7
|
+
DAYTONA: process.env.DAYTONA_API_URL || "https://api.daytona.io",
|
|
8
|
+
NOSANA: process.env.NOSANA_API_URL || "https://api.nosana.io",
|
|
9
|
+
NVD: "https://services.nvd.nist.gov/rest/json/cves/2.0",
|
|
10
|
+
SNYK: "https://security.snyk.io/api/v1/vulnerabilities",
|
|
11
|
+
} as const;
|
|
12
|
+
|
|
13
|
+
// Timeout configurations
|
|
14
|
+
export const TIMEOUTS = {
|
|
15
|
+
BRIGHT_DATA_SCRAPE: 30000, // 30s
|
|
16
|
+
DAYTONA_SANDBOX: 60000, // 60s per CVE
|
|
17
|
+
NOSANA_LLM: 45000, // 45s
|
|
18
|
+
HTTP_GENERAL: 10000, // 10s
|
|
19
|
+
} as const;
|
|
20
|
+
|
|
21
|
+
// Sandbox configuration
|
|
22
|
+
export const SANDBOX_CONFIG = {
|
|
23
|
+
BASE_IMAGE: "node:20-alpine",
|
|
24
|
+
CPU: 1,
|
|
25
|
+
MEMORY: "512Mi",
|
|
26
|
+
DISK: "5Gi",
|
|
27
|
+
TIMEOUT_MS: TIMEOUTS.DAYTONA_SANDBOX,
|
|
28
|
+
} as const;
|
|
29
|
+
|
|
30
|
+
// File paths
|
|
31
|
+
export const PATHS = {
|
|
32
|
+
HOME: os.homedir(),
|
|
33
|
+
CODEPROBE_DIR: path.join(os.homedir(), '.codeprobe'),
|
|
34
|
+
CONFIG_DIR: path.join(os.homedir(), '.codeprobe'),
|
|
35
|
+
SCANS_DIR: path.join(os.homedir(), '.codeprobe', 'scans'),
|
|
36
|
+
CONFIG_FILE: path.join(os.homedir(), '.codeprobe', 'config.json'),
|
|
37
|
+
LATEST_SCAN: path.join(os.homedir(), '.codeprobe', 'scans', 'latest.json'),
|
|
38
|
+
CVE_CACHE: path.join(process.cwd(), 'cve-cache.json'),
|
|
39
|
+
CACHE_FILE: path.join(process.cwd(), 'cve-cache.json'),
|
|
40
|
+
PATCHES_FILE: path.join(process.cwd(), 'patches.json'),
|
|
41
|
+
} as const;
|
|
42
|
+
|
|
43
|
+
// Retry configuration
|
|
44
|
+
export const RETRY_CONFIG = {
|
|
45
|
+
MAX_RETRIES: 2,
|
|
46
|
+
INITIAL_DELAY_MS: 1000,
|
|
47
|
+
BACKOFF_MULTIPLIER: 2,
|
|
48
|
+
} as const;
|
|
49
|
+
|
|
50
|
+
// File permissions
|
|
51
|
+
export const FILE_PERMISSIONS = {
|
|
52
|
+
DIR: 0o700, // drwx------
|
|
53
|
+
FILE: 0o600, // -rw-------
|
|
54
|
+
} as const;
|
|
55
|
+
|
|
56
|
+
// Risk scoring
|
|
57
|
+
export const RISK_SCORE_WEIGHTS = {
|
|
58
|
+
EXPLOITABLE: 10,
|
|
59
|
+
THEORETICAL: 3,
|
|
60
|
+
} as const;
|
|
61
|
+
|
|
62
|
+
// Exit codes
|
|
63
|
+
export const EXIT_CODES = {
|
|
64
|
+
SUCCESS: 0,
|
|
65
|
+
VULNERABILITIES_FOUND: 1,
|
|
66
|
+
SCAN_FAILED: 2,
|
|
67
|
+
} as const;
|
|
68
|
+
|
|
69
|
+
// App metadata
|
|
70
|
+
export const APP_VERSION = '1.0.0';
|
|
71
|
+
export const APP_NAME = 'CodeProbe';
|
|
72
|
+
|
|
73
|
+
// Demo CVE for testing
|
|
74
|
+
export const DEMO_CVE = {
|
|
75
|
+
id: "CVE-2022-29078",
|
|
76
|
+
package: "ejs",
|
|
77
|
+
affected_versions: ["3.1.0", "3.1.1", "3.1.2", "3.1.3", "3.1.4", "3.1.5", "3.1.6"],
|
|
78
|
+
fixed_version: "3.1.7",
|
|
79
|
+
severity: "CRITICAL",
|
|
80
|
+
cvss: 9.8,
|
|
81
|
+
description:
|
|
82
|
+
"ejs before 3.1.7 has a template injection vulnerability that allows arbitrary code execution",
|
|
83
|
+
exploit_type: "Template Injection RCE",
|
|
84
|
+
} as const;
|
|
85
|
+
|
|
86
|
+
// Demo CVE ID and package (alternative format used in tests)
|
|
87
|
+
export const DEMO_CVE_ID = 'CVE-2023-44487';
|
|
88
|
+
export const DEMO_PACKAGE = 'http2-server';
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
// CVE schema from vulnerability scraper (Stage 1: Bright Data, NVD, Snyk)
|
|
4
|
+
export const CVESchema = z.object({
|
|
5
|
+
id: z.string(),
|
|
6
|
+
package: z.string(),
|
|
7
|
+
affected_versions: z.array(z.string()),
|
|
8
|
+
fixed_version: z.string(),
|
|
9
|
+
severity: z.enum(["CRITICAL", "HIGH", "MEDIUM", "LOW"]),
|
|
10
|
+
cvss: z.number().min(0).max(10),
|
|
11
|
+
description: z.string(),
|
|
12
|
+
cwe: z.string().optional(),
|
|
13
|
+
exploit_url: z.string().optional(),
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
export type CVE = z.infer<typeof CVESchema>;
|
|
17
|
+
|
|
18
|
+
// CVE result in a scan report (includes verification results from sandbox)
|
|
19
|
+
export const ScanCVESchema = z.object({
|
|
20
|
+
id: z.string(),
|
|
21
|
+
package: z.string(),
|
|
22
|
+
version_vulnerable: z.string(),
|
|
23
|
+
version_fixed: z.string().optional(),
|
|
24
|
+
severity: z.enum(["CRITICAL", "HIGH", "MEDIUM", "LOW"]),
|
|
25
|
+
cvss: z.number().min(0).max(10),
|
|
26
|
+
description: z.string(),
|
|
27
|
+
exploitable: z.boolean(),
|
|
28
|
+
exploit_evidence: z.string().optional(),
|
|
29
|
+
patch_diff: z.string().optional(),
|
|
30
|
+
patch_version: z.string().optional(),
|
|
31
|
+
verification_time_ms: z.number().optional(),
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
export type ScanCVE = z.infer<typeof ScanCVESchema>;
|
|
35
|
+
|
|
36
|
+
// Complete scan result (Stage 1 + Stage 2)
|
|
37
|
+
export const ScanSchema = z.object({
|
|
38
|
+
id: z.string(),
|
|
39
|
+
timestamp: z.string().datetime(),
|
|
40
|
+
repo_url: z.string(),
|
|
41
|
+
repo_path: z.string().optional(),
|
|
42
|
+
cves: z.array(ScanCVESchema),
|
|
43
|
+
risk_score: z.number().min(0).max(10),
|
|
44
|
+
exploitable_count: z.number(),
|
|
45
|
+
theoretical_count: z.number(),
|
|
46
|
+
total_dependencies: z.number(),
|
|
47
|
+
patches_available: z.number().optional(),
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
export type Scan = z.infer<typeof ScanSchema>;
|
|
51
|
+
|
|
52
|
+
// Report with summary (for CLI output and Dashboard)
|
|
53
|
+
export const ReportSchema = z.object({
|
|
54
|
+
scan: ScanSchema,
|
|
55
|
+
summary: z.object({
|
|
56
|
+
total_cves: z.number(),
|
|
57
|
+
exploitable_count: z.number(),
|
|
58
|
+
theoretical_count: z.number(),
|
|
59
|
+
scan_duration_ms: z.number(),
|
|
60
|
+
}),
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
export type Report = z.infer<typeof ReportSchema>;
|
|
64
|
+
|
|
65
|
+
// Dependency from package.json
|
|
66
|
+
export interface Dependency {
|
|
67
|
+
name: string;
|
|
68
|
+
version: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Dependency matched with CVE
|
|
72
|
+
export interface DependencyMatch {
|
|
73
|
+
dependency: Dependency;
|
|
74
|
+
cves: CVE[];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Result from CVE scraper (Bright Data, NVD, etc.)
|
|
78
|
+
export interface ScrapeResult {
|
|
79
|
+
cves: CVE[];
|
|
80
|
+
source: string;
|
|
81
|
+
timestamp: string;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Result from exploit sandbox (Daytona)
|
|
85
|
+
export interface SandboxResult {
|
|
86
|
+
exploit_ran: boolean;
|
|
87
|
+
exit_code: number;
|
|
88
|
+
stdout: string;
|
|
89
|
+
stderr: string;
|
|
90
|
+
success: boolean;
|
|
91
|
+
time_ms: number;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Scan event for progress reporting
|
|
95
|
+
export interface ScanEvent {
|
|
96
|
+
phase:
|
|
97
|
+
| 'parsing'
|
|
98
|
+
| 'scraping'
|
|
99
|
+
| 'matching'
|
|
100
|
+
| 'sandboxing'
|
|
101
|
+
| 'verification'
|
|
102
|
+
| 'patching'
|
|
103
|
+
| 'report';
|
|
104
|
+
status: 'start' | 'progress' | 'complete' | 'error';
|
|
105
|
+
message: string;
|
|
106
|
+
timestamp: string;
|
|
107
|
+
level: 'info' | 'warn' | 'error' | 'success';
|
|
108
|
+
metadata?: Record<string, unknown>;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// CLI options
|
|
112
|
+
export interface CliOptions {
|
|
113
|
+
fix?: boolean;
|
|
114
|
+
json?: boolean;
|
|
115
|
+
verbose?: boolean;
|
|
116
|
+
exportFormat?: 'json' | 'html' | 'text';
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Scan result with duration
|
|
120
|
+
export interface ScanResult {
|
|
121
|
+
report: Report;
|
|
122
|
+
duration_ms: number;
|
|
123
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
|
|
3
|
+
export function formatRiskScore(score: number): string {
|
|
4
|
+
const level = getRiskLevel(score);
|
|
5
|
+
const color = getRiskColor(score);
|
|
6
|
+
return color(`${score.toFixed(1)}/10 (${level})`);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function getRiskLevel(score: number): 'CRITICAL' | 'HIGH' | 'MEDIUM' | 'LOW' {
|
|
10
|
+
if (score >= 8) return 'CRITICAL';
|
|
11
|
+
if (score >= 6) return 'HIGH';
|
|
12
|
+
if (score >= 4) return 'MEDIUM';
|
|
13
|
+
return 'LOW';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function getRiskColor(score: number) {
|
|
17
|
+
if (score >= 8) return chalk.red;
|
|
18
|
+
if (score >= 6) return chalk.yellow;
|
|
19
|
+
if (score >= 4) return chalk.cyan;
|
|
20
|
+
return chalk.green;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function formatSeverity(severity: string): string {
|
|
24
|
+
switch (severity) {
|
|
25
|
+
case 'CRITICAL':
|
|
26
|
+
return chalk.red(severity);
|
|
27
|
+
case 'HIGH':
|
|
28
|
+
return chalk.yellow(severity);
|
|
29
|
+
case 'MEDIUM':
|
|
30
|
+
return chalk.cyan(severity);
|
|
31
|
+
case 'LOW':
|
|
32
|
+
return chalk.green(severity);
|
|
33
|
+
default:
|
|
34
|
+
return chalk.gray(severity);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function formatExploitable(exploitable: boolean): string {
|
|
39
|
+
if (exploitable) {
|
|
40
|
+
return chalk.red('✓ EXPLOITABLE');
|
|
41
|
+
}
|
|
42
|
+
return chalk.yellow('~ THEORETICAL');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function generateScanId(): string {
|
|
46
|
+
return `scan_${Date.now()}_${Math.random().toString(36).substring(7)}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function bytesToHuman(bytes: number): string {
|
|
50
|
+
const units = ['B', 'KB', 'MB', 'GB'];
|
|
51
|
+
let size = bytes;
|
|
52
|
+
let unitIdx = 0;
|
|
53
|
+
|
|
54
|
+
while (size >= 1024 && unitIdx < units.length - 1) {
|
|
55
|
+
size /= 1024;
|
|
56
|
+
unitIdx++;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return `${size.toFixed(2)} ${units[unitIdx]}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function msToHuman(ms: number): string {
|
|
63
|
+
if (ms < 1000) {
|
|
64
|
+
return `${ms}ms`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const totalSeconds = Math.floor(ms / 1000);
|
|
68
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
69
|
+
const seconds = totalSeconds % 60;
|
|
70
|
+
|
|
71
|
+
if (minutes > 0) {
|
|
72
|
+
return `${minutes}m ${seconds}s`;
|
|
73
|
+
}
|
|
74
|
+
return `${seconds}s`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function truncate(str: string, length: number): string {
|
|
78
|
+
if (str.length <= length) return str;
|
|
79
|
+
return str.substring(0, length - 3) + '...';
|
|
80
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'bun:test';
|
|
2
|
+
import { mkdirSync, rmSync, writeFileSync, readFileSync, existsSync } from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import os from 'os';
|
|
5
|
+
|
|
6
|
+
// Test utilities
|
|
7
|
+
const TEST_CONFIG_DIR = path.join(os.tmpdir(), 'codeprobe-test-' + Date.now());
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
mkdirSync(TEST_CONFIG_DIR, { recursive: true });
|
|
11
|
+
process.env.HOME = TEST_CONFIG_DIR;
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
if (existsSync(TEST_CONFIG_DIR)) {
|
|
16
|
+
rmSync(TEST_CONFIG_DIR, { recursive: true, force: true });
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe('CLI: Config Management', () => {
|
|
21
|
+
it('should export ensureConfigDir function', async () => {
|
|
22
|
+
const { ensureConfigDir } = await import('../cli/config');
|
|
23
|
+
|
|
24
|
+
expect(typeof ensureConfigDir).toBe('function');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('should save and load config', async () => {
|
|
28
|
+
const { setConfig, getConfig } = await import('../cli/config');
|
|
29
|
+
|
|
30
|
+
await setConfig('test_key', 'test_value');
|
|
31
|
+
const value = await getConfig('test_key');
|
|
32
|
+
|
|
33
|
+
expect(value).toBe('test_value');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('should handle missing config gracefully', async () => {
|
|
37
|
+
const { getConfig } = await import('../cli/config');
|
|
38
|
+
|
|
39
|
+
const value = await getConfig('nonexistent_key');
|
|
40
|
+
|
|
41
|
+
expect(value).toBeUndefined();
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe('CLI: Progress Logger', () => {
|
|
46
|
+
it('should format timestamps correctly', async () => {
|
|
47
|
+
const { ProgressLogger } = await import('../cli/progress');
|
|
48
|
+
|
|
49
|
+
const logger = new ProgressLogger(false);
|
|
50
|
+
|
|
51
|
+
expect(logger).toBeDefined();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('should handle events without crashing', async () => {
|
|
55
|
+
const { ProgressLogger } = await import('../cli/progress');
|
|
56
|
+
|
|
57
|
+
const logger = new ProgressLogger(false);
|
|
58
|
+
const event = {
|
|
59
|
+
phase: 'parsing' as const,
|
|
60
|
+
status: 'start' as const,
|
|
61
|
+
message: 'Test message',
|
|
62
|
+
timestamp: new Date().toISOString(),
|
|
63
|
+
level: 'info' as const,
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
// Should not throw
|
|
67
|
+
logger.log(event);
|
|
68
|
+
|
|
69
|
+
expect(logger).toBeDefined();
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe('CLI: Error Handling', () => {
|
|
74
|
+
it('should create CodeProbeError with code', async () => {
|
|
75
|
+
const { CodeProbeError } = await import('../cli/errors');
|
|
76
|
+
|
|
77
|
+
const error = new CodeProbeError('TEST_CODE', 'Test message', 'Details');
|
|
78
|
+
|
|
79
|
+
expect(error.code).toBe('TEST_CODE');
|
|
80
|
+
expect(error.message).toBe('Test message');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('should create specific error types', async () => {
|
|
84
|
+
const { BrightDataError, DaytonaError, GitError } = await import('../cli/errors');
|
|
85
|
+
|
|
86
|
+
const brightError = new BrightDataError('API timeout');
|
|
87
|
+
const daytonaError = new DaytonaError('Container failed');
|
|
88
|
+
const gitError = new GitError('Push failed');
|
|
89
|
+
|
|
90
|
+
expect(brightError.code).toBe('BRIGHT_DATA_FAILED');
|
|
91
|
+
expect(daytonaError.code).toBe('DAYTONA_FAILED');
|
|
92
|
+
expect(gitError.code).toBe('GIT_ERROR');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should retry with backoff', async () => {
|
|
96
|
+
const { retryWithBackoff } = await import('../cli/errors');
|
|
97
|
+
|
|
98
|
+
let attempts = 0;
|
|
99
|
+
const fn = async () => {
|
|
100
|
+
attempts++;
|
|
101
|
+
if (attempts < 2) throw new Error('Temporary failure');
|
|
102
|
+
return 'success';
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const result = await retryWithBackoff(fn, 2, 10);
|
|
106
|
+
|
|
107
|
+
expect(result).toBe('success');
|
|
108
|
+
expect(attempts).toBe(2);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe('CLI: Utils', () => {
|
|
113
|
+
it('should generate unique scan IDs', async () => {
|
|
114
|
+
const { generateScanId } = await import('../shared/utils');
|
|
115
|
+
|
|
116
|
+
const id1 = generateScanId();
|
|
117
|
+
const id2 = generateScanId();
|
|
118
|
+
|
|
119
|
+
expect(id1).not.toBe(id2);
|
|
120
|
+
expect(id1.startsWith('scan_')).toBe(true);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('should format risk scores', async () => {
|
|
124
|
+
const { formatRiskScore } = await import('../shared/utils');
|
|
125
|
+
|
|
126
|
+
const highRisk = formatRiskScore(8.5);
|
|
127
|
+
const mediumRisk = formatRiskScore(5.0);
|
|
128
|
+
const lowRisk = formatRiskScore(2.0);
|
|
129
|
+
|
|
130
|
+
expect(highRisk.includes('8.5')).toBe(true);
|
|
131
|
+
expect(mediumRisk.includes('5.0')).toBe(true);
|
|
132
|
+
expect(lowRisk.includes('2.0')).toBe(true);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('should get risk level correctly', async () => {
|
|
136
|
+
const { getRiskLevel } = await import('../shared/utils');
|
|
137
|
+
|
|
138
|
+
expect(getRiskLevel(9)).toBe('CRITICAL');
|
|
139
|
+
expect(getRiskLevel(7)).toBe('HIGH');
|
|
140
|
+
expect(getRiskLevel(5)).toBe('MEDIUM');
|
|
141
|
+
expect(getRiskLevel(1)).toBe('LOW');
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('should convert milliseconds to human readable', async () => {
|
|
145
|
+
const { msToHuman } = await import('../shared/utils');
|
|
146
|
+
|
|
147
|
+
expect(msToHuman(500)).toBe('500ms');
|
|
148
|
+
expect(msToHuman(5000)).toBe('5s');
|
|
149
|
+
expect(msToHuman(65000)).toBe('1m 5s');
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
describe('CLI: Types', () => {
|
|
154
|
+
it('should have correct CVE type structure', async () => {
|
|
155
|
+
const { Report } = await import('../shared/types');
|
|
156
|
+
|
|
157
|
+
const mockReport: Report = {
|
|
158
|
+
scan: {
|
|
159
|
+
id: 'scan_123',
|
|
160
|
+
timestamp: new Date().toISOString(),
|
|
161
|
+
repo_url: '.',
|
|
162
|
+
cves: [
|
|
163
|
+
{
|
|
164
|
+
id: 'CVE-2023-44487',
|
|
165
|
+
package: 'http2-server',
|
|
166
|
+
version_vulnerable: '1.0.0',
|
|
167
|
+
severity: 'CRITICAL',
|
|
168
|
+
cvss: 8.5,
|
|
169
|
+
exploitable: true,
|
|
170
|
+
exploit_evidence: 'DoS confirmed',
|
|
171
|
+
patch_version: '1.0.1',
|
|
172
|
+
},
|
|
173
|
+
],
|
|
174
|
+
risk_score: 8.5,
|
|
175
|
+
patches_available: 1,
|
|
176
|
+
},
|
|
177
|
+
summary: {
|
|
178
|
+
exploitable_count: 1,
|
|
179
|
+
theoretical_count: 0,
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
expect(mockReport.scan.id).toBe('scan_123');
|
|
184
|
+
expect(mockReport.scan.cves[0].id).toBe('CVE-2023-44487');
|
|
185
|
+
expect(mockReport.scan.cves[0].exploitable).toBe(true);
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
describe('CLI: Constants', () => {
|
|
190
|
+
it('should define exit codes correctly', async () => {
|
|
191
|
+
const { EXIT_CODES } = await import('../shared/constants');
|
|
192
|
+
|
|
193
|
+
expect(EXIT_CODES.SUCCESS).toBe(0);
|
|
194
|
+
expect(EXIT_CODES.VULNERABILITIES_FOUND).toBe(1);
|
|
195
|
+
expect(EXIT_CODES.SCAN_FAILED).toBe(2);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('should define file permissions', async () => {
|
|
199
|
+
const { FILE_PERMISSIONS } = await import('../shared/constants');
|
|
200
|
+
|
|
201
|
+
expect(FILE_PERMISSIONS.DIR).toBe(0o700);
|
|
202
|
+
expect(FILE_PERMISSIONS.FILE).toBe(0o600);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('should define risk score weights', async () => {
|
|
206
|
+
const { RISK_SCORE_WEIGHTS } = await import('../shared/constants');
|
|
207
|
+
|
|
208
|
+
expect(RISK_SCORE_WEIGHTS.EXPLOITABLE).toBe(10);
|
|
209
|
+
expect(RISK_SCORE_WEIGHTS.THEORETICAL).toBe(3);
|
|
210
|
+
});
|
|
211
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { test, expect } from "bun:test";
|
|
2
|
+
|
|
3
|
+
test("Dashboard: API server starts", async () => {
|
|
4
|
+
const server = Bun.serve({
|
|
5
|
+
port: 0,
|
|
6
|
+
fetch() {
|
|
7
|
+
return new Response("OK");
|
|
8
|
+
},
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
const res = await fetch(`http://${server.hostname}:${server.port}/`);
|
|
12
|
+
expect(res.ok).toBe(true);
|
|
13
|
+
|
|
14
|
+
server.stop();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test("Dashboard: OAuth endpoint exists", async () => {
|
|
18
|
+
// Test would require full server setup
|
|
19
|
+
// Simplified check: verify imports work
|
|
20
|
+
const authModule = await import("../api/auth.ts");
|
|
21
|
+
expect(authModule.exchangeGitHubToken).toBeDefined();
|
|
22
|
+
expect(authModule.validateGitHubToken).toBeDefined();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("Dashboard: Components render", async () => {
|
|
26
|
+
// Note: JSX/React component testing requires a DOM environment
|
|
27
|
+
// In production, use Playwright or similar for E2E tests
|
|
28
|
+
const componentModules = [
|
|
29
|
+
import("../dashboard/components/RiskGauge.tsx"),
|
|
30
|
+
import("../dashboard/components/CVETable.tsx"),
|
|
31
|
+
import("../dashboard/components/BusinessImpactCard.tsx"),
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
const results = await Promise.all(componentModules);
|
|
35
|
+
results.forEach((mod) => {
|
|
36
|
+
expect(Object.keys(mod).length).toBeGreaterThan(0);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "scan-demo-001",
|
|
3
|
+
"repo": "https://github.com/demo/vulnerable-app",
|
|
4
|
+
"riskScore": 8.5,
|
|
5
|
+
"timestamp": 1718281200,
|
|
6
|
+
"duration": 45,
|
|
7
|
+
"supplyChainWarnings": 0,
|
|
8
|
+
"cves": [
|
|
9
|
+
{
|
|
10
|
+
"id": "CVE-2023-44487",
|
|
11
|
+
"package": "http2-server",
|
|
12
|
+
"severity": "CRITICAL",
|
|
13
|
+
"status": "✅ Confirmed Exploitable",
|
|
14
|
+
"patch": "v1.0.1",
|
|
15
|
+
"description": "HTTP/2 server implementation vulnerable to rapid reset attacks. Attacker can trigger remote code execution.",
|
|
16
|
+
"affectedVersions": "1.0.0 - 1.0.0",
|
|
17
|
+
"poc": "$ codeprobe poc CVE-2023-44487\n[*] Setting up sandbox...\n[+] Server started on :8000\n[*] Sending exploit payload...\n[+] RCE confirmed: /bin/sh opened\n$ whoami\nroot\n$ exit\n[+] Patch required: Upgrade to http2-server >= 1.0.1",
|
|
18
|
+
"diff": "--- a/package.json\n+++ b/package.json\n@@ -5,1 +5,1 @@\n- \"http2-server\": \"1.0.0\"\n+ \"http2-server\": \"1.0.1\""
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"id": "CVE-2023-12345",
|
|
22
|
+
"package": "lodash",
|
|
23
|
+
"severity": "HIGH",
|
|
24
|
+
"status": "⚠️ Theoretical Risk",
|
|
25
|
+
"patch": "N/A",
|
|
26
|
+
"description": "Prototype pollution in lodash utility functions. Could allow arbitrary code execution.",
|
|
27
|
+
"affectedVersions": "4.17.0 - 4.17.20",
|
|
28
|
+
"poc": "Manual code review identified unsafe merge patterns.",
|
|
29
|
+
"diff": "--- a/src/utils.js\n+++ b/src/utils.js\n@@ -12,3 +12,5 @@\n function merge(obj, src) {\n+ if (!src) return obj;\n+ if (typeof src !== 'object') return obj;\n return Object.assign(obj, src);"
|
|
30
|
+
}
|
|
31
|
+
]
|
|
32
|
+
}
|