ghostpatch 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/LICENSE +21 -0
- package/README.md +213 -0
- package/__tests__/detectors.test.ts +224 -0
- package/__tests__/rules.test.ts +117 -0
- package/__tests__/scanner.test.ts +222 -0
- package/dist/ai/anthropic.d.ts +11 -0
- package/dist/ai/anthropic.d.ts.map +1 -0
- package/dist/ai/anthropic.js +76 -0
- package/dist/ai/anthropic.js.map +1 -0
- package/dist/ai/huggingface.d.ts +12 -0
- package/dist/ai/huggingface.d.ts.map +1 -0
- package/dist/ai/huggingface.js +95 -0
- package/dist/ai/huggingface.js.map +1 -0
- package/dist/ai/openai.d.ts +11 -0
- package/dist/ai/openai.d.ts.map +1 -0
- package/dist/ai/openai.js +71 -0
- package/dist/ai/openai.js.map +1 -0
- package/dist/ai/prompts.d.ts +5 -0
- package/dist/ai/prompts.d.ts.map +1 -0
- package/dist/ai/prompts.js +101 -0
- package/dist/ai/prompts.js.map +1 -0
- package/dist/ai/provider.d.ts +9 -0
- package/dist/ai/provider.d.ts.map +1 -0
- package/dist/ai/provider.js +66 -0
- package/dist/ai/provider.js.map +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +318 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/core/reporter.d.ts +7 -0
- package/dist/core/reporter.d.ts.map +1 -0
- package/dist/core/reporter.js +366 -0
- package/dist/core/reporter.js.map +1 -0
- package/dist/core/rules.d.ts +8 -0
- package/dist/core/rules.d.ts.map +1 -0
- package/dist/core/rules.js +1077 -0
- package/dist/core/rules.js.map +1 -0
- package/dist/core/scanner.d.ts +6 -0
- package/dist/core/scanner.d.ts.map +1 -0
- package/dist/core/scanner.js +217 -0
- package/dist/core/scanner.js.map +1 -0
- package/dist/core/severity.d.ts +100 -0
- package/dist/core/severity.d.ts.map +1 -0
- package/dist/core/severity.js +52 -0
- package/dist/core/severity.js.map +1 -0
- package/dist/detectors/auth.d.ts +3 -0
- package/dist/detectors/auth.d.ts.map +1 -0
- package/dist/detectors/auth.js +138 -0
- package/dist/detectors/auth.js.map +1 -0
- package/dist/detectors/crypto.d.ts +3 -0
- package/dist/detectors/crypto.d.ts.map +1 -0
- package/dist/detectors/crypto.js +128 -0
- package/dist/detectors/crypto.js.map +1 -0
- package/dist/detectors/dependency.d.ts +4 -0
- package/dist/detectors/dependency.d.ts.map +1 -0
- package/dist/detectors/dependency.js +267 -0
- package/dist/detectors/dependency.js.map +1 -0
- package/dist/detectors/deserialize.d.ts +3 -0
- package/dist/detectors/deserialize.d.ts.map +1 -0
- package/dist/detectors/deserialize.js +107 -0
- package/dist/detectors/deserialize.js.map +1 -0
- package/dist/detectors/injection.d.ts +3 -0
- package/dist/detectors/injection.d.ts.map +1 -0
- package/dist/detectors/injection.js +158 -0
- package/dist/detectors/injection.js.map +1 -0
- package/dist/detectors/misconfig.d.ts +3 -0
- package/dist/detectors/misconfig.d.ts.map +1 -0
- package/dist/detectors/misconfig.js +153 -0
- package/dist/detectors/misconfig.js.map +1 -0
- package/dist/detectors/pathtraversal.d.ts +3 -0
- package/dist/detectors/pathtraversal.d.ts.map +1 -0
- package/dist/detectors/pathtraversal.js +90 -0
- package/dist/detectors/pathtraversal.js.map +1 -0
- package/dist/detectors/prototype.d.ts +3 -0
- package/dist/detectors/prototype.d.ts.map +1 -0
- package/dist/detectors/prototype.js +79 -0
- package/dist/detectors/prototype.js.map +1 -0
- package/dist/detectors/secrets.d.ts +4 -0
- package/dist/detectors/secrets.d.ts.map +1 -0
- package/dist/detectors/secrets.js +137 -0
- package/dist/detectors/secrets.js.map +1 -0
- package/dist/detectors/ssrf.d.ts +3 -0
- package/dist/detectors/ssrf.d.ts.map +1 -0
- package/dist/detectors/ssrf.js +78 -0
- package/dist/detectors/ssrf.js.map +1 -0
- package/dist/detectors/zeroday.d.ts +9 -0
- package/dist/detectors/zeroday.d.ts.map +1 -0
- package/dist/detectors/zeroday.js +77 -0
- package/dist/detectors/zeroday.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +42 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp/server.d.ts +2 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +358 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/utils/config.d.ts +4 -0
- package/dist/utils/config.d.ts.map +1 -0
- package/dist/utils/config.js +97 -0
- package/dist/utils/config.js.map +1 -0
- package/dist/utils/fingerprint.d.ts +5 -0
- package/dist/utils/fingerprint.d.ts.map +1 -0
- package/dist/utils/fingerprint.js +55 -0
- package/dist/utils/fingerprint.js.map +1 -0
- package/dist/utils/languages.d.ts +8 -0
- package/dist/utils/languages.d.ts.map +1 -0
- package/dist/utils/languages.js +128 -0
- package/dist/utils/languages.js.map +1 -0
- package/package.json +53 -0
- package/src/ai/anthropic.ts +82 -0
- package/src/ai/huggingface.ts +111 -0
- package/src/ai/openai.ts +75 -0
- package/src/ai/prompts.ts +100 -0
- package/src/ai/provider.ts +68 -0
- package/src/cli/index.ts +314 -0
- package/src/core/reporter.ts +356 -0
- package/src/core/rules.ts +1089 -0
- package/src/core/scanner.ts +201 -0
- package/src/core/severity.ts +140 -0
- package/src/detectors/auth.ts +152 -0
- package/src/detectors/crypto.ts +128 -0
- package/src/detectors/dependency.ts +240 -0
- package/src/detectors/deserialize.ts +106 -0
- package/src/detectors/injection.ts +172 -0
- package/src/detectors/misconfig.ts +152 -0
- package/src/detectors/pathtraversal.ts +89 -0
- package/src/detectors/prototype.ts +77 -0
- package/src/detectors/secrets.ts +138 -0
- package/src/detectors/ssrf.ts +77 -0
- package/src/detectors/zeroday.ts +93 -0
- package/src/index.ts +24 -0
- package/src/mcp/server.ts +379 -0
- package/src/utils/config.ts +64 -0
- package/src/utils/fingerprint.ts +21 -0
- package/src/utils/languages.ts +95 -0
- package/tsconfig.json +20 -0
- package/vitest.config.ts +8 -0
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { Finding, Severity } from '../core/severity';
|
|
2
|
+
import { generateFingerprint } from '../utils/fingerprint';
|
|
3
|
+
import * as fs from 'fs';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
import { execSync } from 'child_process';
|
|
6
|
+
|
|
7
|
+
export function detect(content: string, filePath: string, _language: string): Finding[] {
|
|
8
|
+
const findings: Finding[] = [];
|
|
9
|
+
const basename = path.basename(filePath);
|
|
10
|
+
|
|
11
|
+
if (basename === 'package.json') {
|
|
12
|
+
findings.push(...checkPackageJson(content, filePath));
|
|
13
|
+
} else if (basename === 'requirements.txt' || basename === 'Pipfile') {
|
|
14
|
+
findings.push(...checkPythonDeps(content, filePath));
|
|
15
|
+
} else if (basename === 'pom.xml') {
|
|
16
|
+
findings.push(...checkMavenDeps(content, filePath));
|
|
17
|
+
} else if (basename === 'go.mod') {
|
|
18
|
+
findings.push(...checkGoDeps(content, filePath));
|
|
19
|
+
} else if (basename === 'Gemfile') {
|
|
20
|
+
findings.push(...checkRubyDeps(content, filePath));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return findings;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function checkPackageJson(content: string, filePath: string): Finding[] {
|
|
27
|
+
const findings: Finding[] = [];
|
|
28
|
+
try {
|
|
29
|
+
const pkg = JSON.parse(content);
|
|
30
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
31
|
+
|
|
32
|
+
// Check for wildcard or latest versions
|
|
33
|
+
const lines = content.split('\n');
|
|
34
|
+
for (const [name, version] of Object.entries(allDeps)) {
|
|
35
|
+
const ver = version as string;
|
|
36
|
+
if (ver === '*' || ver === 'latest' || ver === '') {
|
|
37
|
+
const lineNum = findLine(lines, name);
|
|
38
|
+
findings.push({
|
|
39
|
+
id: `DEP-WILDCARD-${filePath}:${lineNum}`,
|
|
40
|
+
ruleId: 'DEP-WILDCARD',
|
|
41
|
+
title: `Wildcard Dependency: ${name}`,
|
|
42
|
+
description: `Package "${name}" uses wildcard version "${ver}". This can introduce breaking changes or vulnerabilities.`,
|
|
43
|
+
severity: Severity.MEDIUM,
|
|
44
|
+
confidence: 'high',
|
|
45
|
+
filePath, line: lineNum,
|
|
46
|
+
codeSnippet: getSnippetFromLines(lines, lineNum - 1),
|
|
47
|
+
cwe: 'CWE-1104', owasp: 'A06',
|
|
48
|
+
remediation: 'Pin to a specific version range (e.g., ^1.2.3).',
|
|
49
|
+
fingerprint: generateFingerprint('DEP-WILDCARD', filePath, name),
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Check for known vulnerable packages
|
|
55
|
+
const knownVulnerable: Record<string, { maxSafe: string; cve: string; desc: string }> = {
|
|
56
|
+
'lodash': { maxSafe: '4.17.21', cve: 'CVE-2021-23337', desc: 'Prototype pollution in lodash' },
|
|
57
|
+
'minimist': { maxSafe: '1.2.6', cve: 'CVE-2021-44906', desc: 'Prototype pollution in minimist' },
|
|
58
|
+
'node-fetch': { maxSafe: '2.6.7', cve: 'CVE-2022-0235', desc: 'Exposure of sensitive information' },
|
|
59
|
+
'express': { maxSafe: '4.18.2', cve: 'CVE-2024-29041', desc: 'Open redirect in express' },
|
|
60
|
+
'axios': { maxSafe: '1.6.0', cve: 'CVE-2023-45857', desc: 'CSRF in axios' },
|
|
61
|
+
'jsonwebtoken': { maxSafe: '9.0.0', cve: 'CVE-2022-23529', desc: 'Insecure default algorithm' },
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
for (const [name, info] of Object.entries(knownVulnerable)) {
|
|
65
|
+
if (allDeps[name]) {
|
|
66
|
+
const lineNum = findLine(lines, name);
|
|
67
|
+
findings.push({
|
|
68
|
+
id: `DEP-VULN-${name}-${filePath}:${lineNum}`,
|
|
69
|
+
ruleId: 'DEP-KNOWN-VULN',
|
|
70
|
+
title: `Potentially Vulnerable: ${name}`,
|
|
71
|
+
description: `${info.desc} (${info.cve}). Check if version is below ${info.maxSafe}.`,
|
|
72
|
+
severity: Severity.MEDIUM,
|
|
73
|
+
confidence: 'low',
|
|
74
|
+
filePath, line: lineNum,
|
|
75
|
+
codeSnippet: getSnippetFromLines(lines, lineNum - 1),
|
|
76
|
+
cwe: 'CWE-1035', owasp: 'A06',
|
|
77
|
+
remediation: `Update ${name} to version ${info.maxSafe} or later.`,
|
|
78
|
+
fingerprint: generateFingerprint('DEP-KNOWN-VULN', filePath, name),
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
} catch {
|
|
83
|
+
// Invalid JSON — skip
|
|
84
|
+
}
|
|
85
|
+
return findings;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function checkPythonDeps(content: string, filePath: string): Finding[] {
|
|
89
|
+
const findings: Finding[] = [];
|
|
90
|
+
const lines = content.split('\n');
|
|
91
|
+
const knownVuln: Record<string, string> = {
|
|
92
|
+
'django': 'CVE-2023-46695',
|
|
93
|
+
'flask': 'CVE-2023-30861',
|
|
94
|
+
'requests': 'CVE-2023-32681',
|
|
95
|
+
'pillow': 'CVE-2023-44271',
|
|
96
|
+
'pyyaml': 'CVE-2020-14343',
|
|
97
|
+
'jinja2': 'CVE-2024-22195',
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
for (let i = 0; i < lines.length; i++) {
|
|
101
|
+
const line = lines[i].trim();
|
|
102
|
+
if (!line || line.startsWith('#')) continue;
|
|
103
|
+
const match = line.match(/^([a-zA-Z0-9_-]+)/);
|
|
104
|
+
if (match) {
|
|
105
|
+
const pkg = match[1].toLowerCase();
|
|
106
|
+
if (knownVuln[pkg]) {
|
|
107
|
+
findings.push({
|
|
108
|
+
id: `DEP-PY-${pkg}-${filePath}:${i + 1}`,
|
|
109
|
+
ruleId: 'DEP-PYTHON-VULN',
|
|
110
|
+
title: `Check Python Package: ${pkg}`,
|
|
111
|
+
description: `Package ${pkg} has known vulnerability ${knownVuln[pkg]}. Verify version.`,
|
|
112
|
+
severity: Severity.MEDIUM, confidence: 'low',
|
|
113
|
+
filePath, line: i + 1,
|
|
114
|
+
codeSnippet: getSnippetFromLines(lines, i),
|
|
115
|
+
cwe: 'CWE-1035', owasp: 'A06',
|
|
116
|
+
remediation: `Update ${pkg} to the latest patched version.`,
|
|
117
|
+
fingerprint: generateFingerprint('DEP-PYTHON-VULN', filePath, pkg),
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return findings;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function checkMavenDeps(content: string, filePath: string): Finding[] {
|
|
126
|
+
const findings: Finding[] = [];
|
|
127
|
+
const lines = content.split('\n');
|
|
128
|
+
const knownVuln = ['log4j', 'commons-collections', 'struts', 'spring-core'];
|
|
129
|
+
|
|
130
|
+
for (let i = 0; i < lines.length; i++) {
|
|
131
|
+
const line = lines[i];
|
|
132
|
+
for (const pkg of knownVuln) {
|
|
133
|
+
if (line.includes(pkg)) {
|
|
134
|
+
findings.push({
|
|
135
|
+
id: `DEP-MAVEN-${pkg}-${filePath}:${i + 1}`,
|
|
136
|
+
ruleId: 'DEP-MAVEN-VULN',
|
|
137
|
+
title: `Check Maven Dependency: ${pkg}`,
|
|
138
|
+
description: `Package ${pkg} has a history of critical vulnerabilities. Verify version.`,
|
|
139
|
+
severity: Severity.MEDIUM, confidence: 'low',
|
|
140
|
+
filePath, line: i + 1,
|
|
141
|
+
codeSnippet: getSnippetFromLines(lines, i),
|
|
142
|
+
cwe: 'CWE-1035', owasp: 'A06',
|
|
143
|
+
remediation: `Update ${pkg} to the latest patched version.`,
|
|
144
|
+
fingerprint: generateFingerprint('DEP-MAVEN-VULN', filePath, pkg),
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return findings;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function checkGoDeps(content: string, filePath: string): Finding[] {
|
|
153
|
+
return []; // Go module checking would require network access
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function checkRubyDeps(content: string, filePath: string): Finding[] {
|
|
157
|
+
const findings: Finding[] = [];
|
|
158
|
+
const lines = content.split('\n');
|
|
159
|
+
const knownVuln = ['rails', 'rack', 'actionpack', 'activesupport'];
|
|
160
|
+
|
|
161
|
+
for (let i = 0; i < lines.length; i++) {
|
|
162
|
+
const line = lines[i];
|
|
163
|
+
for (const pkg of knownVuln) {
|
|
164
|
+
if (line.includes(`'${pkg}'`) || line.includes(`"${pkg}"`)) {
|
|
165
|
+
findings.push({
|
|
166
|
+
id: `DEP-RUBY-${pkg}-${filePath}:${i + 1}`,
|
|
167
|
+
ruleId: 'DEP-RUBY-VULN',
|
|
168
|
+
title: `Check Ruby Gem: ${pkg}`,
|
|
169
|
+
description: `Gem ${pkg} has known vulnerabilities. Verify version.`,
|
|
170
|
+
severity: Severity.MEDIUM, confidence: 'low',
|
|
171
|
+
filePath, line: i + 1,
|
|
172
|
+
codeSnippet: getSnippetFromLines(lines, i),
|
|
173
|
+
cwe: 'CWE-1035', owasp: 'A06',
|
|
174
|
+
remediation: `Update ${pkg} to the latest patched version.`,
|
|
175
|
+
fingerprint: generateFingerprint('DEP-RUBY-VULN', filePath, pkg),
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return findings;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function runNpmAudit(projectDir: string): Finding[] {
|
|
184
|
+
const findings: Finding[] = [];
|
|
185
|
+
try {
|
|
186
|
+
const lockPath = path.join(projectDir, 'package-lock.json');
|
|
187
|
+
if (!fs.existsSync(lockPath)) return findings;
|
|
188
|
+
|
|
189
|
+
const result = execSync('npm audit --json 2>/dev/null', {
|
|
190
|
+
cwd: projectDir,
|
|
191
|
+
timeout: 30000,
|
|
192
|
+
encoding: 'utf-8',
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
const audit = JSON.parse(result);
|
|
196
|
+
if (audit.vulnerabilities) {
|
|
197
|
+
for (const [name, info] of Object.entries(audit.vulnerabilities) as [string, any][]) {
|
|
198
|
+
const severity = info.severity === 'critical' ? Severity.CRITICAL
|
|
199
|
+
: info.severity === 'high' ? Severity.HIGH
|
|
200
|
+
: info.severity === 'moderate' ? Severity.MEDIUM
|
|
201
|
+
: Severity.LOW;
|
|
202
|
+
|
|
203
|
+
findings.push({
|
|
204
|
+
id: `NPM-AUDIT-${name}`,
|
|
205
|
+
ruleId: 'DEP-NPM-AUDIT',
|
|
206
|
+
title: `Vulnerable Package: ${name}`,
|
|
207
|
+
description: `${info.via?.[0]?.title || 'Known vulnerability'} in ${name}@${info.range || 'unknown'}`,
|
|
208
|
+
severity, confidence: 'high',
|
|
209
|
+
filePath: path.join(projectDir, 'package.json'),
|
|
210
|
+
line: 1,
|
|
211
|
+
codeSnippet: `${name}: ${info.range || 'unknown version'}`,
|
|
212
|
+
cwe: info.via?.[0]?.cwe?.[0] || 'CWE-1035',
|
|
213
|
+
owasp: 'A06',
|
|
214
|
+
remediation: info.fixAvailable ? `Run: npm audit fix` : `Update ${name} manually.`,
|
|
215
|
+
fingerprint: generateFingerprint('DEP-NPM-AUDIT', name, info.range || ''),
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
} catch {
|
|
220
|
+
// npm audit failed or not available
|
|
221
|
+
}
|
|
222
|
+
return findings;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function findLine(lines: string[], searchTerm: string): number {
|
|
226
|
+
for (let i = 0; i < lines.length; i++) {
|
|
227
|
+
if (lines[i].includes(searchTerm)) return i + 1;
|
|
228
|
+
}
|
|
229
|
+
return 1;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function getSnippetFromLines(lines: string[], index: number, context = 2): string {
|
|
233
|
+
const start = Math.max(0, index - context);
|
|
234
|
+
const end = Math.min(lines.length, index + context + 1);
|
|
235
|
+
return lines.slice(start, end).map((l, i) => {
|
|
236
|
+
const lineNum = start + i + 1;
|
|
237
|
+
const marker = (start + i === index) ? '>' : ' ';
|
|
238
|
+
return `${marker} ${lineNum} | ${l}`;
|
|
239
|
+
}).join('\n');
|
|
240
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { Finding, Severity } from '../core/severity';
|
|
2
|
+
import { generateFingerprint } from '../utils/fingerprint';
|
|
3
|
+
|
|
4
|
+
const PATTERNS = [
|
|
5
|
+
{
|
|
6
|
+
id: 'DESER-JS', name: 'Insecure Deserialization (JS)', severity: Severity.CRITICAL, confidence: 'high' as const,
|
|
7
|
+
cwe: 'CWE-502', languages: ['javascript', 'typescript'],
|
|
8
|
+
pattern: /(?:serialize|node-serialize|funcster|cryo)\s*\.\s*(?:unserialize|parse|deserialize)\s*\(/i,
|
|
9
|
+
description: 'Node.js deserialization of untrusted data can lead to RCE.',
|
|
10
|
+
remediation: 'Avoid native serialization. Use JSON for data exchange.',
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
id: 'DESER-YAML-JS', name: 'Unsafe YAML Loading (JS)', severity: Severity.HIGH, confidence: 'high' as const,
|
|
14
|
+
cwe: 'CWE-502', languages: ['javascript', 'typescript'],
|
|
15
|
+
pattern: /js-yaml\.load\s*\(/,
|
|
16
|
+
antiPattern: /(?:safeLoad|schema.*SAFE|JSON_SCHEMA|FAILSAFE)/i,
|
|
17
|
+
description: 'js-yaml.load() without safe schema can execute code.',
|
|
18
|
+
remediation: 'Use yaml.load(data, { schema: SAFE_SCHEMA }) or yaml.safeLoad().',
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
id: 'DESER-PICKLE', name: 'Insecure Deserialization (Python pickle)', severity: Severity.CRITICAL, confidence: 'high' as const,
|
|
22
|
+
cwe: 'CWE-502', languages: ['python'],
|
|
23
|
+
pattern: /(?:pickle\.loads?|cPickle\.loads?|shelve\.open|dill\.loads?)\s*\(/,
|
|
24
|
+
description: 'Python pickle deserialization enables arbitrary code execution.',
|
|
25
|
+
remediation: 'Avoid pickle for untrusted data. Use JSON or protocol buffers.',
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
id: 'DESER-YAML-PY', name: 'Unsafe YAML Loading (Python)', severity: Severity.HIGH, confidence: 'high' as const,
|
|
29
|
+
cwe: 'CWE-502', languages: ['python'],
|
|
30
|
+
pattern: /yaml\.(?:load|unsafe_load)\s*\(/,
|
|
31
|
+
antiPattern: /(?:safe_load|SafeLoader|Loader\s*=\s*(?:yaml\.)?SafeLoader)/,
|
|
32
|
+
description: 'yaml.load() without SafeLoader enables code execution.',
|
|
33
|
+
remediation: 'Use yaml.safe_load() or yaml.load(data, Loader=SafeLoader).',
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
id: 'DESER-JAVA', name: 'Insecure Deserialization (Java)', severity: Severity.CRITICAL, confidence: 'high' as const,
|
|
37
|
+
cwe: 'CWE-502', languages: ['java', 'kotlin'],
|
|
38
|
+
pattern: /(?:ObjectInputStream|readObject\s*\(|XMLDecoder|XStream|Kryo\.readObject|Hessian)\s*[\.(]/,
|
|
39
|
+
antiPattern: /(?:ObjectInputFilter|whitelist|allowlist|resolveClass)/i,
|
|
40
|
+
description: 'Java native deserialization enables RCE.',
|
|
41
|
+
remediation: 'Use allowlist-based ObjectInputFilter or avoid native serialization.',
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
id: 'DESER-PHP', name: 'Insecure Deserialization (PHP)', severity: Severity.CRITICAL, confidence: 'high' as const,
|
|
45
|
+
cwe: 'CWE-502', languages: ['php'],
|
|
46
|
+
pattern: /(?:unserialize|phpunserialize)\s*\(\s*\$/,
|
|
47
|
+
description: 'PHP unserialize() with user input enables object injection.',
|
|
48
|
+
remediation: 'Use json_decode() instead. If using unserialize, set allowed_classes.',
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
id: 'DESER-RUBY', name: 'Insecure Deserialization (Ruby)', severity: Severity.CRITICAL, confidence: 'high' as const,
|
|
52
|
+
cwe: 'CWE-502', languages: ['ruby'],
|
|
53
|
+
pattern: /(?:Marshal\.load|YAML\.load|Psych\.load)\s*\(/,
|
|
54
|
+
antiPattern: /(?:safe_load|permitted_classes)/i,
|
|
55
|
+
description: 'Ruby deserialization of untrusted data.',
|
|
56
|
+
remediation: 'Use YAML.safe_load or JSON.parse instead.',
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
id: 'DESER-DOTNET', name: 'Insecure Deserialization (.NET)', severity: Severity.CRITICAL, confidence: 'high' as const,
|
|
60
|
+
cwe: 'CWE-502', languages: ['csharp'],
|
|
61
|
+
pattern: /(?:BinaryFormatter|SoapFormatter|NetDataContractSerializer|ObjectStateFormatter|LosFormatter)\.Deserialize\s*\(/,
|
|
62
|
+
description: '.NET deserialization with unsafe formatters.',
|
|
63
|
+
remediation: 'Use System.Text.Json or Newtonsoft.Json instead.',
|
|
64
|
+
},
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
export function detect(content: string, filePath: string, language: string): Finding[] {
|
|
68
|
+
const findings: Finding[] = [];
|
|
69
|
+
const lines = content.split('\n');
|
|
70
|
+
|
|
71
|
+
for (const pat of PATTERNS) {
|
|
72
|
+
if (!pat.languages.includes(language)) continue;
|
|
73
|
+
|
|
74
|
+
for (let i = 0; i < lines.length; i++) {
|
|
75
|
+
const line = lines[i];
|
|
76
|
+
if (pat.pattern.test(line)) {
|
|
77
|
+
if (pat.antiPattern) {
|
|
78
|
+
const cs = Math.max(0, i - 3);
|
|
79
|
+
const ce = Math.min(lines.length, i + 4);
|
|
80
|
+
if (pat.antiPattern.test(lines.slice(cs, ce).join('\n'))) continue;
|
|
81
|
+
}
|
|
82
|
+
findings.push({
|
|
83
|
+
id: `${pat.id}-${filePath}:${i + 1}`,
|
|
84
|
+
ruleId: pat.id, title: pat.name, description: pat.description,
|
|
85
|
+
severity: pat.severity, confidence: pat.confidence,
|
|
86
|
+
filePath, line: i + 1,
|
|
87
|
+
codeSnippet: getSnippet(lines, i),
|
|
88
|
+
cwe: pat.cwe, owasp: 'A08',
|
|
89
|
+
remediation: pat.remediation,
|
|
90
|
+
fingerprint: generateFingerprint(pat.id, filePath, line.trim()),
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return findings;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function getSnippet(lines: string[], index: number, context = 2): string {
|
|
99
|
+
const start = Math.max(0, index - context);
|
|
100
|
+
const end = Math.min(lines.length, index + context + 1);
|
|
101
|
+
return lines.slice(start, end).map((l, i) => {
|
|
102
|
+
const lineNum = start + i + 1;
|
|
103
|
+
const marker = (start + i === index) ? '>' : ' ';
|
|
104
|
+
return `${marker} ${lineNum} | ${l}`;
|
|
105
|
+
}).join('\n');
|
|
106
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { Finding, Severity } from '../core/severity';
|
|
2
|
+
import { generateFingerprint } from '../utils/fingerprint';
|
|
3
|
+
|
|
4
|
+
interface DetectorPattern {
|
|
5
|
+
id: string;
|
|
6
|
+
name: string;
|
|
7
|
+
severity: Severity;
|
|
8
|
+
confidence: 'high' | 'medium' | 'low';
|
|
9
|
+
cwe: string;
|
|
10
|
+
pattern: RegExp;
|
|
11
|
+
antiPattern?: RegExp;
|
|
12
|
+
languages: string[];
|
|
13
|
+
description: string;
|
|
14
|
+
remediation: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const PATTERNS: DetectorPattern[] = [
|
|
18
|
+
{
|
|
19
|
+
id: 'INJ-SQL-CONCAT', name: 'SQL Injection (String Concatenation)', severity: Severity.CRITICAL, confidence: 'high',
|
|
20
|
+
cwe: 'CWE-89', pattern: /(?:query|execute|exec|raw|prepare)\s*\(\s*['"`](?:SELECT|INSERT|UPDATE|DELETE|DROP|UNION|ALTER)\b[^'"`]*['"`]\s*\+/i,
|
|
21
|
+
languages: ['javascript', 'typescript', 'python', 'java', 'php', 'ruby', 'csharp', 'go'],
|
|
22
|
+
description: 'SQL query constructed via string concatenation with potential user input.',
|
|
23
|
+
remediation: 'Use parameterized queries or prepared statements.',
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
id: 'INJ-SQL-TEMPLATE', name: 'SQL Injection (Template Literal)', severity: Severity.CRITICAL, confidence: 'high',
|
|
27
|
+
cwe: 'CWE-89', pattern: /(?:query|execute|exec|raw)\s*\(\s*`[^`]*(?:SELECT|INSERT|UPDATE|DELETE|DROP)\b[^`]*\$\{/i,
|
|
28
|
+
languages: ['javascript', 'typescript'],
|
|
29
|
+
description: 'SQL query built with template literals containing interpolated values.',
|
|
30
|
+
remediation: 'Use parameterized queries or tagged template literals (e.g., sql`...`).',
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
id: 'INJ-SQL-FSTRING', name: 'SQL Injection (f-string/format)', severity: Severity.CRITICAL, confidence: 'high',
|
|
34
|
+
cwe: 'CWE-89', pattern: /(?:execute|cursor)\s*\(\s*(?:f['"]|['"].*['"]\s*\.format\s*\(|['"].*['"]\s*%\s*)/i,
|
|
35
|
+
antiPattern: /(?:parameterize|placeholder|%s.*,\s*\(|%s.*,\s*\[)/,
|
|
36
|
+
languages: ['python'],
|
|
37
|
+
description: 'SQL query built with Python f-string or .format().',
|
|
38
|
+
remediation: 'Use parameterized queries with %s placeholders and tuple arguments.',
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
id: 'INJ-CMD-EXEC', name: 'Command Injection', severity: Severity.CRITICAL, confidence: 'high',
|
|
42
|
+
cwe: 'CWE-78', pattern: /(?:child_process|exec|execSync|spawn|spawnSync|system|popen|subprocess)\s*[\(.]\s*(?:`[^`]*\$\{|['"][^'"]*['"]\s*\+\s*\w|f['"])/i,
|
|
43
|
+
languages: ['javascript', 'typescript', 'python', 'ruby', 'php'],
|
|
44
|
+
description: 'Operating system command built with dynamic input.',
|
|
45
|
+
remediation: 'Use execFile with argument arrays. Never construct commands from user input.',
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
id: 'INJ-XSS-INNERHTML', name: 'XSS via innerHTML', severity: Severity.HIGH, confidence: 'high',
|
|
49
|
+
cwe: 'CWE-79', pattern: /\.innerHTML\s*=\s*(?['"])/,
|
|
50
|
+
languages: ['javascript', 'typescript'],
|
|
51
|
+
description: 'Setting innerHTML with potentially untrusted content.',
|
|
52
|
+
remediation: 'Use textContent for plain text, or sanitize with DOMPurify before innerHTML.',
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
id: 'INJ-XSS-DOCWRITE', name: 'XSS via document.write', severity: Severity.HIGH, confidence: 'high',
|
|
56
|
+
cwe: 'CWE-79', pattern: /document\.write(?:ln)?\s*\(/,
|
|
57
|
+
languages: ['javascript', 'typescript'],
|
|
58
|
+
description: 'document.write() can introduce XSS vulnerabilities.',
|
|
59
|
+
remediation: 'Use DOM manipulation methods instead.',
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
id: 'INJ-XSS-REACT', name: 'XSS via dangerouslySetInnerHTML', severity: Severity.HIGH, confidence: 'medium',
|
|
63
|
+
cwe: 'CWE-79', pattern: /dangerouslySetInnerHTML/,
|
|
64
|
+
antiPattern: /(?:DOMPurify|sanitize|purify|xss|escape)/i,
|
|
65
|
+
languages: ['javascript', 'typescript'],
|
|
66
|
+
description: 'React dangerouslySetInnerHTML used without visible sanitization.',
|
|
67
|
+
remediation: 'Sanitize content with DOMPurify before using dangerouslySetInnerHTML.',
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
id: 'INJ-EVAL', name: 'Code Injection via eval()', severity: Severity.CRITICAL, confidence: 'high',
|
|
71
|
+
cwe: 'CWE-95', pattern: /\beval\s*\(\s*(?!['"][^'"]*['"])/,
|
|
72
|
+
languages: ['javascript', 'typescript', 'python', 'php', 'ruby'],
|
|
73
|
+
description: 'eval() with dynamic input enables arbitrary code execution.',
|
|
74
|
+
remediation: 'Avoid eval(). Use JSON.parse() for data, or purpose-built parsers.',
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
id: 'INJ-LDAP', name: 'LDAP Injection', severity: Severity.HIGH, confidence: 'medium',
|
|
78
|
+
cwe: 'CWE-90', pattern: /(?:ldap|LDAP).*(?:search|bind|modify|add)\s*\(.*(?:\+\s*\w|`[^`]*\$\{|\.format|%s)/,
|
|
79
|
+
languages: ['javascript', 'typescript', 'python', 'java', 'csharp', 'php'],
|
|
80
|
+
description: 'LDAP query built with dynamic string construction.',
|
|
81
|
+
remediation: 'Escape special LDAP characters and use parameterized filters.',
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
id: 'INJ-NOSQL', name: 'NoSQL Injection', severity: Severity.HIGH, confidence: 'medium',
|
|
85
|
+
cwe: 'CWE-943', pattern: /(?:find|findOne|deleteOne|updateOne|aggregate)\s*\(\s*(?:req\.body|req\.query|req\.params|request\.\w)/,
|
|
86
|
+
languages: ['javascript', 'typescript'],
|
|
87
|
+
description: 'MongoDB query using raw user input without sanitization.',
|
|
88
|
+
remediation: 'Validate input types and use query builders. Reject $-prefixed keys.',
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
id: 'INJ-SSTI', name: 'Server-Side Template Injection', severity: Severity.CRITICAL, confidence: 'medium',
|
|
92
|
+
cwe: 'CWE-1336', pattern: /(?:render_template_string|Template\s*\(|nunjucks\.renderString|ejs\.render)\s*\(\s*(?:req|request|input|user)/i,
|
|
93
|
+
languages: ['javascript', 'typescript', 'python'],
|
|
94
|
+
description: 'User input passed directly as template source.',
|
|
95
|
+
remediation: 'Never use user input as template source. Use data binding with template files.',
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
id: 'INJ-XPATH', name: 'XPath Injection', severity: Severity.HIGH, confidence: 'medium',
|
|
99
|
+
cwe: 'CWE-643', pattern: /(?:xpath|selectNodes?|evaluate)\s*\(.*(?:\+\s*\w|`[^`]*\$\{|\.format)/,
|
|
100
|
+
languages: ['javascript', 'typescript', 'python', 'java', 'csharp', 'php'],
|
|
101
|
+
description: 'XPath query built with dynamic input.',
|
|
102
|
+
remediation: 'Use parameterized XPath queries.',
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
id: 'INJ-HEADER', name: 'HTTP Header Injection', severity: Severity.MEDIUM, confidence: 'medium',
|
|
106
|
+
cwe: 'CWE-113', pattern: /(?:setHeader|writeHead|header)\s*\(\s*['"][^'"]+['"]\s*,\s*(?:req\.|request\.|input|user)/i,
|
|
107
|
+
languages: ['javascript', 'typescript', 'python', 'php'],
|
|
108
|
+
description: 'HTTP header value set from user input.',
|
|
109
|
+
remediation: 'Validate and sanitize header values. Reject newline characters.',
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
id: 'INJ-REGEX', name: 'ReDoS (Regular Expression DoS)', severity: Severity.MEDIUM, confidence: 'medium',
|
|
113
|
+
cwe: 'CWE-1333', pattern: /new\s+RegExp\s*\(\s*(?:req\.|request\.|input|user|param|query|body|arg)/i,
|
|
114
|
+
languages: ['javascript', 'typescript'],
|
|
115
|
+
description: 'Regular expression constructed from user input — ReDoS risk.',
|
|
116
|
+
remediation: 'Escape user input or use RE2 for safe regex evaluation.',
|
|
117
|
+
},
|
|
118
|
+
];
|
|
119
|
+
|
|
120
|
+
export function detect(content: string, filePath: string, language: string): Finding[] {
|
|
121
|
+
const findings: Finding[] = [];
|
|
122
|
+
const lines = content.split('\n');
|
|
123
|
+
|
|
124
|
+
for (const pat of PATTERNS) {
|
|
125
|
+
if (!pat.languages.includes(language)) continue;
|
|
126
|
+
|
|
127
|
+
for (let i = 0; i < lines.length; i++) {
|
|
128
|
+
const line = lines[i];
|
|
129
|
+
if (pat.pattern.test(line)) {
|
|
130
|
+
if (pat.antiPattern && pat.antiPattern.test(line)) continue;
|
|
131
|
+
|
|
132
|
+
// Check surrounding context for anti-patterns
|
|
133
|
+
if (pat.antiPattern) {
|
|
134
|
+
const contextStart = Math.max(0, i - 3);
|
|
135
|
+
const contextEnd = Math.min(lines.length, i + 4);
|
|
136
|
+
const context = lines.slice(contextStart, contextEnd).join('\n');
|
|
137
|
+
if (pat.antiPattern.test(context)) continue;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
findings.push({
|
|
141
|
+
id: `${pat.id}-${filePath}:${i + 1}`,
|
|
142
|
+
ruleId: pat.id,
|
|
143
|
+
title: pat.name,
|
|
144
|
+
description: pat.description,
|
|
145
|
+
severity: pat.severity,
|
|
146
|
+
confidence: pat.confidence,
|
|
147
|
+
filePath,
|
|
148
|
+
line: i + 1,
|
|
149
|
+
codeSnippet: getSnippet(lines, i),
|
|
150
|
+
cwe: pat.cwe,
|
|
151
|
+
owasp: 'A03',
|
|
152
|
+
remediation: pat.remediation,
|
|
153
|
+
fingerprint: generateFingerprint(pat.id, filePath, line.trim()),
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return findings;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function getSnippet(lines: string[], index: number, context: number = 2): string {
|
|
163
|
+
const start = Math.max(0, index - context);
|
|
164
|
+
const end = Math.min(lines.length, index + context + 1);
|
|
165
|
+
return lines.slice(start, end)
|
|
166
|
+
.map((l, i) => {
|
|
167
|
+
const lineNum = start + i + 1;
|
|
168
|
+
const marker = (start + i === index) ? '>' : ' ';
|
|
169
|
+
return `${marker} ${lineNum} | ${l}`;
|
|
170
|
+
})
|
|
171
|
+
.join('\n');
|
|
172
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { Finding, Severity } from '../core/severity';
|
|
2
|
+
import { generateFingerprint } from '../utils/fingerprint';
|
|
3
|
+
|
|
4
|
+
const PATTERNS = [
|
|
5
|
+
{
|
|
6
|
+
id: 'CFG-DEBUG-ON', name: 'Debug Mode Enabled', severity: Severity.MEDIUM, confidence: 'medium' as const,
|
|
7
|
+
cwe: 'CWE-489',
|
|
8
|
+
pattern: /(?:app\.debug\s*=\s*True|DEBUG\s*=\s*True|debug\s*:\s*true|EnableDebugging|\.setDebug\(true\))/i,
|
|
9
|
+
antiPattern: /(?:process\.env|os\.environ|config\.get|if\s|\.env|test|spec)/,
|
|
10
|
+
description: 'Debug mode hardcoded as enabled.',
|
|
11
|
+
remediation: 'Use environment variable for debug mode.',
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
id: 'CFG-INSECURE-COOKIE', name: 'Insecure Cookie Configuration', severity: Severity.MEDIUM, confidence: 'high' as const,
|
|
15
|
+
cwe: 'CWE-614',
|
|
16
|
+
pattern: /(?:secure\s*:\s*false|httpOnly\s*:\s*false)/i,
|
|
17
|
+
description: 'Cookie configured with insecure flags.',
|
|
18
|
+
remediation: 'Set secure: true and httpOnly: true for session cookies.',
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
id: 'CFG-SAMESITE-NONE', name: 'SameSite None Cookie', severity: Severity.MEDIUM, confidence: 'high' as const,
|
|
22
|
+
cwe: 'CWE-1275',
|
|
23
|
+
pattern: /sameSite\s*:\s*['"]?none['"]?/i,
|
|
24
|
+
description: 'Cookie SameSite set to None allows cross-site requests.',
|
|
25
|
+
remediation: 'Use SameSite: "strict" or "lax" unless cross-site is required.',
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
id: 'CFG-MISSING-HELMET', name: 'Missing Security Headers', severity: Severity.LOW, confidence: 'low' as const,
|
|
29
|
+
cwe: 'CWE-693',
|
|
30
|
+
pattern: /(?:app\.listen|createServer|express\(\))/,
|
|
31
|
+
antiPattern: /(?:helmet|security.*header|Content-Security-Policy|X-Frame-Options|Strict-Transport|csp)/i,
|
|
32
|
+
description: 'Web server may lack security headers.',
|
|
33
|
+
remediation: 'Use Helmet.js or manually set CSP, HSTS, X-Frame-Options.',
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
id: 'CFG-CORS-STAR', name: 'CORS Wildcard Origin', severity: Severity.MEDIUM, confidence: 'high' as const,
|
|
37
|
+
cwe: 'CWE-942',
|
|
38
|
+
pattern: /(?:origin\s*:\s*(?:true|['"]?\*['"]?)|Access-Control-Allow-Origin.*\*)/,
|
|
39
|
+
description: 'CORS allows all origins.',
|
|
40
|
+
remediation: 'Restrict CORS to specific trusted origins.',
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
id: 'CFG-GRAPHQL-INTRO', name: 'GraphQL Introspection Enabled', severity: Severity.MEDIUM, confidence: 'high' as const,
|
|
44
|
+
cwe: 'CWE-200',
|
|
45
|
+
pattern: /introspection\s*:\s*true/,
|
|
46
|
+
description: 'GraphQL introspection enabled — schema exposed.',
|
|
47
|
+
remediation: 'Disable introspection in production.',
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
id: 'CFG-ROOT-STATIC', name: 'Static Files from Root', severity: Severity.HIGH, confidence: 'medium' as const,
|
|
51
|
+
cwe: 'CWE-538',
|
|
52
|
+
pattern: /(?:express\.static|serveStatic)\s*\(\s*['"]\.?\/?['"]\s*\)/,
|
|
53
|
+
description: 'Serving static files from root may expose sensitive files.',
|
|
54
|
+
remediation: 'Serve static files from a dedicated public/ directory.',
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
id: 'CFG-DEFAULT-PORT', name: 'Default Debug Port', severity: Severity.LOW, confidence: 'medium' as const,
|
|
58
|
+
cwe: 'CWE-489',
|
|
59
|
+
pattern: /(?:--inspect|--debug|debugger.*port|debug-port)\s*(?:=\s*)?\d+/,
|
|
60
|
+
description: 'Debug port configuration found.',
|
|
61
|
+
remediation: 'Ensure debug ports are not exposed in production.',
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
id: 'CFG-PERMISSIVE-PERMS', name: 'Permissive File Permissions', severity: Severity.MEDIUM, confidence: 'medium' as const,
|
|
65
|
+
cwe: 'CWE-732',
|
|
66
|
+
pattern: /(?:chmod\s+(?:777|666)|0o?777|permissions?\s*[:=]\s*0o?777)/,
|
|
67
|
+
description: 'World-writable file permissions.',
|
|
68
|
+
remediation: 'Use restrictive permissions (644 for files, 755 for directories).',
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
id: 'CFG-BIND-ALL', name: 'Binding to All Interfaces', severity: Severity.LOW, confidence: 'medium' as const,
|
|
72
|
+
cwe: 'CWE-668',
|
|
73
|
+
pattern: /(?:listen\s*\([^)]*['"]0\.0\.0\.0['"]|host\s*[:=]\s*['"]0\.0\.0\.0['"]|INADDR_ANY)/,
|
|
74
|
+
description: 'Server binding to all network interfaces.',
|
|
75
|
+
remediation: 'Bind to 127.0.0.1 in development.',
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
id: 'CFG-STACK-TRACE', name: 'Stack Trace Exposure', severity: Severity.MEDIUM, confidence: 'medium' as const,
|
|
79
|
+
cwe: 'CWE-209',
|
|
80
|
+
pattern: /(?:res\.(?:send|json)\s*\(.*(?:err\.stack|error\.stack|stackTrace)|showStackError\s*:\s*true)/i,
|
|
81
|
+
description: 'Stack traces may be sent to clients.',
|
|
82
|
+
remediation: 'Log errors server-side, send generic messages to clients.',
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
id: 'CFG-BODY-NO-LIMIT', name: 'Body Parser Without Size Limit', severity: Severity.MEDIUM, confidence: 'medium' as const,
|
|
86
|
+
cwe: 'CWE-400',
|
|
87
|
+
pattern: /(?:bodyParser\.json\(\s*\)|express\.json\(\s*\))/,
|
|
88
|
+
antiPattern: /limit/,
|
|
89
|
+
description: 'JSON body parser without size limit.',
|
|
90
|
+
remediation: 'Set a body size limit: express.json({ limit: "100kb" }).',
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
id: 'CFG-ENV-EXPOSURE', name: 'Environment Variables Exposed', severity: Severity.HIGH, confidence: 'high' as const,
|
|
94
|
+
cwe: 'CWE-200',
|
|
95
|
+
pattern: /(?:res\.(?:send|json)|response\.)\s*\(\s*process\.env\s*\)/,
|
|
96
|
+
description: 'Entire process.env sent to client.',
|
|
97
|
+
remediation: 'Only send specific, non-sensitive config values.',
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
id: 'CFG-ELECTRON-NODE', name: 'Electron nodeIntegration Enabled', severity: Severity.HIGH, confidence: 'high' as const,
|
|
101
|
+
cwe: 'CWE-94',
|
|
102
|
+
pattern: /nodeIntegration\s*:\s*true/,
|
|
103
|
+
description: 'Electron nodeIntegration enabled — XSS leads to RCE.',
|
|
104
|
+
remediation: 'Disable nodeIntegration and use contextBridge.',
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
id: 'CFG-ELECTRON-CTX', name: 'Electron contextIsolation Disabled', severity: Severity.HIGH, confidence: 'high' as const,
|
|
108
|
+
cwe: 'CWE-94',
|
|
109
|
+
pattern: /contextIsolation\s*:\s*false/,
|
|
110
|
+
description: 'Electron contextIsolation disabled.',
|
|
111
|
+
remediation: 'Enable contextIsolation.',
|
|
112
|
+
},
|
|
113
|
+
];
|
|
114
|
+
|
|
115
|
+
export function detect(content: string, filePath: string, language: string): Finding[] {
|
|
116
|
+
const findings: Finding[] = [];
|
|
117
|
+
const lines = content.split('\n');
|
|
118
|
+
|
|
119
|
+
for (const pat of PATTERNS) {
|
|
120
|
+
for (let i = 0; i < lines.length; i++) {
|
|
121
|
+
const line = lines[i];
|
|
122
|
+
if (pat.pattern.test(line)) {
|
|
123
|
+
if (pat.antiPattern) {
|
|
124
|
+
const cs = Math.max(0, i - 3);
|
|
125
|
+
const ce = Math.min(lines.length, i + 4);
|
|
126
|
+
if (pat.antiPattern.test(lines.slice(cs, ce).join('\n'))) continue;
|
|
127
|
+
}
|
|
128
|
+
findings.push({
|
|
129
|
+
id: `${pat.id}-${filePath}:${i + 1}`,
|
|
130
|
+
ruleId: pat.id, title: pat.name, description: pat.description,
|
|
131
|
+
severity: pat.severity, confidence: pat.confidence,
|
|
132
|
+
filePath, line: i + 1,
|
|
133
|
+
codeSnippet: getSnippet(lines, i),
|
|
134
|
+
cwe: pat.cwe, owasp: 'A05',
|
|
135
|
+
remediation: pat.remediation,
|
|
136
|
+
fingerprint: generateFingerprint(pat.id, filePath, line.trim()),
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return findings;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function getSnippet(lines: string[], index: number, context = 2): string {
|
|
145
|
+
const start = Math.max(0, index - context);
|
|
146
|
+
const end = Math.min(lines.length, index + context + 1);
|
|
147
|
+
return lines.slice(start, end).map((l, i) => {
|
|
148
|
+
const lineNum = start + i + 1;
|
|
149
|
+
const marker = (start + i === index) ? '>' : ' ';
|
|
150
|
+
return `${marker} ${lineNum} | ${l}`;
|
|
151
|
+
}).join('\n');
|
|
152
|
+
}
|