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,137 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { writeFile, chmod, mkdir } from 'fs/promises';
|
|
3
|
+
import { existsSync } from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { PATHS, EXIT_CODES, FILE_PERMISSIONS } from '../../shared/constants.js';
|
|
6
|
+
import { ProgressLogger, createEventHandler } from '../progress.js';
|
|
7
|
+
import { handleError, CodeProbeError } from '../errors.js';
|
|
8
|
+
import { generateScanId, formatRiskScore, msToHuman } from '../../shared/utils.js';
|
|
9
|
+
import { Report } from '../../shared/types.js';
|
|
10
|
+
import { createEngine } from '../../engine/index.js';
|
|
11
|
+
|
|
12
|
+
interface ScanOptions {
|
|
13
|
+
fix: boolean;
|
|
14
|
+
json: boolean;
|
|
15
|
+
verbose: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function parseArgs(args: string[]): { repoPath: string; options: ScanOptions } {
|
|
19
|
+
const options: ScanOptions = {
|
|
20
|
+
fix: false,
|
|
21
|
+
json: false,
|
|
22
|
+
verbose: false,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
let repoPath = '.';
|
|
26
|
+
|
|
27
|
+
for (let i = 0; i < args.length; i++) {
|
|
28
|
+
const arg = args[i];
|
|
29
|
+
if (arg === '--fix') {
|
|
30
|
+
options.fix = true;
|
|
31
|
+
} else if (arg === '--json') {
|
|
32
|
+
options.json = true;
|
|
33
|
+
} else if (arg === '--verbose') {
|
|
34
|
+
options.verbose = true;
|
|
35
|
+
} else if (!arg.startsWith('--')) {
|
|
36
|
+
repoPath = arg;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return { repoPath, options };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function saveReport(report: Report): Promise<string> {
|
|
44
|
+
// Ensure directory exists
|
|
45
|
+
if (!existsSync(PATHS.SCANS_DIR)) {
|
|
46
|
+
await mkdir(PATHS.SCANS_DIR, { mode: FILE_PERMISSIONS.DIR, recursive: true });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const scanPath = path.join(PATHS.SCANS_DIR, `${report.scan.id}.json`);
|
|
50
|
+
const content = JSON.stringify(report, null, 2);
|
|
51
|
+
|
|
52
|
+
await writeFile(scanPath, content, 'utf-8');
|
|
53
|
+
await chmod(scanPath, FILE_PERMISSIONS.FILE); // Owner read/write only
|
|
54
|
+
|
|
55
|
+
// Also update latest.json (copy, not symlink, for portability)
|
|
56
|
+
const latestPath = path.join(PATHS.SCANS_DIR, 'latest.json');
|
|
57
|
+
await writeFile(latestPath, content, 'utf-8');
|
|
58
|
+
await chmod(latestPath, FILE_PERMISSIONS.FILE);
|
|
59
|
+
|
|
60
|
+
return scanPath;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function displayReport(report: Report, json: boolean, durationMs: number): void {
|
|
64
|
+
if (json) {
|
|
65
|
+
console.log(JSON.stringify({ report, duration_ms: durationMs }, null, 2));
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const logger = new ProgressLogger(false);
|
|
70
|
+
logger.printSeparator();
|
|
71
|
+
|
|
72
|
+
console.log(chalk.bold('SCAN COMPLETE'));
|
|
73
|
+
console.log(`Risk Score: ${formatRiskScore(report.scan.risk_score)}`);
|
|
74
|
+
console.log(
|
|
75
|
+
chalk.cyan(
|
|
76
|
+
`Confirmed Exploitable: ${report.summary.exploitable_count} | ` +
|
|
77
|
+
`Theoretical Risk: ${report.summary.theoretical_count}`
|
|
78
|
+
)
|
|
79
|
+
);
|
|
80
|
+
console.log(`Patches Available: ${report.scan.patches_available}`);
|
|
81
|
+
console.log(`Duration: ${msToHuman(durationMs)}`);
|
|
82
|
+
|
|
83
|
+
if (report.scan.cves.length > 0) {
|
|
84
|
+
console.log(chalk.bold('\nCVE Details:'));
|
|
85
|
+
report.scan.cves.forEach((cve) => {
|
|
86
|
+
const exploitStatus = cve.exploitable
|
|
87
|
+
? chalk.red('✓ CONFIRMED EXPLOITABLE')
|
|
88
|
+
: chalk.yellow('~ Theoretical Risk');
|
|
89
|
+
console.log(
|
|
90
|
+
` ${cve.id}: ${cve.package} ${cve.version_vulnerable} [${cve.severity}] ${exploitStatus}`
|
|
91
|
+
);
|
|
92
|
+
if (cve.patch_version) {
|
|
93
|
+
console.log(` → Patch available: ${cve.patch_version}`);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
logger.printSeparator();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export async function scanCommand(args: string[]): Promise<void> {
|
|
102
|
+
const { repoPath, options } = parseArgs(args);
|
|
103
|
+
const logger = new ProgressLogger(options.verbose);
|
|
104
|
+
|
|
105
|
+
logger.printHeader();
|
|
106
|
+
|
|
107
|
+
const startTime = Date.now();
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
// Initialize engine
|
|
111
|
+
const engine = createEngine();
|
|
112
|
+
|
|
113
|
+
// Run the actual engine scan (not mocked)
|
|
114
|
+
const report = await engine.scan(repoPath);
|
|
115
|
+
|
|
116
|
+
const duration = Date.now() - startTime;
|
|
117
|
+
|
|
118
|
+
// Save report
|
|
119
|
+
const scanPath = await saveReport(report);
|
|
120
|
+
logger.logPhaseComplete('report', `Report saved to ${scanPath}`);
|
|
121
|
+
|
|
122
|
+
// Display results
|
|
123
|
+
displayReport(report, options.json, duration);
|
|
124
|
+
|
|
125
|
+
// If --fix, handle that next
|
|
126
|
+
if (options.fix) {
|
|
127
|
+
const { scanWithFixCommand } = await import('./scan-with-fix.js');
|
|
128
|
+
await scanWithFixCommand([repoPath], report, logger);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Exit with appropriate code
|
|
132
|
+
const hasVulnerabilities = report.scan.cves.length > 0;
|
|
133
|
+
process.exit(hasVulnerabilities ? EXIT_CODES.VULNERABILITIES_FOUND : EXIT_CODES.SUCCESS);
|
|
134
|
+
} catch (error) {
|
|
135
|
+
handleError(error, logger, true);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { existsSync, mkdirSync } from 'fs';
|
|
2
|
+
import { readFile, writeFile, chmod } from 'fs/promises';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { createCipheriv, createDecipheriv, randomBytes, scryptSync } from 'crypto';
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
import { PATHS, FILE_PERMISSIONS } from '../shared/constants.js';
|
|
7
|
+
|
|
8
|
+
interface ConfigData {
|
|
9
|
+
github_token?: string;
|
|
10
|
+
bright_data_api_key?: string;
|
|
11
|
+
daytona_api_key?: string;
|
|
12
|
+
nosana_api_key?: string;
|
|
13
|
+
[key: string]: string | undefined;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Key derivation for AES-256-GCM
|
|
17
|
+
// Recommendation: Use machine ID for cross-session consistency
|
|
18
|
+
// For MVP: Use a fixed salt (in production, store per-machine)
|
|
19
|
+
const ENCRYPTION_SALT = 'codeprobe-mvp-salt-2026';
|
|
20
|
+
const ALGORITHM = 'aes-256-gcm';
|
|
21
|
+
const KEY_LENGTH = 32; // 256 bits
|
|
22
|
+
const IV_LENGTH = 16; // 128 bits for GCM
|
|
23
|
+
const TAG_LENGTH = 16; // GCM tag length
|
|
24
|
+
|
|
25
|
+
function getMachineKey(): Buffer {
|
|
26
|
+
// Derive key from stable machine fingerprint (hostname + platform + arch)
|
|
27
|
+
// Using process.pid breaks cross-session decryption
|
|
28
|
+
const os = require('os');
|
|
29
|
+
const fingerprint = `${os.hostname()}-${process.platform}-${process.arch}`;
|
|
30
|
+
return scryptSync(fingerprint, ENCRYPTION_SALT, KEY_LENGTH);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function encryptToken(token: string): string {
|
|
34
|
+
try {
|
|
35
|
+
const key = getMachineKey();
|
|
36
|
+
const iv = randomBytes(IV_LENGTH);
|
|
37
|
+
const cipher = createCipheriv(ALGORITHM, key, iv);
|
|
38
|
+
|
|
39
|
+
let encrypted = cipher.update(token, 'utf8', 'hex');
|
|
40
|
+
encrypted += cipher.final('hex');
|
|
41
|
+
const tag = cipher.getAuthTag();
|
|
42
|
+
|
|
43
|
+
// Format: iv:tag:encrypted
|
|
44
|
+
return `${iv.toString('hex')}:${tag.toString('hex')}:${encrypted}`;
|
|
45
|
+
} catch (error) {
|
|
46
|
+
console.warn(
|
|
47
|
+
chalk.yellow(
|
|
48
|
+
'⚠️ Token encryption failed. Falling back to plaintext (not secure).\n' +
|
|
49
|
+
'Warning: ~/.codeprobe/config.json contains unencrypted secrets.'
|
|
50
|
+
)
|
|
51
|
+
);
|
|
52
|
+
return token;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function decryptToken(encryptedToken: string): string {
|
|
57
|
+
try {
|
|
58
|
+
// Check if token is already plaintext (fallback case)
|
|
59
|
+
if (!encryptedToken.includes(':')) {
|
|
60
|
+
return encryptedToken;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const [ivHex, tagHex, encrypted] = encryptedToken.split(':');
|
|
64
|
+
const key = getMachineKey();
|
|
65
|
+
const iv = Buffer.from(ivHex, 'hex');
|
|
66
|
+
const tag = Buffer.from(tagHex, 'hex');
|
|
67
|
+
|
|
68
|
+
const decipher = createDecipheriv(ALGORITHM, key, iv);
|
|
69
|
+
decipher.setAuthTag(tag);
|
|
70
|
+
|
|
71
|
+
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
|
|
72
|
+
decrypted += decipher.final('utf8');
|
|
73
|
+
|
|
74
|
+
return decrypted;
|
|
75
|
+
} catch (error) {
|
|
76
|
+
console.warn(chalk.yellow('⚠️ Token decryption failed. Token may be corrupted.'));
|
|
77
|
+
return encryptedToken;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function ensureConfigDir(): Promise<void> {
|
|
82
|
+
if (!existsSync(PATHS.CODEPROBE_DIR)) {
|
|
83
|
+
mkdirSync(PATHS.CODEPROBE_DIR, { mode: FILE_PERMISSIONS.DIR, recursive: true });
|
|
84
|
+
}
|
|
85
|
+
if (!existsSync(PATHS.SCANS_DIR)) {
|
|
86
|
+
mkdirSync(PATHS.SCANS_DIR, { mode: FILE_PERMISSIONS.DIR, recursive: true });
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Ensure permissions are correct
|
|
90
|
+
try {
|
|
91
|
+
await chmod(PATHS.CODEPROBE_DIR, FILE_PERMISSIONS.DIR);
|
|
92
|
+
await chmod(PATHS.SCANS_DIR, FILE_PERMISSIONS.DIR);
|
|
93
|
+
} catch (error) {
|
|
94
|
+
// Ignore permission errors on some filesystems
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function loadConfig(): Promise<ConfigData> {
|
|
99
|
+
try {
|
|
100
|
+
if (!existsSync(PATHS.CONFIG_FILE)) {
|
|
101
|
+
return {};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const content = await readFile(PATHS.CONFIG_FILE, 'utf-8');
|
|
105
|
+
return JSON.parse(content);
|
|
106
|
+
} catch (error) {
|
|
107
|
+
console.warn(chalk.yellow('⚠️ Failed to load config file.'));
|
|
108
|
+
return {};
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function saveConfig(config: ConfigData): Promise<void> {
|
|
113
|
+
await ensureConfigDir();
|
|
114
|
+
|
|
115
|
+
const content = JSON.stringify(config, null, 2);
|
|
116
|
+
await writeFile(PATHS.CONFIG_FILE, content, 'utf-8');
|
|
117
|
+
await chmod(PATHS.CONFIG_FILE, FILE_PERMISSIONS.FILE);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export async function getConfig(key?: string): Promise<ConfigData | string | undefined> {
|
|
121
|
+
const config = await loadConfig();
|
|
122
|
+
|
|
123
|
+
if (key) {
|
|
124
|
+
const value = config[key];
|
|
125
|
+
if (!value) return undefined;
|
|
126
|
+
|
|
127
|
+
// Decrypt if it looks like an encrypted token
|
|
128
|
+
if (typeof value === 'string' && value.includes(':')) {
|
|
129
|
+
return decryptToken(value);
|
|
130
|
+
}
|
|
131
|
+
return value;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return config;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export async function setConfig(key: string, value: string): Promise<void> {
|
|
138
|
+
const config = await loadConfig();
|
|
139
|
+
|
|
140
|
+
// Encrypt if it's a token/secret field (specific fields only)
|
|
141
|
+
const secretFields = ['github_token', 'bright_data_api_key', 'daytona_api_key', 'nosana_api_key'];
|
|
142
|
+
if (secretFields.includes(key.toLowerCase())) {
|
|
143
|
+
config[key] = encryptToken(value);
|
|
144
|
+
} else {
|
|
145
|
+
config[key] = value;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
await saveConfig(config);
|
|
149
|
+
console.log(chalk.green(`✓ Config saved: ${key}`));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export async function clearConfig(key: string): Promise<void> {
|
|
153
|
+
const config = await loadConfig();
|
|
154
|
+
delete config[key];
|
|
155
|
+
await saveConfig(config);
|
|
156
|
+
console.log(chalk.green(`✓ Config cleared: ${key}`));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export async function getApiKey(service: 'BRIGHT_DATA' | 'DAYTONA' | 'NOSANA'): Promise<string> {
|
|
160
|
+
const envKey = process.env[`${service}_API_KEY`];
|
|
161
|
+
if (envKey) {
|
|
162
|
+
return envKey;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const configKey = await getConfig(service.toLowerCase() + '_api_key');
|
|
166
|
+
if (configKey && typeof configKey === 'string') {
|
|
167
|
+
return configKey;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
throw new Error(
|
|
171
|
+
`No ${service} API key found.\n` +
|
|
172
|
+
`Set environment variable: export ${service}_API_KEY=<key>\n` +
|
|
173
|
+
`Or run: codeprobe config set ${service.toLowerCase()}_api_key <key>`
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export async function getGitHubToken(): Promise<string> {
|
|
178
|
+
const token = await getConfig('github_token');
|
|
179
|
+
if (token && typeof token === 'string') {
|
|
180
|
+
return token;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
throw new Error(
|
|
184
|
+
'No GitHub token found.\n' +
|
|
185
|
+
'Set: codeprobe config set github_token <token>\n' +
|
|
186
|
+
'Or: export GITHUB_TOKEN=<token>'
|
|
187
|
+
);
|
|
188
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { ProgressLogger } from './progress.js';
|
|
3
|
+
|
|
4
|
+
export class CodeProbeError extends Error {
|
|
5
|
+
constructor(
|
|
6
|
+
public code: string,
|
|
7
|
+
public message: string,
|
|
8
|
+
public details?: string
|
|
9
|
+
) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.name = 'CodeProbeError';
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class BrightDataError extends CodeProbeError {
|
|
16
|
+
constructor(details?: string) {
|
|
17
|
+
super('BRIGHT_DATA_FAILED', 'CVE scraping failed', details);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class DaytonaError extends CodeProbeError {
|
|
22
|
+
constructor(details?: string) {
|
|
23
|
+
super('DAYTONA_FAILED', 'Sandbox operation failed', details);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export class NetworkError extends CodeProbeError {
|
|
28
|
+
constructor(details?: string) {
|
|
29
|
+
super('NETWORK_ERROR', 'Network operation failed', details);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export class GitError extends CodeProbeError {
|
|
34
|
+
constructor(details?: string) {
|
|
35
|
+
super('GIT_ERROR', 'Git operation failed', details);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export class ConfigError extends CodeProbeError {
|
|
40
|
+
constructor(details?: string) {
|
|
41
|
+
super('CONFIG_ERROR', 'Configuration error', details);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function handleError(error: unknown, logger: ProgressLogger, exitOnError = true): void {
|
|
46
|
+
console.log('');
|
|
47
|
+
logger.printSeparator();
|
|
48
|
+
|
|
49
|
+
if (error instanceof CodeProbeError) {
|
|
50
|
+
logger.logError(error.message, error.details);
|
|
51
|
+
|
|
52
|
+
// Provide guidance for specific errors
|
|
53
|
+
if (error.code === 'BRIGHT_DATA_FAILED') {
|
|
54
|
+
console.log(chalk.yellow('→ Using cached CVE data (may be outdated)'));
|
|
55
|
+
console.log(chalk.cyan('→ Run: codeprobe config set bright_data_api_key <key>'));
|
|
56
|
+
} else if (error.code === 'GIT_ERROR') {
|
|
57
|
+
console.log(chalk.yellow('→ Ensure git repository is clean'));
|
|
58
|
+
console.log(chalk.cyan('→ Run: git status && git commit'));
|
|
59
|
+
} else if (error.code === 'CONFIG_ERROR') {
|
|
60
|
+
console.log(chalk.yellow('→ Run: codeprobe config set <key> <value>'));
|
|
61
|
+
}
|
|
62
|
+
} else if (error instanceof Error) {
|
|
63
|
+
logger.logError(error.message, error.stack);
|
|
64
|
+
} else {
|
|
65
|
+
logger.logError('Unknown error occurred');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
logger.printSeparator();
|
|
69
|
+
console.log('');
|
|
70
|
+
|
|
71
|
+
if (exitOnError) {
|
|
72
|
+
process.exit(2); // Exit code 2 = scan failed
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function wrapWithTimeout<T>(
|
|
77
|
+
promise: Promise<T>,
|
|
78
|
+
timeoutMs: number,
|
|
79
|
+
errorMessage: string
|
|
80
|
+
): Promise<T> {
|
|
81
|
+
return Promise.race([
|
|
82
|
+
promise,
|
|
83
|
+
new Promise<T>((_, reject) =>
|
|
84
|
+
setTimeout(() => reject(new Error(`${errorMessage} (timeout: ${timeoutMs}ms)`)), timeoutMs)
|
|
85
|
+
),
|
|
86
|
+
]);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export async function retryWithBackoff<T>(
|
|
90
|
+
fn: () => Promise<T>,
|
|
91
|
+
maxRetries = 2,
|
|
92
|
+
backoffMs = 1000
|
|
93
|
+
): Promise<T> {
|
|
94
|
+
let lastError: Error | undefined;
|
|
95
|
+
|
|
96
|
+
for (let i = 0; i <= maxRetries; i++) {
|
|
97
|
+
try {
|
|
98
|
+
return await fn();
|
|
99
|
+
} catch (error) {
|
|
100
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
101
|
+
if (i < maxRetries) {
|
|
102
|
+
await new Promise((resolve) => setTimeout(resolve, backoffMs * (i + 1)));
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
throw lastError;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function formatError(error: unknown): string {
|
|
111
|
+
if (error instanceof Error) {
|
|
112
|
+
return error.message;
|
|
113
|
+
}
|
|
114
|
+
return String(error);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function logFallback(message: string, reason: string): void {
|
|
118
|
+
console.log(chalk.yellow(`→ ${message}`));
|
|
119
|
+
console.log(chalk.gray(` Reason: ${reason}`));
|
|
120
|
+
}
|
package/src/cli/index.ts
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { APP_NAME, APP_VERSION, EXIT_CODES } from '../shared/constants.js';
|
|
5
|
+
import { ProgressLogger } from './progress.js';
|
|
6
|
+
import { handleError } from './errors.js';
|
|
7
|
+
import { scanCommand } from './commands/scan.js';
|
|
8
|
+
import { reportCommand } from './commands/report.js';
|
|
9
|
+
|
|
10
|
+
const logger = new ProgressLogger();
|
|
11
|
+
|
|
12
|
+
function showHelp(): void {
|
|
13
|
+
console.log(`
|
|
14
|
+
${chalk.bold.cyan(`⚡ ${APP_NAME} v${APP_VERSION}`)}
|
|
15
|
+
|
|
16
|
+
${chalk.bold('USAGE')}
|
|
17
|
+
codeprobe <command> [options] [arguments]
|
|
18
|
+
|
|
19
|
+
${chalk.bold('COMMANDS')}
|
|
20
|
+
scan [path] Scan a repository for vulnerabilities (default: current dir)
|
|
21
|
+
report Display last scan results
|
|
22
|
+
config Manage configuration
|
|
23
|
+
help Show this help message
|
|
24
|
+
|
|
25
|
+
${chalk.bold('OPTIONS')}
|
|
26
|
+
--fix Auto-fix vulnerabilities (creates git branch & commits)
|
|
27
|
+
--json Output results as JSON
|
|
28
|
+
--verbose Show detailed logs
|
|
29
|
+
--help Show help
|
|
30
|
+
|
|
31
|
+
${chalk.bold('EXAMPLES')}
|
|
32
|
+
codeprobe scan
|
|
33
|
+
codeprobe scan ./my-app
|
|
34
|
+
codeprobe scan --fix
|
|
35
|
+
codeprobe scan --json > report.json
|
|
36
|
+
codeprobe report
|
|
37
|
+
codeprobe config set bright_data_api_key <key>
|
|
38
|
+
|
|
39
|
+
${chalk.bold('DOCS')}
|
|
40
|
+
https://github.com/codeprobe/codeprobe
|
|
41
|
+
`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function main(): Promise<void> {
|
|
45
|
+
const args = process.argv.slice(2);
|
|
46
|
+
|
|
47
|
+
// No args = show help
|
|
48
|
+
if (args.length === 0) {
|
|
49
|
+
showHelp();
|
|
50
|
+
process.exit(EXIT_CODES.SUCCESS);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const command = args[0];
|
|
54
|
+
const restArgs = args.slice(1);
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
switch (command) {
|
|
58
|
+
case 'scan':
|
|
59
|
+
await scanCommand(restArgs);
|
|
60
|
+
break;
|
|
61
|
+
|
|
62
|
+
case 'report':
|
|
63
|
+
await reportCommand(restArgs);
|
|
64
|
+
break;
|
|
65
|
+
|
|
66
|
+
case 'config':
|
|
67
|
+
await handleConfigCommand(restArgs);
|
|
68
|
+
break;
|
|
69
|
+
|
|
70
|
+
case '--help':
|
|
71
|
+
case '-h':
|
|
72
|
+
case 'help':
|
|
73
|
+
showHelp();
|
|
74
|
+
break;
|
|
75
|
+
|
|
76
|
+
default:
|
|
77
|
+
console.error(chalk.red(`Unknown command: ${command}`));
|
|
78
|
+
console.log(`Run ${chalk.cyan('codeprobe --help')} for usage`);
|
|
79
|
+
process.exit(EXIT_CODES.SCAN_FAILED);
|
|
80
|
+
}
|
|
81
|
+
} catch (error) {
|
|
82
|
+
handleError(error, logger, true);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function handleConfigCommand(args: string[]): Promise<void> {
|
|
87
|
+
const { getConfig, setConfig, clearConfig } = await import('./config.js');
|
|
88
|
+
|
|
89
|
+
const subcommand = args[0];
|
|
90
|
+
|
|
91
|
+
switch (subcommand) {
|
|
92
|
+
case 'get':
|
|
93
|
+
{
|
|
94
|
+
const key = args[1];
|
|
95
|
+
if (!key) {
|
|
96
|
+
const config = await getConfig();
|
|
97
|
+
console.log(JSON.stringify(config, null, 2));
|
|
98
|
+
} else {
|
|
99
|
+
const value = await getConfig(key);
|
|
100
|
+
console.log(value || `${key} not set`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
break;
|
|
104
|
+
|
|
105
|
+
case 'set':
|
|
106
|
+
{
|
|
107
|
+
const key = args[1];
|
|
108
|
+
const value = args[2];
|
|
109
|
+
if (!key || !value) {
|
|
110
|
+
console.error(chalk.red('Usage: codeprobe config set <key> <value>'));
|
|
111
|
+
process.exit(EXIT_CODES.SCAN_FAILED);
|
|
112
|
+
}
|
|
113
|
+
await setConfig(key, value);
|
|
114
|
+
}
|
|
115
|
+
break;
|
|
116
|
+
|
|
117
|
+
case 'clear':
|
|
118
|
+
{
|
|
119
|
+
const key = args[1];
|
|
120
|
+
if (!key) {
|
|
121
|
+
console.error(chalk.red('Usage: codeprobe config clear <key>'));
|
|
122
|
+
process.exit(EXIT_CODES.SCAN_FAILED);
|
|
123
|
+
}
|
|
124
|
+
await clearConfig(key);
|
|
125
|
+
}
|
|
126
|
+
break;
|
|
127
|
+
|
|
128
|
+
default:
|
|
129
|
+
console.error(chalk.red(`Unknown config subcommand: ${subcommand}`));
|
|
130
|
+
console.log(`Usage: codeprobe config [get|set|clear]`);
|
|
131
|
+
process.exit(EXIT_CODES.SCAN_FAILED);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
main().catch((error) => {
|
|
136
|
+
handleError(error, logger, true);
|
|
137
|
+
});
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import dayjs from 'dayjs';
|
|
3
|
+
import { ScanEvent } from '../shared/types.js';
|
|
4
|
+
|
|
5
|
+
export class ProgressLogger {
|
|
6
|
+
private startTime: number = Date.now();
|
|
7
|
+
private lastPhase: string = '';
|
|
8
|
+
|
|
9
|
+
constructor(private verbose: boolean = false) {}
|
|
10
|
+
|
|
11
|
+
log(event: ScanEvent): void {
|
|
12
|
+
if (!this.verbose && event.level === 'info') {
|
|
13
|
+
// Only show progress/complete/error in non-verbose mode
|
|
14
|
+
if (event.status === 'progress' || event.status === 'start') {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const timestamp = dayjs().format('HH:mm:ss');
|
|
20
|
+
const prefix = `[${timestamp}]`;
|
|
21
|
+
|
|
22
|
+
let output = '';
|
|
23
|
+
const icon = this.getIcon(event.level, event.status);
|
|
24
|
+
|
|
25
|
+
switch (event.level) {
|
|
26
|
+
case 'success':
|
|
27
|
+
output = chalk.green(`${prefix} ${icon} ${event.message}`);
|
|
28
|
+
break;
|
|
29
|
+
case 'warn':
|
|
30
|
+
output = chalk.yellow(`${prefix} ${icon} ${event.message}`);
|
|
31
|
+
break;
|
|
32
|
+
case 'error':
|
|
33
|
+
output = chalk.red(`${prefix} ${icon} ${event.message}`);
|
|
34
|
+
break;
|
|
35
|
+
default:
|
|
36
|
+
output = chalk.cyan(`${prefix} ${icon} ${event.message}`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (event.metadata && this.verbose) {
|
|
40
|
+
output += chalk.gray(` ${JSON.stringify(event.metadata)}`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
console.log(output);
|
|
44
|
+
this.lastPhase = event.phase;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
private getIcon(level: string, status: string): string {
|
|
48
|
+
switch (level) {
|
|
49
|
+
case 'success':
|
|
50
|
+
return '✓';
|
|
51
|
+
case 'error':
|
|
52
|
+
return '❌';
|
|
53
|
+
case 'warn':
|
|
54
|
+
return '⚠️';
|
|
55
|
+
default:
|
|
56
|
+
if (status === 'start') return '▶️';
|
|
57
|
+
if (status === 'complete') return '✓';
|
|
58
|
+
return '•';
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
logPhaseStart(phase: string, message: string): void {
|
|
63
|
+
const timestamp = dayjs().format('HH:mm:ss');
|
|
64
|
+
console.log(chalk.cyan(`[${timestamp}] ▶️ ${message}...`));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
logPhaseComplete(phase: string, message: string): void {
|
|
68
|
+
const timestamp = dayjs().format('HH:mm:ss');
|
|
69
|
+
console.log(chalk.green(`[${timestamp}] ✓ ${message}`));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
logError(message: string, details?: string): void {
|
|
73
|
+
const timestamp = dayjs().format('HH:mm:ss');
|
|
74
|
+
console.log(chalk.red(`[${timestamp}] ❌ ${message}`));
|
|
75
|
+
if (details) {
|
|
76
|
+
console.log(chalk.gray(` ${details}`));
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
logWarning(message: string, details?: string): void {
|
|
81
|
+
const timestamp = dayjs().format('HH:mm:ss');
|
|
82
|
+
console.log(chalk.yellow(`[${timestamp}] ⚠️ ${message}`));
|
|
83
|
+
if (details) {
|
|
84
|
+
console.log(chalk.gray(` ${details}`));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
printSeparator(): void {
|
|
89
|
+
console.log(chalk.gray('────────────────────────────────────────────────'));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
printHeader(): void {
|
|
93
|
+
console.log(chalk.bold.cyan('⚡ CodeProbe v1.0.0'));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
printElapsedTime(): void {
|
|
97
|
+
const elapsed = Date.now() - this.startTime;
|
|
98
|
+
const ms = elapsed % 1000;
|
|
99
|
+
const s = Math.floor(elapsed / 1000) % 60;
|
|
100
|
+
const m = Math.floor(elapsed / 60000);
|
|
101
|
+
|
|
102
|
+
const timeStr =
|
|
103
|
+
m > 0
|
|
104
|
+
? `${m}m ${s}s`
|
|
105
|
+
: s > 0
|
|
106
|
+
? `${s}s`
|
|
107
|
+
: `${ms}ms`;
|
|
108
|
+
|
|
109
|
+
console.log(chalk.gray(`⏱️ Completed in ${timeStr}`));
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function createEventHandler(verbose: boolean = false) {
|
|
114
|
+
const logger = new ProgressLogger(verbose);
|
|
115
|
+
|
|
116
|
+
return (event: ScanEvent) => {
|
|
117
|
+
logger.log(event);
|
|
118
|
+
};
|
|
119
|
+
}
|