codeprobe-scanner 1.0.15 → 1.0.16
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/package.json +1 -1
- package/src/cli/commands/scan-with-fix.ts +13 -3
- package/src/engine/index.ts +9 -0
- package/src/engine/report.ts +2 -1
- package/src/engine/sast.ts +274 -0
- package/src/mcp/server.ts +2 -1
- package/src/shared/utils.ts +2 -1
package/package.json
CHANGED
|
@@ -8,6 +8,10 @@ import { ProgressLogger } from '../progress.js';
|
|
|
8
8
|
import { GitError, handleError } from '../errors.js';
|
|
9
9
|
import { EXIT_CODES } from '../../shared/constants.js';
|
|
10
10
|
|
|
11
|
+
function escapeShellArg(arg: string): string {
|
|
12
|
+
return `'${arg.replace(/'/g, "'\\''")}'`;
|
|
13
|
+
}
|
|
14
|
+
|
|
11
15
|
function checkGitRepo(): boolean {
|
|
12
16
|
try {
|
|
13
17
|
execSync('git rev-parse --git-dir', { encoding: 'utf-8', stdio: 'pipe' });
|
|
@@ -28,7 +32,7 @@ function isGitDirty(): boolean {
|
|
|
28
32
|
|
|
29
33
|
function createBranch(name: string): void {
|
|
30
34
|
try {
|
|
31
|
-
execSync(`git checkout -b ${name}`, { encoding: 'utf-8', stdio: 'pipe' });
|
|
35
|
+
execSync(`git checkout -b ${escapeShellArg(name)}`, { encoding: 'utf-8', stdio: 'pipe' });
|
|
32
36
|
} catch {
|
|
33
37
|
throw new GitError(`Failed to create branch: ${name}`);
|
|
34
38
|
}
|
|
@@ -36,9 +40,15 @@ function createBranch(name: string): void {
|
|
|
36
40
|
|
|
37
41
|
function commitAndPush(message: string, branchName: string): void {
|
|
38
42
|
execSync('git add package.json', { encoding: 'utf-8', stdio: 'pipe' });
|
|
39
|
-
execSync(`git -c commit.gpgsign=false commit -m
|
|
43
|
+
execSync(`git -c commit.gpgsign=false commit -m ${escapeShellArg(message)}`, {
|
|
44
|
+
encoding: 'utf-8',
|
|
45
|
+
stdio: 'pipe',
|
|
46
|
+
});
|
|
40
47
|
try {
|
|
41
|
-
execSync(`git push -u origin ${branchName}`, {
|
|
48
|
+
execSync(`git push -u origin ${escapeShellArg(branchName)}`, {
|
|
49
|
+
encoding: 'utf-8',
|
|
50
|
+
stdio: 'pipe',
|
|
51
|
+
});
|
|
42
52
|
} catch {
|
|
43
53
|
// Push failed — not fatal, user can push manually
|
|
44
54
|
}
|
package/src/engine/index.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { createPatcher } from "./patcher";
|
|
|
6
6
|
import { createReportBuilder } from "./report";
|
|
7
7
|
import { Report, ScanCVE } from "../shared/types";
|
|
8
8
|
import { findAllPackageJsons } from "../shared/file-scanner";
|
|
9
|
+
import { createSASTScanner, CodeVulnerability } from "./sast";
|
|
9
10
|
|
|
10
11
|
export class CodeProbeEngine {
|
|
11
12
|
private parser = createParser();
|
|
@@ -14,11 +15,19 @@ export class CodeProbeEngine {
|
|
|
14
15
|
private matcher = createMatcher();
|
|
15
16
|
private patcher = createPatcher();
|
|
16
17
|
private reportBuilder = createReportBuilder();
|
|
18
|
+
private sastScanner = createSASTScanner();
|
|
17
19
|
|
|
18
20
|
getVideoRecorder() {
|
|
19
21
|
return this.sandbox.getVideoRecorder();
|
|
20
22
|
}
|
|
21
23
|
|
|
24
|
+
async scanCodeVulnerabilities(rootPath: string): Promise<CodeVulnerability[]> {
|
|
25
|
+
console.log("🔐 Scanning source code for vulnerabilities...");
|
|
26
|
+
const vulnerabilities = await this.sastScanner.scanRepository(rootPath);
|
|
27
|
+
console.log(` Found ${vulnerabilities.length} potential vulnerabilities`);
|
|
28
|
+
return vulnerabilities;
|
|
29
|
+
}
|
|
30
|
+
|
|
22
31
|
async scanRecursive(rootPath: string): Promise<Report> {
|
|
23
32
|
const startTime = Date.now();
|
|
24
33
|
|
package/src/engine/report.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Scan, ScanCVE, Report } from "../shared/types";
|
|
2
2
|
import { PATHS } from "../shared/constants";
|
|
3
3
|
import dayjs from "dayjs";
|
|
4
|
+
import { randomBytes } from "crypto";
|
|
4
5
|
|
|
5
6
|
export class ReportBuilder {
|
|
6
7
|
async buildReport(
|
|
@@ -66,7 +67,7 @@ export class ReportBuilder {
|
|
|
66
67
|
}
|
|
67
68
|
|
|
68
69
|
private generateScanId(): string {
|
|
69
|
-
return `scan_${Date.now()}_${
|
|
70
|
+
return `scan_${Date.now()}_${randomBytes(6).toString('hex')}`;
|
|
70
71
|
}
|
|
71
72
|
|
|
72
73
|
formatForTerminal(report: Report): string {
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import { readdir, readFile } from "fs/promises";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
|
|
4
|
+
export interface CodeVulnerability {
|
|
5
|
+
id: string;
|
|
6
|
+
file: string;
|
|
7
|
+
line: number;
|
|
8
|
+
severity: "CRITICAL" | "HIGH" | "MEDIUM" | "LOW";
|
|
9
|
+
type: string;
|
|
10
|
+
description: string;
|
|
11
|
+
code: string;
|
|
12
|
+
suggestion: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class SASTScanner {
|
|
16
|
+
private vulnerabilities: CodeVulnerability[] = [];
|
|
17
|
+
|
|
18
|
+
async scanRepository(rootPath: string): Promise<CodeVulnerability[]> {
|
|
19
|
+
this.vulnerabilities = [];
|
|
20
|
+
await this.scanDirectory(rootPath);
|
|
21
|
+
return this.vulnerabilities;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
private async scanDirectory(dirPath: string, depth: number = 0): Promise<void> {
|
|
25
|
+
if (depth > 5) return; // Prevent deep recursion
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
const entries = await readdir(dirPath, { withFileTypes: true });
|
|
29
|
+
|
|
30
|
+
for (const entry of entries) {
|
|
31
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
32
|
+
|
|
33
|
+
const fullPath = join(dirPath, entry.name);
|
|
34
|
+
|
|
35
|
+
if (entry.isFile() && (entry.name.endsWith(".ts") || entry.name.endsWith(".js"))) {
|
|
36
|
+
await this.scanFile(fullPath);
|
|
37
|
+
} else if (entry.isDirectory()) {
|
|
38
|
+
await this.scanDirectory(fullPath, depth + 1);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
} catch {
|
|
42
|
+
// Permission denied or other errors
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
private async scanFile(filePath: string): Promise<void> {
|
|
47
|
+
try {
|
|
48
|
+
const content = await readFile(filePath, "utf-8");
|
|
49
|
+
const lines = content.split("\n");
|
|
50
|
+
|
|
51
|
+
lines.forEach((line, index) => {
|
|
52
|
+
this.checkForHardcodedSecrets(line, filePath, index + 1);
|
|
53
|
+
this.checkForSQLInjection(line, filePath, index + 1);
|
|
54
|
+
this.checkForCommandInjection(line, filePath, index + 1);
|
|
55
|
+
this.checkForInsecureEval(line, filePath, index + 1);
|
|
56
|
+
this.checkForXSSVulnerabilities(line, filePath, index + 1);
|
|
57
|
+
this.checkForPathTraversal(line, filePath, index + 1);
|
|
58
|
+
this.checkForInsecureRandom(line, filePath, index + 1);
|
|
59
|
+
});
|
|
60
|
+
} catch {
|
|
61
|
+
// File read error
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
private checkForHardcodedSecrets(line: string, file: string, lineNum: number): void {
|
|
66
|
+
const secretPatterns = [
|
|
67
|
+
/(?:api_?key|apiKey|api_secret|secret)\s*[:=]\s*["'][^"']*["']/i,
|
|
68
|
+
/(?:password|passwd)\s*[:=]\s*["'][^"']*["']/i,
|
|
69
|
+
/(?:token|auth|bearer)\s*[:=]\s*["'][A-Za-z0-9_-]{20,}["']/i,
|
|
70
|
+
/(?:private_key|privateKey)\s*[:=]/i,
|
|
71
|
+
/(?:database_url|db_url|dburl)\s*[:=]\s*["'].*?(?:password|pwd)=/i,
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
for (const pattern of secretPatterns) {
|
|
75
|
+
if (pattern.test(line)) {
|
|
76
|
+
this.addVulnerability({
|
|
77
|
+
id: `SECRET-${file}-${lineNum}`,
|
|
78
|
+
file,
|
|
79
|
+
line: lineNum,
|
|
80
|
+
severity: "CRITICAL",
|
|
81
|
+
type: "Hardcoded Secret",
|
|
82
|
+
description: "Hardcoded credentials or secrets detected in source code",
|
|
83
|
+
code: line.trim(),
|
|
84
|
+
suggestion:
|
|
85
|
+
"Move secrets to environment variables or use a secrets management tool like dotenv",
|
|
86
|
+
});
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
private checkForSQLInjection(line: string, file: string, lineNum: number): void {
|
|
93
|
+
const sqlPatterns = [
|
|
94
|
+
/query\s*\(\s*[`'"]\s*SELECT.*?\$\{/i,
|
|
95
|
+
/execute\s*\(\s*[`'"]\s*SELECT.*?\$\{/i,
|
|
96
|
+
/sql\s*`\s*SELECT.*?\$\{/i,
|
|
97
|
+
/db\s*\.\s*query\s*\(\s*[`'"][^`'"]*\+/i,
|
|
98
|
+
];
|
|
99
|
+
|
|
100
|
+
for (const pattern of sqlPatterns) {
|
|
101
|
+
if (pattern.test(line)) {
|
|
102
|
+
this.addVulnerability({
|
|
103
|
+
id: `SQL-${file}-${lineNum}`,
|
|
104
|
+
file,
|
|
105
|
+
line: lineNum,
|
|
106
|
+
severity: "CRITICAL",
|
|
107
|
+
type: "SQL Injection",
|
|
108
|
+
description: "Potential SQL injection vulnerability detected",
|
|
109
|
+
code: line.trim(),
|
|
110
|
+
suggestion:
|
|
111
|
+
"Use parameterized queries or prepared statements instead of string concatenation",
|
|
112
|
+
});
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
private checkForCommandInjection(line: string, file: string, lineNum: number): void {
|
|
119
|
+
// Skip if using proper escaping functions
|
|
120
|
+
if (line.includes('escapeShellArg') || line.includes('quote') || line.includes('shellEscape')) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const cmdPatterns = [
|
|
125
|
+
/exec\s*\(\s*[`'"]\s*[^`'"]*\$\{[^}]*\}/i,
|
|
126
|
+
/execSync\s*\(\s*[`'"]\s*[^`'"]*\$\{[^}]*\}/i,
|
|
127
|
+
/spawn\s*\(\s*[`'"]\s*[^`'"]*\+/i,
|
|
128
|
+
/shell:\s*true/,
|
|
129
|
+
];
|
|
130
|
+
|
|
131
|
+
for (const pattern of cmdPatterns) {
|
|
132
|
+
if (pattern.test(line)) {
|
|
133
|
+
this.addVulnerability({
|
|
134
|
+
id: `CMD-${file}-${lineNum}`,
|
|
135
|
+
file,
|
|
136
|
+
line: lineNum,
|
|
137
|
+
severity: "CRITICAL",
|
|
138
|
+
type: "Command Injection",
|
|
139
|
+
description: "Potential command injection vulnerability detected",
|
|
140
|
+
code: line.trim(),
|
|
141
|
+
suggestion:
|
|
142
|
+
"Avoid using shell=true and never pass user input to exec/spawn without sanitization",
|
|
143
|
+
});
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private checkForInsecureEval(line: string, file: string, lineNum: number): void {
|
|
150
|
+
// Skip if this is a comment or string literal describing the vulnerability
|
|
151
|
+
if (line.trim().startsWith("//") || line.trim().startsWith("*") || line.includes('description:') || line.includes('suggestion:')) {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const evalPatterns = [
|
|
156
|
+
/\beval\s*\(/,
|
|
157
|
+
/new\s+Function\s*\(/,
|
|
158
|
+
/setTimeout\s*\(\s*["'`].*?["'`]/,
|
|
159
|
+
/setInterval\s*\(\s*["'`].*?["'`]/,
|
|
160
|
+
];
|
|
161
|
+
|
|
162
|
+
for (const pattern of evalPatterns) {
|
|
163
|
+
if (pattern.test(line)) {
|
|
164
|
+
this.addVulnerability({
|
|
165
|
+
id: `EVAL-${file}-${lineNum}`,
|
|
166
|
+
file,
|
|
167
|
+
line: lineNum,
|
|
168
|
+
severity: "HIGH",
|
|
169
|
+
type: "Insecure Eval",
|
|
170
|
+
description: "Using eval() or Function() constructor with untrusted input is dangerous",
|
|
171
|
+
code: line.trim(),
|
|
172
|
+
suggestion: "Avoid eval() and Function() constructor. Use safer alternatives like JSON.parse()",
|
|
173
|
+
});
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
private checkForXSSVulnerabilities(line: string, file: string, lineNum: number): void {
|
|
180
|
+
// Skip if this is a comment or string literal describing the vulnerability
|
|
181
|
+
if (line.trim().startsWith("//") || line.trim().startsWith("*") || line.includes('description:') || line.includes('suggestion:')) {
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const xssPatterns = [
|
|
186
|
+
/innerHTML\s*=\s*[^`'"]*\$\{/i,
|
|
187
|
+
/innerHTML\s*=\s*[^`'"]*\+/i,
|
|
188
|
+
/dangerouslySetInnerHTML\s*=\s*[^}]*}/,
|
|
189
|
+
];
|
|
190
|
+
|
|
191
|
+
for (const pattern of xssPatterns) {
|
|
192
|
+
if (pattern.test(line)) {
|
|
193
|
+
this.addVulnerability({
|
|
194
|
+
id: `XSS-${file}-${lineNum}`,
|
|
195
|
+
file,
|
|
196
|
+
line: lineNum,
|
|
197
|
+
severity: "HIGH",
|
|
198
|
+
type: "Cross-Site Scripting (XSS)",
|
|
199
|
+
description: "Potential XSS vulnerability through innerHTML or dangerouslySetInnerHTML",
|
|
200
|
+
code: line.trim(),
|
|
201
|
+
suggestion:
|
|
202
|
+
"Use textContent instead of innerHTML, or sanitize user input before rendering. For React, use a library like DOMPurify",
|
|
203
|
+
});
|
|
204
|
+
break;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
private checkForPathTraversal(line: string, file: string, lineNum: number): void {
|
|
210
|
+
const pathPatterns = [
|
|
211
|
+
/fs\.readFile\s*\(\s*[^,]*req\./i,
|
|
212
|
+
/path\.join\s*\(\s*[^,]*req\./i,
|
|
213
|
+
/fs\.readFileSync\s*\(\s*[^,]*req\./i,
|
|
214
|
+
/readFile\s*\(\s*\.\.\//,
|
|
215
|
+
];
|
|
216
|
+
|
|
217
|
+
for (const pattern of pathPatterns) {
|
|
218
|
+
if (pattern.test(line)) {
|
|
219
|
+
this.addVulnerability({
|
|
220
|
+
id: `PATH-${file}-${lineNum}`,
|
|
221
|
+
file,
|
|
222
|
+
line: lineNum,
|
|
223
|
+
severity: "HIGH",
|
|
224
|
+
type: "Path Traversal",
|
|
225
|
+
description: "Potential path traversal vulnerability detected",
|
|
226
|
+
code: line.trim(),
|
|
227
|
+
suggestion:
|
|
228
|
+
"Validate and sanitize file paths. Use path.normalize() and ensure the path is within the allowed directory",
|
|
229
|
+
});
|
|
230
|
+
break;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
private checkForInsecureRandom(line: string, file: string, lineNum: number): void {
|
|
236
|
+
// Skip if this is for timing delays (non-security critical)
|
|
237
|
+
if (
|
|
238
|
+
/[Tt]ime/.test(line) ||
|
|
239
|
+
line.includes('delay') ||
|
|
240
|
+
line.includes('timeout') ||
|
|
241
|
+
line.includes('* 1000') ||
|
|
242
|
+
line.includes('* 2000')
|
|
243
|
+
) {
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Skip if this is a comment or string literal
|
|
248
|
+
if (line.trim().startsWith("//") || line.trim().startsWith("*") || line.includes('description:') || line.includes('suggestion:')) {
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (/Math\.random\s*\(\)/.test(line)) {
|
|
253
|
+
this.addVulnerability({
|
|
254
|
+
id: `RAND-${file}-${lineNum}`,
|
|
255
|
+
file,
|
|
256
|
+
line: lineNum,
|
|
257
|
+
severity: "MEDIUM",
|
|
258
|
+
type: "Insecure Random",
|
|
259
|
+
description: "Math.random() is not cryptographically secure",
|
|
260
|
+
code: line.trim(),
|
|
261
|
+
suggestion:
|
|
262
|
+
"Use crypto.getRandomBytes() or crypto.randomUUID() for security-sensitive operations",
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
private addVulnerability(vuln: CodeVulnerability): void {
|
|
268
|
+
if (!this.vulnerabilities.find((v) => v.id === vuln.id)) {
|
|
269
|
+
this.vulnerabilities.push(vuln);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export const createSASTScanner = () => new SASTScanner();
|
package/src/mcp/server.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { createEngine } from "../engine/index.js";
|
|
|
2
2
|
import { PATHS } from "../shared/constants.js";
|
|
3
3
|
import { readFile } from "fs/promises";
|
|
4
4
|
import path from "path";
|
|
5
|
+
import { randomBytes } from "crypto";
|
|
5
6
|
|
|
6
7
|
interface MCPRequest {
|
|
7
8
|
jsonrpc: string;
|
|
@@ -25,7 +26,7 @@ async function handleToolCall(method: string, params?: Record<string, unknown>):
|
|
|
25
26
|
switch (method) {
|
|
26
27
|
case "scan_repository": {
|
|
27
28
|
const repoUrl = params?.repo_url as string;
|
|
28
|
-
const scanId = `scan-${Date.now()}-${
|
|
29
|
+
const scanId = `scan-${Date.now()}-${randomBytes(4).toString('hex')}`;
|
|
29
30
|
|
|
30
31
|
activeScan.set(scanId, { status: "running", progress: 0 });
|
|
31
32
|
|
package/src/shared/utils.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
+
import { randomBytes } from 'crypto';
|
|
2
3
|
|
|
3
4
|
export function formatRiskScore(score: number): string {
|
|
4
5
|
const level = getRiskLevel(score);
|
|
@@ -43,7 +44,7 @@ export function formatExploitable(exploitable: boolean): string {
|
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
export function generateScanId(): string {
|
|
46
|
-
return `scan_${Date.now()}_${
|
|
47
|
+
return `scan_${Date.now()}_${randomBytes(6).toString('hex')}`;
|
|
47
48
|
}
|
|
48
49
|
|
|
49
50
|
export function bytesToHuman(bytes: number): string {
|