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,201 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { glob } from 'glob';
|
|
4
|
+
import micromatch from 'micromatch';
|
|
5
|
+
import { Finding, ScanResult, ScanSummary, ScanOptions, Severity, meetsMinSeverity } from './severity';
|
|
6
|
+
import { detectLanguage, isSupportedFile, DEFAULT_EXCLUDE, isConfigFile } from '../utils/languages';
|
|
7
|
+
import { loadConfig } from '../utils/config';
|
|
8
|
+
import { deduplicateFindings } from '../utils/fingerprint';
|
|
9
|
+
|
|
10
|
+
import * as injectionDetector from '../detectors/injection';
|
|
11
|
+
import * as authDetector from '../detectors/auth';
|
|
12
|
+
import * as cryptoDetector from '../detectors/crypto';
|
|
13
|
+
import * as secretsDetector from '../detectors/secrets';
|
|
14
|
+
import * as ssrfDetector from '../detectors/ssrf';
|
|
15
|
+
import * as pathTraversalDetector from '../detectors/pathtraversal';
|
|
16
|
+
import * as prototypeDetector from '../detectors/prototype';
|
|
17
|
+
import * as deserializeDetector from '../detectors/deserialize';
|
|
18
|
+
import * as dependencyDetector from '../detectors/dependency';
|
|
19
|
+
import * as misconfigDetector from '../detectors/misconfig';
|
|
20
|
+
import * as zerodayDetector from '../detectors/zeroday';
|
|
21
|
+
|
|
22
|
+
const ALL_DETECTORS = [
|
|
23
|
+
injectionDetector,
|
|
24
|
+
authDetector,
|
|
25
|
+
cryptoDetector,
|
|
26
|
+
secretsDetector,
|
|
27
|
+
ssrfDetector,
|
|
28
|
+
pathTraversalDetector,
|
|
29
|
+
prototypeDetector,
|
|
30
|
+
deserializeDetector,
|
|
31
|
+
dependencyDetector,
|
|
32
|
+
misconfigDetector,
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
export async function scan(target: string, options: ScanOptions = {}): Promise<ScanResult> {
|
|
36
|
+
const startTime = new Date();
|
|
37
|
+
const resolvedTarget = path.resolve(target || '.');
|
|
38
|
+
const config = loadConfig(options.configPath, resolvedTarget);
|
|
39
|
+
const minSeverity = options.severity || config.severity || Severity.LOW;
|
|
40
|
+
const excludePatterns = options.exclude || config.exclude || DEFAULT_EXCLUDE;
|
|
41
|
+
const maxFileSize = options.maxFileSize || config.maxFileSize || 1048576;
|
|
42
|
+
|
|
43
|
+
let files: string[] = [];
|
|
44
|
+
const stat = fs.statSync(resolvedTarget);
|
|
45
|
+
|
|
46
|
+
if (stat.isFile()) {
|
|
47
|
+
files = [resolvedTarget];
|
|
48
|
+
} else if (stat.isDirectory()) {
|
|
49
|
+
const allFiles = await glob('**/*', {
|
|
50
|
+
cwd: resolvedTarget,
|
|
51
|
+
nodir: true,
|
|
52
|
+
absolute: true,
|
|
53
|
+
ignore: excludePatterns,
|
|
54
|
+
});
|
|
55
|
+
files = allFiles.filter(f => {
|
|
56
|
+
const relative = path.relative(resolvedTarget, f);
|
|
57
|
+
return !micromatch.isMatch(relative, excludePatterns);
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Filter by supported languages and file size
|
|
62
|
+
let filesScanned = 0;
|
|
63
|
+
let filesSkipped = 0;
|
|
64
|
+
let allFindings: Finding[] = [];
|
|
65
|
+
|
|
66
|
+
const scanPromises = files.map(async (filePath) => {
|
|
67
|
+
try {
|
|
68
|
+
const fileStat = fs.statSync(filePath);
|
|
69
|
+
if (fileStat.size > maxFileSize) {
|
|
70
|
+
filesSkipped++;
|
|
71
|
+
return [];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const language = detectLanguage(filePath);
|
|
75
|
+
const isConfig = isConfigFile(filePath);
|
|
76
|
+
|
|
77
|
+
if (!language && !isConfig) {
|
|
78
|
+
filesSkipped++;
|
|
79
|
+
return [];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
filesScanned++;
|
|
83
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
84
|
+
return scanFile(filePath, content, language || 'generic');
|
|
85
|
+
} catch {
|
|
86
|
+
filesSkipped++;
|
|
87
|
+
return [];
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const results = await Promise.all(scanPromises);
|
|
92
|
+
allFindings = results.flat();
|
|
93
|
+
|
|
94
|
+
// Run zero-day suspicious pattern detection
|
|
95
|
+
if (options.ai !== false) {
|
|
96
|
+
for (const filePath of files) {
|
|
97
|
+
try {
|
|
98
|
+
const language = detectLanguage(filePath);
|
|
99
|
+
if (!language) continue;
|
|
100
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
101
|
+
const suspiciousFindings = zerodayDetector.detectSuspiciousPatterns(content, filePath, language);
|
|
102
|
+
allFindings.push(...suspiciousFindings);
|
|
103
|
+
} catch {
|
|
104
|
+
// Skip files that can't be read
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Deduplicate
|
|
110
|
+
allFindings = deduplicateFindings(allFindings);
|
|
111
|
+
|
|
112
|
+
// Filter by severity
|
|
113
|
+
allFindings = allFindings.filter(f => meetsMinSeverity(f.severity, minSeverity));
|
|
114
|
+
|
|
115
|
+
// Sort by severity (critical first)
|
|
116
|
+
const severityOrder = { critical: 0, high: 1, medium: 2, low: 3, info: 4 };
|
|
117
|
+
allFindings.sort((a, b) => severityOrder[a.severity] - severityOrder[b.severity]);
|
|
118
|
+
|
|
119
|
+
const endTime = new Date();
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
target: resolvedTarget,
|
|
123
|
+
startTime,
|
|
124
|
+
endTime,
|
|
125
|
+
durationMs: endTime.getTime() - startTime.getTime(),
|
|
126
|
+
filesScanned,
|
|
127
|
+
filesSkipped,
|
|
128
|
+
findings: allFindings,
|
|
129
|
+
summary: buildSummary(allFindings),
|
|
130
|
+
aiEnabled: options.ai || false,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function scanFile(filePath: string, content: string, language: string): Finding[] {
|
|
135
|
+
const findings: Finding[] = [];
|
|
136
|
+
|
|
137
|
+
for (const detector of ALL_DETECTORS) {
|
|
138
|
+
try {
|
|
139
|
+
const detectorFindings = detector.detect(content, filePath, language);
|
|
140
|
+
findings.push(...detectorFindings);
|
|
141
|
+
} catch {
|
|
142
|
+
// Skip detector errors
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return findings;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export async function scanWithAI(
|
|
150
|
+
findings: Finding[],
|
|
151
|
+
files: Map<string, string>,
|
|
152
|
+
provider: zerodayDetector.AIProvider,
|
|
153
|
+
): Promise<Finding[]> {
|
|
154
|
+
const aiFindings: Finding[] = [];
|
|
155
|
+
|
|
156
|
+
for (const [filePath, content] of files) {
|
|
157
|
+
const language = detectLanguage(filePath);
|
|
158
|
+
if (!language) continue;
|
|
159
|
+
|
|
160
|
+
try {
|
|
161
|
+
const results = await zerodayDetector.analyzeWithAI(content, filePath, language, provider);
|
|
162
|
+
aiFindings.push(...results);
|
|
163
|
+
} catch {
|
|
164
|
+
// AI analysis failed for this file
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return [...findings, ...aiFindings];
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function buildSummary(findings: Finding[]): ScanSummary {
|
|
172
|
+
const bySeverity: Record<Severity, number> = {
|
|
173
|
+
[Severity.CRITICAL]: 0,
|
|
174
|
+
[Severity.HIGH]: 0,
|
|
175
|
+
[Severity.MEDIUM]: 0,
|
|
176
|
+
[Severity.LOW]: 0,
|
|
177
|
+
[Severity.INFO]: 0,
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
const byCategory: Record<string, number> = {};
|
|
181
|
+
const fileCount: Record<string, number> = {};
|
|
182
|
+
|
|
183
|
+
for (const f of findings) {
|
|
184
|
+
bySeverity[f.severity]++;
|
|
185
|
+
const cat = f.owasp || 'other';
|
|
186
|
+
byCategory[cat] = (byCategory[cat] || 0) + 1;
|
|
187
|
+
fileCount[f.filePath] = (fileCount[f.filePath] || 0) + 1;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const topFiles = Object.entries(fileCount)
|
|
191
|
+
.map(([file, count]) => ({ file, count }))
|
|
192
|
+
.sort((a, b) => b.count - a.count)
|
|
193
|
+
.slice(0, 10);
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
total: findings.length,
|
|
197
|
+
bySeverity,
|
|
198
|
+
byCategory,
|
|
199
|
+
topFiles,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
export enum Severity {
|
|
2
|
+
CRITICAL = 'critical',
|
|
3
|
+
HIGH = 'high',
|
|
4
|
+
MEDIUM = 'medium',
|
|
5
|
+
LOW = 'low',
|
|
6
|
+
INFO = 'info',
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const SEVERITY_ORDER: Record<Severity, number> = {
|
|
10
|
+
[Severity.CRITICAL]: 5,
|
|
11
|
+
[Severity.HIGH]: 4,
|
|
12
|
+
[Severity.MEDIUM]: 3,
|
|
13
|
+
[Severity.LOW]: 2,
|
|
14
|
+
[Severity.INFO]: 1,
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const SEVERITY_COLORS: Record<Severity, string> = {
|
|
18
|
+
[Severity.CRITICAL]: '\x1b[41m\x1b[37m',
|
|
19
|
+
[Severity.HIGH]: '\x1b[31m',
|
|
20
|
+
[Severity.MEDIUM]: '\x1b[33m',
|
|
21
|
+
[Severity.LOW]: '\x1b[36m',
|
|
22
|
+
[Severity.INFO]: '\x1b[90m',
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const SEVERITY_ICONS: Record<Severity, string> = {
|
|
26
|
+
[Severity.CRITICAL]: '[!!!]',
|
|
27
|
+
[Severity.HIGH]: '[!!]',
|
|
28
|
+
[Severity.MEDIUM]: '[!]',
|
|
29
|
+
[Severity.LOW]: '[~]',
|
|
30
|
+
[Severity.INFO]: '[i]',
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export function severityFromString(s: string): Severity {
|
|
34
|
+
const lower = s.toLowerCase();
|
|
35
|
+
if (lower in Severity) return lower as Severity;
|
|
36
|
+
const map: Record<string, Severity> = {
|
|
37
|
+
crit: Severity.CRITICAL,
|
|
38
|
+
error: Severity.HIGH,
|
|
39
|
+
warn: Severity.MEDIUM,
|
|
40
|
+
warning: Severity.MEDIUM,
|
|
41
|
+
note: Severity.LOW,
|
|
42
|
+
information: Severity.INFO,
|
|
43
|
+
};
|
|
44
|
+
return map[lower] ?? Severity.MEDIUM;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function meetsMinSeverity(severity: Severity, minSeverity: Severity): boolean {
|
|
48
|
+
return SEVERITY_ORDER[severity] >= SEVERITY_ORDER[minSeverity];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface Finding {
|
|
52
|
+
id: string;
|
|
53
|
+
ruleId: string;
|
|
54
|
+
title: string;
|
|
55
|
+
description: string;
|
|
56
|
+
severity: Severity;
|
|
57
|
+
confidence: 'high' | 'medium' | 'low';
|
|
58
|
+
filePath: string;
|
|
59
|
+
line: number;
|
|
60
|
+
column?: number;
|
|
61
|
+
endLine?: number;
|
|
62
|
+
endColumn?: number;
|
|
63
|
+
codeSnippet: string;
|
|
64
|
+
cwe?: string;
|
|
65
|
+
owasp?: string;
|
|
66
|
+
remediation?: string;
|
|
67
|
+
aiEnhanced?: boolean;
|
|
68
|
+
fingerprint: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface AIFinding {
|
|
72
|
+
title: string;
|
|
73
|
+
description: string;
|
|
74
|
+
severity: Severity;
|
|
75
|
+
confidence: 'high' | 'medium' | 'low';
|
|
76
|
+
line?: number;
|
|
77
|
+
cwe?: string;
|
|
78
|
+
remediation?: string;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface ScanResult {
|
|
82
|
+
target: string;
|
|
83
|
+
startTime: Date;
|
|
84
|
+
endTime: Date;
|
|
85
|
+
durationMs: number;
|
|
86
|
+
filesScanned: number;
|
|
87
|
+
filesSkipped: number;
|
|
88
|
+
findings: Finding[];
|
|
89
|
+
summary: ScanSummary;
|
|
90
|
+
aiEnabled: boolean;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface ScanSummary {
|
|
94
|
+
total: number;
|
|
95
|
+
bySeverity: Record<Severity, number>;
|
|
96
|
+
byCategory: Record<string, number>;
|
|
97
|
+
topFiles: Array<{ file: string; count: number }>;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface Rule {
|
|
101
|
+
id: string;
|
|
102
|
+
name: string;
|
|
103
|
+
severity: Severity;
|
|
104
|
+
confidence: 'high' | 'medium' | 'low';
|
|
105
|
+
cwe?: string;
|
|
106
|
+
owasp?: string;
|
|
107
|
+
pattern: RegExp;
|
|
108
|
+
antiPattern?: RegExp;
|
|
109
|
+
languages: string[];
|
|
110
|
+
description: string;
|
|
111
|
+
remediation: string;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export interface ScanOptions {
|
|
115
|
+
target?: string;
|
|
116
|
+
output?: 'terminal' | 'json' | 'sarif' | 'html';
|
|
117
|
+
severity?: Severity;
|
|
118
|
+
ai?: boolean;
|
|
119
|
+
provider?: 'huggingface' | 'anthropic' | 'openai';
|
|
120
|
+
fix?: boolean;
|
|
121
|
+
quiet?: boolean;
|
|
122
|
+
exclude?: string[];
|
|
123
|
+
maxFileSize?: number;
|
|
124
|
+
configPath?: string;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export interface GhostPatchConfig {
|
|
128
|
+
exclude: string[];
|
|
129
|
+
severity: Severity;
|
|
130
|
+
ai: {
|
|
131
|
+
provider: string;
|
|
132
|
+
model: string;
|
|
133
|
+
};
|
|
134
|
+
rules: {
|
|
135
|
+
disabled: string[];
|
|
136
|
+
custom: Rule[];
|
|
137
|
+
};
|
|
138
|
+
maxFileSize: number;
|
|
139
|
+
languages: string | string[];
|
|
140
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
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: 'AUTH-BYPASS-NONE-ALG', name: 'JWT None Algorithm', severity: Severity.CRITICAL, confidence: 'high',
|
|
20
|
+
cwe: 'CWE-345', pattern: /algorithms?\s*:\s*\[?\s*['"]none['"]/i,
|
|
21
|
+
languages: ['javascript', 'typescript'],
|
|
22
|
+
description: 'JWT accepts "none" algorithm, allowing signature bypass.',
|
|
23
|
+
remediation: 'Explicitly set allowed algorithms: algorithms: ["HS256"] or ["RS256"].',
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
id: 'AUTH-HARDCODED-JWT', name: 'Hardcoded JWT Secret', severity: Severity.CRITICAL, confidence: 'high',
|
|
27
|
+
cwe: 'CWE-798', pattern: /(?:jwt|jsonwebtoken)\.(?:sign|verify)\s*\([^)]*,\s*['"][^'"]{4,}['"]/i,
|
|
28
|
+
languages: ['javascript', 'typescript'],
|
|
29
|
+
description: 'JWT signing secret hardcoded in source code.',
|
|
30
|
+
remediation: 'Use environment variables for JWT secrets.',
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
id: 'AUTH-NO-EXPIRY', name: 'JWT Without Expiration', severity: Severity.MEDIUM, confidence: 'medium',
|
|
34
|
+
cwe: 'CWE-613', pattern: /jwt\.sign\s*\(\s*\{[^}]*\}\s*,/,
|
|
35
|
+
antiPattern: /(?:expiresIn|exp\s*:|maxAge)/i,
|
|
36
|
+
languages: ['javascript', 'typescript'],
|
|
37
|
+
description: 'JWT created without expiration time.',
|
|
38
|
+
remediation: 'Set token expiration: jwt.sign(payload, secret, { expiresIn: "1h" }).',
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
id: 'AUTH-WEAK-PASSWORD', name: 'Weak Password Policy', severity: Severity.MEDIUM, confidence: 'medium',
|
|
42
|
+
cwe: 'CWE-521', pattern: /(?:password|passwd).*(?:min|length|minLength)\s*[:=<]\s*[1-7]\b/i,
|
|
43
|
+
languages: ['javascript', 'typescript', 'python', 'java', 'php', 'ruby', 'csharp', 'go'],
|
|
44
|
+
description: 'Password minimum length is too short.',
|
|
45
|
+
remediation: 'Enforce minimum 8-character passwords, ideally 12+.',
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
id: 'AUTH-PLAINTEXT-PASS', name: 'Password Stored Without Hashing', severity: Severity.CRITICAL, confidence: 'medium',
|
|
49
|
+
cwe: 'CWE-256', pattern: /(?:user|account).*(?:password|passwd)\s*[:=]\s*(?:req\.|request\.|input\.|body\.)/i,
|
|
50
|
+
antiPattern: /(?:hash|bcrypt|scrypt|argon|pbkdf|encrypt|crypt)/i,
|
|
51
|
+
languages: ['javascript', 'typescript', 'python', 'java', 'php', 'ruby'],
|
|
52
|
+
description: 'Password from user input may be stored without hashing.',
|
|
53
|
+
remediation: 'Hash passwords with bcrypt, scrypt, or argon2 before storage.',
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
id: 'AUTH-SESSION-FIXATION', name: 'Session Fixation', severity: Severity.HIGH, confidence: 'low',
|
|
57
|
+
cwe: 'CWE-384', pattern: /(?:login|authenticate|signIn)\s*(?:=|:|\().*(?:session|cookie)/i,
|
|
58
|
+
antiPattern: /(?:regenerate|destroy|invalidate|new.*session)/i,
|
|
59
|
+
languages: ['javascript', 'typescript', 'python', 'java', 'php'],
|
|
60
|
+
description: 'Login handler may not regenerate session ID.',
|
|
61
|
+
remediation: 'Regenerate session ID after successful authentication.',
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
id: 'AUTH-DEFAULT-CREDS', name: 'Default Credentials', severity: Severity.CRITICAL, confidence: 'high',
|
|
65
|
+
cwe: 'CWE-798', pattern: /(?:password|passwd|pwd)\s*[:=]\s*['"](?:admin|password|123456|root|default|test|guest|letmein|welcome|12345678|qwerty|abc123)['"]/i,
|
|
66
|
+
antiPattern: /(?:test|spec|mock|example|placeholder)/i,
|
|
67
|
+
languages: ['javascript', 'typescript', 'python', 'java', 'php', 'ruby', 'go', 'csharp'],
|
|
68
|
+
description: 'Default or commonly guessed password found in code.',
|
|
69
|
+
remediation: 'Remove hardcoded credentials. Use environment variables or secret management.',
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
id: 'AUTH-NO-LOCKOUT', name: 'Missing Account Lockout', severity: Severity.MEDIUM, confidence: 'low',
|
|
73
|
+
cwe: 'CWE-307', pattern: /(?:login|authenticate|signIn)\s*(?:=|:|\().*(?:password|credential)/i,
|
|
74
|
+
antiPattern: /(?:lockout|maxAttempts|failedAttempts|brute|throttle|rateLimit|delay)/i,
|
|
75
|
+
languages: ['javascript', 'typescript', 'python', 'java', 'php'],
|
|
76
|
+
description: 'Login function without brute force protection.',
|
|
77
|
+
remediation: 'Implement account lockout after N failed attempts.',
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
id: 'AUTH-PASS-IN-URL', name: 'Password in URL', severity: Severity.HIGH, confidence: 'high',
|
|
81
|
+
cwe: 'CWE-598', pattern: /(?:url|href|redirect|link|location).*[?&](?:password|passwd|pwd|secret|token)=/i,
|
|
82
|
+
languages: ['javascript', 'typescript', 'python', 'java', 'php', 'ruby', 'go', 'csharp'],
|
|
83
|
+
description: 'Sensitive credential sent in URL query parameter.',
|
|
84
|
+
remediation: 'Send credentials in request body or headers.',
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
id: 'AUTH-MISSING-MIDDLEWARE', name: 'Route Without Auth Middleware', severity: Severity.HIGH, confidence: 'medium',
|
|
88
|
+
cwe: 'CWE-862', pattern: /(?:app|router)\.(get|post|put|delete|patch)\s*\(\s*['"]\/(?:admin|api\/admin|dashboard|settings|users?\/\w|account)/i,
|
|
89
|
+
antiPattern: /(?:auth|protect|guard|session|verify|middleware|isAuthenticated|requireAuth|passport)/i,
|
|
90
|
+
languages: ['javascript', 'typescript'],
|
|
91
|
+
description: 'Sensitive route may lack authentication middleware.',
|
|
92
|
+
remediation: 'Add authentication middleware before the route handler.',
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
id: 'AUTH-PRIVILEGE-ESCALATION', name: 'Privilege Escalation Risk', severity: Severity.CRITICAL, confidence: 'medium',
|
|
96
|
+
cwe: 'CWE-269', pattern: /(?:role|isAdmin|is_admin|permission|privilege)\s*=\s*(?:req\.|request\.|params|body|query|input)/i,
|
|
97
|
+
languages: ['javascript', 'typescript', 'python', 'java', 'php'],
|
|
98
|
+
description: 'User role or admin status set from user-controlled input.',
|
|
99
|
+
remediation: 'Derive roles from server-side session data, never from user input.',
|
|
100
|
+
},
|
|
101
|
+
];
|
|
102
|
+
|
|
103
|
+
export function detect(content: string, filePath: string, language: string): Finding[] {
|
|
104
|
+
const findings: Finding[] = [];
|
|
105
|
+
const lines = content.split('\n');
|
|
106
|
+
|
|
107
|
+
for (const pat of PATTERNS) {
|
|
108
|
+
if (!pat.languages.includes(language)) continue;
|
|
109
|
+
|
|
110
|
+
for (let i = 0; i < lines.length; i++) {
|
|
111
|
+
const line = lines[i];
|
|
112
|
+
if (pat.pattern.test(line)) {
|
|
113
|
+
if (pat.antiPattern) {
|
|
114
|
+
const contextStart = Math.max(0, i - 5);
|
|
115
|
+
const contextEnd = Math.min(lines.length, i + 6);
|
|
116
|
+
const context = lines.slice(contextStart, contextEnd).join('\n');
|
|
117
|
+
if (pat.antiPattern.test(context)) continue;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
findings.push({
|
|
121
|
+
id: `${pat.id}-${filePath}:${i + 1}`,
|
|
122
|
+
ruleId: pat.id,
|
|
123
|
+
title: pat.name,
|
|
124
|
+
description: pat.description,
|
|
125
|
+
severity: pat.severity,
|
|
126
|
+
confidence: pat.confidence,
|
|
127
|
+
filePath,
|
|
128
|
+
line: i + 1,
|
|
129
|
+
codeSnippet: getSnippet(lines, i),
|
|
130
|
+
cwe: pat.cwe,
|
|
131
|
+
owasp: 'A07',
|
|
132
|
+
remediation: pat.remediation,
|
|
133
|
+
fingerprint: generateFingerprint(pat.id, filePath, line.trim()),
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return findings;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function getSnippet(lines: string[], index: number, context: number = 2): string {
|
|
143
|
+
const start = Math.max(0, index - context);
|
|
144
|
+
const end = Math.min(lines.length, index + context + 1);
|
|
145
|
+
return lines.slice(start, end)
|
|
146
|
+
.map((l, i) => {
|
|
147
|
+
const lineNum = start + i + 1;
|
|
148
|
+
const marker = (start + i === index) ? '>' : ' ';
|
|
149
|
+
return `${marker} ${lineNum} | ${l}`;
|
|
150
|
+
})
|
|
151
|
+
.join('\n');
|
|
152
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { Finding, Severity } from '../core/severity';
|
|
2
|
+
import { generateFingerprint } from '../utils/fingerprint';
|
|
3
|
+
|
|
4
|
+
const PATTERNS = [
|
|
5
|
+
{
|
|
6
|
+
id: 'CRYPTO-MD5', name: 'Weak Hash (MD5)', severity: Severity.HIGH, confidence: 'high' as const,
|
|
7
|
+
cwe: 'CWE-328', pattern: /(?:md5|MD5)\s*[\(.<]/,
|
|
8
|
+
languages: ['javascript', 'typescript', 'python', 'java', 'go', 'php', 'ruby', 'csharp', 'c', 'cpp'],
|
|
9
|
+
description: 'MD5 is cryptographically broken.', remediation: 'Use SHA-256+ for integrity, bcrypt/argon2 for passwords.',
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
id: 'CRYPTO-SHA1', name: 'Weak Hash (SHA-1)', severity: Severity.MEDIUM, confidence: 'high' as const,
|
|
13
|
+
cwe: 'CWE-328', pattern: /(?:sha-?1|SHA-?1)\s*[\(.<'"]/,
|
|
14
|
+
languages: ['javascript', 'typescript', 'python', 'java', 'go', 'php', 'ruby', 'csharp'],
|
|
15
|
+
description: 'SHA-1 is deprecated for security use.', remediation: 'Use SHA-256 or stronger.',
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
id: 'CRYPTO-WEAK-CIPHER', name: 'Weak Cipher Algorithm', severity: Severity.HIGH, confidence: 'high' as const,
|
|
19
|
+
cwe: 'CWE-327', pattern: /(?:createCipher(?:iv)?\s*\(\s*['"](?:des|rc4|rc2|blowfish)|DES(?:ede)?|RC4|Blowfish)\b/i,
|
|
20
|
+
languages: ['javascript', 'typescript', 'python', 'java', 'go', 'csharp'],
|
|
21
|
+
description: 'Weak or broken cipher algorithm.', remediation: 'Use AES-256-GCM or ChaCha20-Poly1305.',
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
id: 'CRYPTO-ECB', name: 'ECB Mode', severity: Severity.HIGH, confidence: 'high' as const,
|
|
25
|
+
cwe: 'CWE-327', pattern: /(?:aes.*ecb|ECB|\.ECB|mode.*ecb|ecb.*mode)/i,
|
|
26
|
+
languages: ['javascript', 'typescript', 'python', 'java', 'go', 'csharp'],
|
|
27
|
+
description: 'ECB mode does not provide semantic security.', remediation: 'Use GCM or CBC with HMAC.',
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
id: 'CRYPTO-MATH-RANDOM', name: 'Insecure Random (Math.random)', severity: Severity.HIGH, confidence: 'high' as const,
|
|
31
|
+
cwe: 'CWE-330', pattern: /Math\.random\s*\(\)/,
|
|
32
|
+
antiPattern: /(?:test|mock|sample|example|demo|shuffle|color|animation|ui|css|game|placeholder)/i,
|
|
33
|
+
languages: ['javascript', 'typescript'],
|
|
34
|
+
description: 'Math.random() is not cryptographically secure.', remediation: 'Use crypto.randomBytes() or crypto.getRandomValues().',
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
id: 'CRYPTO-HARDCODED-KEY', name: 'Hardcoded Encryption Key', severity: Severity.CRITICAL, confidence: 'high' as const,
|
|
38
|
+
cwe: 'CWE-321', pattern: /(?:(?:encryption|encrypt|cipher|aes|secret)[-_]?key)\s*[:=]\s*['"][^'"]{8,}['"]/i,
|
|
39
|
+
antiPattern: /(?:process\.env|os\.environ|config\.|env\[|example|placeholder|your_)/i,
|
|
40
|
+
languages: ['javascript', 'typescript', 'python', 'java', 'go', 'php', 'ruby', 'csharp'],
|
|
41
|
+
description: 'Hardcoded encryption key in source code.', remediation: 'Use environment variables or key management service.',
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
id: 'CRYPTO-HARDCODED-IV', name: 'Hardcoded IV/Nonce', severity: Severity.HIGH, confidence: 'medium' as const,
|
|
45
|
+
cwe: 'CWE-329', pattern: /(?:iv|nonce|IV|NONCE)\s*[:=]\s*(?:['"][^'"]{8,}['"]|Buffer\.from\s*\(\s*['"])/,
|
|
46
|
+
languages: ['javascript', 'typescript', 'python', 'java', 'go'],
|
|
47
|
+
description: 'Hardcoded initialization vector.', remediation: 'Generate unique random IV per encryption operation.',
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
id: 'CRYPTO-TLS-DISABLED', name: 'TLS Verification Disabled', severity: Severity.CRITICAL, confidence: 'high' as const,
|
|
51
|
+
cwe: 'CWE-295', pattern: /(?:rejectUnauthorized\s*:\s*false|verify\s*=\s*False|InsecureSkipVerify\s*:\s*true|SSL_VERIFY_NONE|check_hostname\s*=\s*False)/,
|
|
52
|
+
languages: ['javascript', 'typescript', 'python', 'java', 'go', 'ruby'],
|
|
53
|
+
description: 'TLS certificate verification disabled.', remediation: 'Always verify TLS certificates in production.',
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
id: 'CRYPTO-WEAK-PASS-HASH', name: 'Plain Hash for Password', severity: Severity.HIGH, confidence: 'high' as const,
|
|
57
|
+
cwe: 'CWE-916', pattern: /(?:createHash\s*\(\s*['"](?:md5|sha1|sha256)['"]|hashlib\.(?:md5|sha1|sha256))\s*[(.]/,
|
|
58
|
+
antiPattern: /(?:hmac|pbkdf2|checksum|file.*hash|integrity|verify)/i,
|
|
59
|
+
languages: ['javascript', 'typescript', 'python'],
|
|
60
|
+
description: 'Plain hash used for password storage.', remediation: 'Use bcrypt, scrypt, or argon2 for password hashing.',
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
id: 'CRYPTO-SMALL-KEY', name: 'Insufficient Key Length', severity: Severity.MEDIUM, confidence: 'medium' as const,
|
|
64
|
+
cwe: 'CWE-326', pattern: /(?:generateKeyPair|RSA|keySize|modulusLength)\s*[:(]\s*(?:512|768|1024)\b/,
|
|
65
|
+
languages: ['javascript', 'typescript', 'python', 'java', 'go', 'csharp'],
|
|
66
|
+
description: 'RSA key length below 2048 bits.', remediation: 'Use at least 2048-bit RSA keys.',
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
id: 'CRYPTO-HTTP', name: 'Unencrypted HTTP', severity: Severity.MEDIUM, confidence: 'medium' as const,
|
|
70
|
+
cwe: 'CWE-319', pattern: /['"]http:\/\/(?!localhost|127\.0\.0\.1|0\.0\.0\.0|::1|example\.com)[^'"]+['"]/,
|
|
71
|
+
languages: ['javascript', 'typescript', 'python', 'java', 'go', 'php', 'ruby', 'csharp'],
|
|
72
|
+
description: 'Unencrypted HTTP URL for non-local endpoint.', remediation: 'Use HTTPS for all external communications.',
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
id: 'CRYPTO-TIMING', name: 'Timing Attack Vulnerable Comparison', severity: Severity.MEDIUM, confidence: 'low' as const,
|
|
76
|
+
cwe: 'CWE-208', pattern: /(?:===?\s*(?:password|token|secret|apiKey|hash)|(?:password|token|secret|apiKey|hash)\s*===?)/i,
|
|
77
|
+
antiPattern: /(?:timingSafe|constantTime|hmac\.compare|secrets\.compare_digest)/i,
|
|
78
|
+
languages: ['javascript', 'typescript', 'python', 'java', 'go'],
|
|
79
|
+
description: 'String comparison for secrets vulnerable to timing attacks.', remediation: 'Use crypto.timingSafeEqual() for secret comparison.',
|
|
80
|
+
},
|
|
81
|
+
];
|
|
82
|
+
|
|
83
|
+
export function detect(content: string, filePath: string, language: string): Finding[] {
|
|
84
|
+
const findings: Finding[] = [];
|
|
85
|
+
const lines = content.split('\n');
|
|
86
|
+
|
|
87
|
+
for (const pat of PATTERNS) {
|
|
88
|
+
if (!pat.languages.includes(language)) continue;
|
|
89
|
+
|
|
90
|
+
for (let i = 0; i < lines.length; i++) {
|
|
91
|
+
const line = lines[i];
|
|
92
|
+
if (pat.pattern.test(line)) {
|
|
93
|
+
if (pat.antiPattern) {
|
|
94
|
+
const cs = Math.max(0, i - 3);
|
|
95
|
+
const ce = Math.min(lines.length, i + 4);
|
|
96
|
+
const ctx = lines.slice(cs, ce).join('\n');
|
|
97
|
+
if (pat.antiPattern.test(ctx)) continue;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
findings.push({
|
|
101
|
+
id: `${pat.id}-${filePath}:${i + 1}`,
|
|
102
|
+
ruleId: pat.id,
|
|
103
|
+
title: pat.name,
|
|
104
|
+
description: pat.description,
|
|
105
|
+
severity: pat.severity,
|
|
106
|
+
confidence: pat.confidence,
|
|
107
|
+
filePath, line: i + 1,
|
|
108
|
+
codeSnippet: getSnippet(lines, i),
|
|
109
|
+
cwe: pat.cwe, owasp: 'A02',
|
|
110
|
+
remediation: pat.remediation,
|
|
111
|
+
fingerprint: generateFingerprint(pat.id, filePath, line.trim()),
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return findings;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function getSnippet(lines: string[], index: number, context = 2): string {
|
|
120
|
+
const start = Math.max(0, index - context);
|
|
121
|
+
const end = Math.min(lines.length, index + context + 1);
|
|
122
|
+
return lines.slice(start, end)
|
|
123
|
+
.map((l, i) => {
|
|
124
|
+
const lineNum = start + i + 1;
|
|
125
|
+
const marker = (start + i === index) ? '>' : ' ';
|
|
126
|
+
return `${marker} ${lineNum} | ${l}`;
|
|
127
|
+
}).join('\n');
|
|
128
|
+
}
|