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.
Files changed (96) hide show
  1. package/.claude/settings.local.json +19 -0
  2. package/.dockerignore +17 -0
  3. package/.env.development +8 -0
  4. package/.env.example +20 -0
  5. package/.env.setup +214 -0
  6. package/.github/workflows/codeprobe-scan.yml +137 -0
  7. package/.github/workflows/codeprobe.yml +84 -0
  8. package/.github/workflows/scan-schedule.yml +28 -0
  9. package/ANALYSIS_SUMMARY.md +365 -0
  10. package/API_INTEGRATIONS.md +469 -0
  11. package/BUILD_PLAYBOOK.md +349 -0
  12. package/CLAUDE.md +106 -0
  13. package/DEPLOY.md +452 -0
  14. package/DEPLOYMENT_STATUS.md +240 -0
  15. package/DEPLOY_CHECKLIST.md +316 -0
  16. package/Dockerfile +24 -0
  17. package/EXECUTION_PLAN.html +1086 -0
  18. package/IMPLEMENTATION_COMPLETE.md +288 -0
  19. package/IMPLEMENTATION_SUMMARY.md +443 -0
  20. package/INTERACTIVE_FIX_FLOW.md +308 -0
  21. package/MIGRATION_COMPLETE.md +327 -0
  22. package/ORCHESTRATOR_SYNTHESIS.json +80 -0
  23. package/PENDING_WORK.md +308 -0
  24. package/PREFLIGHT_PLAN.md +182 -0
  25. package/QUICKSTART.md +305 -0
  26. package/README.md +15 -0
  27. package/STAGE_1_SETUP_ENGINE.md +245 -0
  28. package/STAGE_2_ARCHITECTURE.md +714 -0
  29. package/STAGE_2_CLI_VERIFICATION.md +269 -0
  30. package/STAGE_2_COMPLETE.md +332 -0
  31. package/STAGE_2_IMPLEMENTATION_PLAN.md +679 -0
  32. package/STAGE_3_COMPLETE.md +246 -0
  33. package/STAGE_3_DASHBOARD_POLISH.md +371 -0
  34. package/STAGE_3_SETUP.md +155 -0
  35. package/VIDEODB_INTEGRATION.md +237 -0
  36. package/archived/DASHBOARD_UI_WALKTHROUGH.md +392 -0
  37. package/archived/FRONTEND_SETUP.md +236 -0
  38. package/archived/auth.ts +40 -0
  39. package/archived/dashboard/components/BusinessImpactCard.tsx +48 -0
  40. package/archived/dashboard/components/CVETable.tsx +104 -0
  41. package/archived/dashboard/components/ErrorBoundary.tsx +48 -0
  42. package/archived/dashboard/components/PatchDiffViewer.tsx +43 -0
  43. package/archived/dashboard/components/RiskGauge.tsx +64 -0
  44. package/archived/dashboard/frontend.tsx +104 -0
  45. package/archived/dashboard/hooks/useAuth.ts +32 -0
  46. package/archived/dashboard/hooks/useScan.ts +65 -0
  47. package/archived/dashboard/index.html +15 -0
  48. package/archived/dashboard/pages/LoginPage.tsx +28 -0
  49. package/archived/dashboard/pages/ScanDetailPage.tsx +143 -0
  50. package/archived/dashboard/pages/ScansListPage.tsx +160 -0
  51. package/bin/install-and-run.sh +91 -0
  52. package/bun.lock +603 -0
  53. package/codeprobe-prd.md +674 -0
  54. package/cve-cache.json +25 -0
  55. package/demo-vulnerable-app/.github/workflows/codeprobe.yml +32 -0
  56. package/demo-vulnerable-app/README.md +70 -0
  57. package/demo-vulnerable-app/package-lock.json +27 -0
  58. package/demo-vulnerable-app/package.json +15 -0
  59. package/demo-vulnerable-app/server.js +34 -0
  60. package/demo.sh +45 -0
  61. package/index.ts +19 -0
  62. package/package.json +28 -0
  63. package/patches.json +12 -0
  64. package/serve-dashboard.ts +23 -0
  65. package/src/api/server-cli.ts +270 -0
  66. package/src/api/server.ts +293 -0
  67. package/src/bot/server.ts +113 -0
  68. package/src/cli/commands/report.ts +92 -0
  69. package/src/cli/commands/scan-with-fix.ts +123 -0
  70. package/src/cli/commands/scan.ts +137 -0
  71. package/src/cli/config.ts +188 -0
  72. package/src/cli/errors.ts +120 -0
  73. package/src/cli/index.ts +137 -0
  74. package/src/cli/progress.ts +119 -0
  75. package/src/cli-server.ts +523 -0
  76. package/src/engine/index.ts +90 -0
  77. package/src/engine/matcher.ts +115 -0
  78. package/src/engine/parser.ts +91 -0
  79. package/src/engine/patcher.ts +280 -0
  80. package/src/engine/report.ts +137 -0
  81. package/src/engine/sandbox.ts +222 -0
  82. package/src/engine/scraper.ts +122 -0
  83. package/src/integrations/videodb.ts +153 -0
  84. package/src/mcp/server.ts +149 -0
  85. package/src/scraper-cron.ts +103 -0
  86. package/src/shared/constants.ts +88 -0
  87. package/src/shared/types.ts +123 -0
  88. package/src/shared/utils.ts +80 -0
  89. package/src/test/cli.test.ts +211 -0
  90. package/src/test/dashboard.test.ts +38 -0
  91. package/src/test/demo-scan.json +32 -0
  92. package/src/test/engine.test.ts +157 -0
  93. package/tailwind.config.js +11 -0
  94. package/tsconfig.json +30 -0
  95. package/verify-dashboard.ts +87 -0
  96. 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
+ }