redgun-security 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.
@@ -0,0 +1,67 @@
1
+ import { readFileSync, readdirSync, statSync } from 'fs';
2
+ import { join, extname } from 'path';
3
+ import { addFinding } from '../core/findings.js';
4
+ import { DESERIALIZATION_PATTERNS } from '../utils/patterns.js';
5
+
6
+ const SCAN_EXTENSIONS = ['.js', '.ts', '.py', '.rb', '.php', '.java', '.cs', '.go'];
7
+ const IGNORE_DIRS = ['node_modules', '.git', 'dist', 'build', '__pycache__', 'vendor', 'target'];
8
+
9
+ export async function auditDeserialization(projectPath, spinner) {
10
+ spinner.text = 'Scanning for insecure deserialization (HackTricks)...';
11
+ const files = getFiles(projectPath);
12
+
13
+ for (const file of files) {
14
+ try {
15
+ const content = readFileSync(file, 'utf-8');
16
+ const relativePath = file.replace(projectPath, '.');
17
+ const lines = content.split('\n');
18
+
19
+ for (const pattern of DESERIALIZATION_PATTERNS) {
20
+ const regex = new RegExp(pattern.source, pattern.flags);
21
+ let match;
22
+ while ((match = regex.exec(content)) !== null) {
23
+ const lineNum = content.substring(0, match.index).split('\n').length;
24
+ const line = lines[lineNum - 1]?.trim() || '';
25
+ addFinding(
26
+ 'CRITICAL',
27
+ 'Deserialization (HackTricks)',
28
+ 'Insecure deserialization detected',
29
+ `File: ${relativePath}:${lineNum}\nCode: ${line.substring(0, 120)}\nPattern: ${match[0].substring(0, 60)}`,
30
+ 'Never deserialize untrusted data. Use safe alternatives: JSON for data exchange, validate/whitelist classes before deserializing. For Python: use json instead of pickle. For Java: use allowlist-based ObjectInputFilter. For PHP: use json_decode instead of unserialize.'
31
+ );
32
+ }
33
+ }
34
+
35
+ const yamlUnsafe = /yaml\.load\s*\([^)]*\)/g;
36
+ let yamlMatch;
37
+ while ((yamlMatch = yamlUnsafe.exec(content)) !== null) {
38
+ if (!content.substring(yamlMatch.index, yamlMatch.index + 100).includes('SafeLoader')) {
39
+ const lineNum = content.substring(0, yamlMatch.index).split('\n').length;
40
+ addFinding(
41
+ 'HIGH',
42
+ 'Deserialization (HackTricks)',
43
+ 'YAML load without SafeLoader',
44
+ `File: ${relativePath}:${lineNum}`,
45
+ 'Use yaml.safe_load() or yaml.load(data, Loader=yaml.SafeLoader) to prevent arbitrary code execution.'
46
+ );
47
+ }
48
+ }
49
+ } catch {}
50
+ }
51
+ }
52
+
53
+ function getFiles(dir, files = []) {
54
+ try {
55
+ const entries = readdirSync(dir);
56
+ for (const entry of entries) {
57
+ if (IGNORE_DIRS.includes(entry) || entry.startsWith('.')) continue;
58
+ const fullPath = join(dir, entry);
59
+ try {
60
+ const stat = statSync(fullPath);
61
+ if (stat.isDirectory()) getFiles(fullPath, files);
62
+ else if (SCAN_EXTENSIONS.includes(extname(entry).toLowerCase()) && stat.size < 512 * 1024) files.push(fullPath);
63
+ } catch {}
64
+ }
65
+ } catch {}
66
+ return files;
67
+ }
@@ -0,0 +1,80 @@
1
+ import { readFileSync, existsSync } from 'fs';
2
+ import { join } from 'path';
3
+ import { addFinding } from '../core/findings.js';
4
+
5
+ export async function auditEnv(projectPath, spinner) {
6
+ spinner.text = 'Checking environment file security...';
7
+
8
+ const gitignorePath = join(projectPath, '.gitignore');
9
+ const hasGitignore = existsSync(gitignorePath);
10
+ let gitignoreContent = '';
11
+
12
+ if (hasGitignore) {
13
+ gitignoreContent = readFileSync(gitignorePath, 'utf-8');
14
+ }
15
+
16
+ const envFiles = ['.env', '.env.local', '.env.production', '.env.development'];
17
+
18
+ for (const envFile of envFiles) {
19
+ const envPath = join(projectPath, envFile);
20
+ if (!existsSync(envPath)) continue;
21
+
22
+ if (!hasGitignore || !gitignoreContent.includes('.env')) {
23
+ addFinding(
24
+ 'CRITICAL',
25
+ 'Environment Files',
26
+ `${envFile} may not be in .gitignore`,
27
+ `File ${envFile} exists but .gitignore does not exclude .env files`,
28
+ 'Add .env* to your .gitignore file immediately'
29
+ );
30
+ }
31
+
32
+ try {
33
+ const content = readFileSync(envPath, 'utf-8');
34
+ const lines = content.split('\n');
35
+
36
+ for (let i = 0; i < lines.length; i++) {
37
+ const line = lines[i].trim();
38
+ if (!line || line.startsWith('#')) continue;
39
+
40
+ const [key] = line.split('=');
41
+ if (!key) continue;
42
+
43
+ if (/(?:SECRET|PASSWORD|PRIVATE|TOKEN|KEY|API_KEY)(?:_|$)/i.test(key)) {
44
+ const value = line.split('=').slice(1).join('=').trim().replace(/['"]/g, '');
45
+ if (value && value !== '' && !value.startsWith('${') && value !== 'changeme' && value !== 'your-key-here') {
46
+ addFinding(
47
+ 'HIGH',
48
+ 'Environment Files',
49
+ `Real secret in ${envFile}`,
50
+ `${key} contains a real value (line ${i + 1})`,
51
+ 'Ensure this file is never committed. Use a secrets manager for production.'
52
+ );
53
+ }
54
+ }
55
+ }
56
+ } catch {}
57
+ }
58
+
59
+ const exampleEnv = join(projectPath, '.env.example');
60
+ if (existsSync(exampleEnv)) {
61
+ try {
62
+ const content = readFileSync(exampleEnv, 'utf-8');
63
+ const lines = content.split('\n');
64
+ for (let i = 0; i < lines.length; i++) {
65
+ const line = lines[i].trim();
66
+ if (!line || line.startsWith('#')) continue;
67
+ const value = line.split('=').slice(1).join('=').trim().replace(/['"]/g, '');
68
+ if (value && value.length > 20 && !/^(your|example|changeme|placeholder|xxx)/i.test(value)) {
69
+ addFinding(
70
+ 'MEDIUM',
71
+ 'Environment Files',
72
+ 'Possible real secret in .env.example',
73
+ `Line ${i + 1}: ${line.split('=')[0]}=*** (value looks real, not placeholder)`,
74
+ 'Replace with a placeholder value like "your-api-key-here"'
75
+ );
76
+ }
77
+ }
78
+ } catch {}
79
+ }
80
+ }
@@ -0,0 +1,70 @@
1
+ import { readFileSync, existsSync } from 'fs';
2
+ import { join } from 'path';
3
+ import { addFinding } from '../core/findings.js';
4
+
5
+ const CONFIG_FILES = [
6
+ 'nuxt.config.js', 'nuxt.config.ts', 'next.config.js', 'next.config.mjs',
7
+ 'vercel.json', 'netlify.toml', '_headers', 'nginx.conf',
8
+ 'server.js', 'server.ts', 'app.js', 'app.ts', 'index.js', 'index.ts',
9
+ ];
10
+
11
+ export async function auditHeadersConfig(projectPath, spinner) {
12
+ spinner.text = 'Checking security headers configuration...';
13
+
14
+ let foundCsp = false;
15
+ let foundHsts = false;
16
+ let foundXfo = false;
17
+
18
+ for (const configFile of CONFIG_FILES) {
19
+ const filePath = join(projectPath, configFile);
20
+ if (!existsSync(filePath)) continue;
21
+
22
+ try {
23
+ const content = readFileSync(filePath, 'utf-8');
24
+
25
+ if (/content-security-policy|contentSecurityPolicy|csp/i.test(content)) foundCsp = true;
26
+ if (/strict-transport-security|hsts/i.test(content)) foundHsts = true;
27
+ if (/x-frame-options|frameOptions/i.test(content)) foundXfo = true;
28
+
29
+ if (/unsafe-inline|unsafe-eval/i.test(content)) {
30
+ addFinding(
31
+ 'MEDIUM',
32
+ 'Headers Config',
33
+ 'CSP uses unsafe-inline or unsafe-eval',
34
+ `File: ${configFile}`,
35
+ 'Remove unsafe-inline/unsafe-eval from CSP. Use nonce-based or hash-based CSP instead.'
36
+ );
37
+ }
38
+ } catch {}
39
+ }
40
+
41
+ if (!foundCsp) {
42
+ addFinding(
43
+ 'MEDIUM',
44
+ 'Headers Config',
45
+ 'No Content-Security-Policy configured',
46
+ 'CSP header not found in configuration files',
47
+ 'Add a strict CSP header to prevent XSS and data injection attacks'
48
+ );
49
+ }
50
+
51
+ if (!foundHsts) {
52
+ addFinding(
53
+ 'MEDIUM',
54
+ 'Headers Config',
55
+ 'No Strict-Transport-Security configured',
56
+ 'HSTS header not found in configuration files',
57
+ 'Add HSTS header with max-age=31536000; includeSubDomains; preload'
58
+ );
59
+ }
60
+
61
+ if (!foundXfo) {
62
+ addFinding(
63
+ 'LOW',
64
+ 'Headers Config',
65
+ 'No X-Frame-Options configured',
66
+ 'X-Frame-Options not found in configuration',
67
+ 'Add X-Frame-Options: DENY or use CSP frame-ancestors directive'
68
+ );
69
+ }
70
+ }
@@ -0,0 +1,46 @@
1
+ import { auditSecrets } from './secrets.js';
2
+ import { auditEnv } from './env.js';
3
+ import { auditDependencies } from './dependencies.js';
4
+ import { auditCodeVulnerabilities } from './code-vulnerabilities.js';
5
+ import { auditAuth } from './auth.js';
6
+ import { auditHeadersConfig } from './headers-config.js';
7
+ import { auditSsrf } from './ssrf.js';
8
+ import { auditSsti } from './ssti.js';
9
+ import { auditDeserialization } from './deserialization.js';
10
+ import { auditPrototypePollution } from './prototype-pollution.js';
11
+ import { auditJwt } from './jwt.js';
12
+ import { auditPathTraversal } from './path-traversal.js';
13
+ import { auditCommandInjection } from './command-injection.js';
14
+ import { auditCrypto } from './crypto.js';
15
+
16
+ export const LOCAL_MODULES = [
17
+ { name: 'Code Secrets', value: 'secrets', fn: auditSecrets },
18
+ { name: 'Environment Files', value: 'env', fn: auditEnv },
19
+ { name: 'Dependencies (npm audit)', value: 'deps', fn: auditDependencies },
20
+ { name: 'Code Vulnerabilities (SQLi, XSS)', value: 'codevuln', fn: auditCodeVulnerabilities },
21
+ { name: 'Auth & Middleware', value: 'auth', fn: auditAuth },
22
+ { name: 'Headers Config (CSP/HSTS)', value: 'headers', fn: auditHeadersConfig },
23
+ { name: 'SSRF Detection (HackTricks)', value: 'ssrf', fn: auditSsrf },
24
+ { name: 'SSTI Detection (HackTricks)', value: 'ssti', fn: auditSsti },
25
+ { name: 'Insecure Deserialization (HackTricks)', value: 'deser', fn: auditDeserialization },
26
+ { name: 'Prototype Pollution (HackTricks)', value: 'proto', fn: auditPrototypePollution },
27
+ { name: 'JWT Vulnerabilities (HackTricks)', value: 'jwt', fn: auditJwt },
28
+ { name: 'Path Traversal / LFI (HackTricks)', value: 'lfi', fn: auditPathTraversal },
29
+ { name: 'Command Injection (HackTricks)', value: 'cmdi', fn: auditCommandInjection },
30
+ { name: 'Weak Cryptography (HackTricks)', value: 'crypto', fn: auditCrypto },
31
+ ];
32
+
33
+ export async function runLocalAudit(projectPath, spinner, modules = null) {
34
+ const toRun = modules
35
+ ? LOCAL_MODULES.filter((m) => modules.includes(m.value))
36
+ : LOCAL_MODULES;
37
+
38
+ for (const mod of toRun) {
39
+ try {
40
+ spinner.text = `Running: ${mod.name}...`;
41
+ await mod.fn(projectPath, spinner);
42
+ } catch (err) {
43
+ spinner.text = `Error in ${mod.name}: ${err.message}`;
44
+ }
45
+ }
46
+ }
@@ -0,0 +1,86 @@
1
+ import { readFileSync, readdirSync, statSync } from 'fs';
2
+ import { join, extname } from 'path';
3
+ import { addFinding } from '../core/findings.js';
4
+ import { JWT_PATTERNS } from '../utils/patterns.js';
5
+
6
+ const SCAN_EXTENSIONS = ['.js', '.ts', '.jsx', '.tsx', '.mjs', '.py', '.rb', '.php', '.go', '.java'];
7
+ const IGNORE_DIRS = ['node_modules', '.git', 'dist', 'build', '__pycache__', 'vendor'];
8
+
9
+ export async function auditJwt(projectPath, spinner) {
10
+ spinner.text = 'Scanning for JWT vulnerabilities (HackTricks)...';
11
+ const files = getFiles(projectPath);
12
+
13
+ for (const file of files) {
14
+ try {
15
+ const content = readFileSync(file, 'utf-8');
16
+ const relativePath = file.replace(projectPath, '.');
17
+ const lines = content.split('\n');
18
+
19
+ for (const pattern of JWT_PATTERNS) {
20
+ const regex = new RegExp(pattern.source, pattern.flags);
21
+ let match;
22
+ while ((match = regex.exec(content)) !== null) {
23
+ const lineNum = content.substring(0, match.index).split('\n').length;
24
+ const matchText = match[0];
25
+
26
+ let severity = 'HIGH';
27
+ let title = 'JWT security issue';
28
+
29
+ if (/none/i.test(matchText)) {
30
+ severity = 'CRITICAL';
31
+ title = 'JWT algorithm "none" attack - signature bypass';
32
+ } else if (/verify.*false/i.test(matchText)) {
33
+ severity = 'CRITICAL';
34
+ title = 'JWT verification disabled';
35
+ } else if (/ignoreExpiration.*true/i.test(matchText)) {
36
+ severity = 'HIGH';
37
+ title = 'JWT expiration check disabled';
38
+ } else if (/jwt\.decode/i.test(matchText)) {
39
+ severity = 'MEDIUM';
40
+ title = 'JWT decoded without verification';
41
+ } else if (/HS256/i.test(matchText)) {
42
+ severity = 'MEDIUM';
43
+ title = 'JWT uses HS256 - vulnerable to key confusion if RS256 expected';
44
+ }
45
+
46
+ addFinding(
47
+ severity,
48
+ 'JWT Attacks (HackTricks)',
49
+ title,
50
+ `File: ${relativePath}:${lineNum}\nCode: ${lines[lineNum - 1]?.trim().substring(0, 120)}`,
51
+ 'Always verify JWT signatures. Use RS256/ES256 for asymmetric signing. Never use algorithm "none". Set and enforce expiration. Use a well-tested JWT library.'
52
+ );
53
+ }
54
+ }
55
+
56
+ const weakSecret = /(?:jwt|token).*secret\s*[:=]\s*['"][^'"]{1,16}['"]/gi;
57
+ let secretMatch;
58
+ while ((secretMatch = weakSecret.exec(content)) !== null) {
59
+ const lineNum = content.substring(0, secretMatch.index).split('\n').length;
60
+ addFinding(
61
+ 'HIGH',
62
+ 'JWT Attacks (HackTricks)',
63
+ 'Weak JWT signing secret (< 16 chars)',
64
+ `File: ${relativePath}:${lineNum}`,
65
+ 'Use a strong secret (256+ bits). Store in environment variable. Consider asymmetric signing (RS256).'
66
+ );
67
+ }
68
+ } catch {}
69
+ }
70
+ }
71
+
72
+ function getFiles(dir, files = []) {
73
+ try {
74
+ const entries = readdirSync(dir);
75
+ for (const entry of entries) {
76
+ if (IGNORE_DIRS.includes(entry) || entry.startsWith('.')) continue;
77
+ const fullPath = join(dir, entry);
78
+ try {
79
+ const stat = statSync(fullPath);
80
+ if (stat.isDirectory()) getFiles(fullPath, files);
81
+ else if (SCAN_EXTENSIONS.includes(extname(entry).toLowerCase()) && stat.size < 512 * 1024) files.push(fullPath);
82
+ } catch {}
83
+ }
84
+ } catch {}
85
+ return files;
86
+ }
@@ -0,0 +1,71 @@
1
+ import { readFileSync, readdirSync, statSync } from 'fs';
2
+ import { join, extname } from 'path';
3
+ import { addFinding } from '../core/findings.js';
4
+ import { PATH_TRAVERSAL_PATTERNS } from '../utils/patterns.js';
5
+
6
+ const SCAN_EXTENSIONS = ['.js', '.ts', '.jsx', '.tsx', '.mjs', '.py', '.rb', '.php', '.go', '.java'];
7
+ const IGNORE_DIRS = ['node_modules', '.git', 'dist', 'build', '__pycache__', 'vendor'];
8
+
9
+ export async function auditPathTraversal(projectPath, spinner) {
10
+ spinner.text = 'Scanning for Path Traversal / LFI (HackTricks)...';
11
+ const files = getFiles(projectPath);
12
+
13
+ for (const file of files) {
14
+ try {
15
+ const content = readFileSync(file, 'utf-8');
16
+ const relativePath = file.replace(projectPath, '.');
17
+ const lines = content.split('\n');
18
+
19
+ for (const pattern of PATH_TRAVERSAL_PATTERNS) {
20
+ const regex = new RegExp(pattern.source, pattern.flags);
21
+ let match;
22
+ while ((match = regex.exec(content)) !== null) {
23
+ const lineNum = content.substring(0, match.index).split('\n').length;
24
+ addFinding(
25
+ 'CRITICAL',
26
+ 'Path Traversal (HackTricks)',
27
+ 'Local File Inclusion - user input in file path',
28
+ `File: ${relativePath}:${lineNum}\nCode: ${lines[lineNum - 1]?.trim().substring(0, 120)}`,
29
+ 'Never use user input directly in file paths. Use path.resolve() and verify the resolved path starts with the expected base directory. Strip ../ sequences. Use a whitelist of allowed filenames when possible.'
30
+ );
31
+ }
32
+ }
33
+
34
+ const includePatterns = [
35
+ /(?:include|require|require_once|include_once)\s*\(\s*\$_(?:GET|POST|REQUEST)/gi,
36
+ /(?:file_get_contents|fopen|readfile)\s*\(\s*\$_(?:GET|POST|REQUEST)/gi,
37
+ /open\s*\(\s*(?:request\.args|request\.form)/gi,
38
+ ];
39
+
40
+ for (const pattern of includePatterns) {
41
+ let match;
42
+ while ((match = pattern.exec(content)) !== null) {
43
+ const lineNum = content.substring(0, match.index).split('\n').length;
44
+ addFinding(
45
+ 'CRITICAL',
46
+ 'Path Traversal (HackTricks)',
47
+ 'Remote/Local File Inclusion via user input',
48
+ `File: ${relativePath}:${lineNum}`,
49
+ 'Never include files based on user input. If needed, use a strict whitelist mapping.'
50
+ );
51
+ }
52
+ }
53
+ } catch {}
54
+ }
55
+ }
56
+
57
+ function getFiles(dir, files = []) {
58
+ try {
59
+ const entries = readdirSync(dir);
60
+ for (const entry of entries) {
61
+ if (IGNORE_DIRS.includes(entry) || entry.startsWith('.')) continue;
62
+ const fullPath = join(dir, entry);
63
+ try {
64
+ const stat = statSync(fullPath);
65
+ if (stat.isDirectory()) getFiles(fullPath, files);
66
+ else if (SCAN_EXTENSIONS.includes(extname(entry).toLowerCase()) && stat.size < 512 * 1024) files.push(fullPath);
67
+ } catch {}
68
+ }
69
+ } catch {}
70
+ return files;
71
+ }
@@ -0,0 +1,66 @@
1
+ import { readFileSync, readdirSync, statSync } from 'fs';
2
+ import { join, extname } from 'path';
3
+ import { addFinding } from '../core/findings.js';
4
+ import { PROTOTYPE_POLLUTION_PATTERNS } from '../utils/patterns.js';
5
+
6
+ const SCAN_EXTENSIONS = ['.js', '.ts', '.jsx', '.tsx', '.mjs'];
7
+ const IGNORE_DIRS = ['node_modules', '.git', 'dist', 'build', '.next'];
8
+
9
+ export async function auditPrototypePollution(projectPath, spinner) {
10
+ spinner.text = 'Scanning for Prototype Pollution (HackTricks)...';
11
+ const files = getFiles(projectPath);
12
+
13
+ for (const file of files) {
14
+ try {
15
+ const content = readFileSync(file, 'utf-8');
16
+ const relativePath = file.replace(projectPath, '.');
17
+ const lines = content.split('\n');
18
+
19
+ for (const pattern of PROTOTYPE_POLLUTION_PATTERNS) {
20
+ const regex = new RegExp(pattern.source, pattern.flags);
21
+ let match;
22
+ while ((match = regex.exec(content)) !== null) {
23
+ const lineNum = content.substring(0, match.index).split('\n').length;
24
+ addFinding(
25
+ 'HIGH',
26
+ 'Prototype Pollution (HackTricks)',
27
+ 'Potential prototype pollution vector',
28
+ `File: ${relativePath}:${lineNum}\nCode: ${lines[lineNum - 1]?.trim().substring(0, 120)}`,
29
+ 'Sanitize object keys before merging. Block __proto__, constructor, prototype keys. Use Object.create(null) for dictionary objects. Freeze Object.prototype in critical paths.'
30
+ );
31
+ }
32
+ }
33
+
34
+ const recursiveMerge = /function\s+\w*[Mm]erge\s*\([^)]*\)\s*\{[^}]*for\s*\(/g;
35
+ let mergeMatch;
36
+ while ((mergeMatch = recursiveMerge.exec(content)) !== null) {
37
+ const lineNum = content.substring(0, mergeMatch.index).split('\n').length;
38
+ if (!content.substring(mergeMatch.index, mergeMatch.index + 500).includes('__proto__')) {
39
+ addFinding(
40
+ 'MEDIUM',
41
+ 'Prototype Pollution (HackTricks)',
42
+ 'Custom merge function without __proto__ protection',
43
+ `File: ${relativePath}:${lineNum}`,
44
+ 'Add __proto__ and constructor.prototype key filtering to custom merge/deep-clone functions.'
45
+ );
46
+ }
47
+ }
48
+ } catch {}
49
+ }
50
+ }
51
+
52
+ function getFiles(dir, files = []) {
53
+ try {
54
+ const entries = readdirSync(dir);
55
+ for (const entry of entries) {
56
+ if (IGNORE_DIRS.includes(entry) || entry.startsWith('.')) continue;
57
+ const fullPath = join(dir, entry);
58
+ try {
59
+ const stat = statSync(fullPath);
60
+ if (stat.isDirectory()) getFiles(fullPath, files);
61
+ else if (SCAN_EXTENSIONS.includes(extname(entry).toLowerCase()) && stat.size < 512 * 1024) files.push(fullPath);
62
+ } catch {}
63
+ }
64
+ } catch {}
65
+ return files;
66
+ }
@@ -0,0 +1,92 @@
1
+ import { readFileSync, readdirSync, statSync } from 'fs';
2
+ import { join, extname } from 'path';
3
+ import { addFinding } from '../core/findings.js';
4
+ import { SECRET_PATTERNS } from '../utils/patterns.js';
5
+
6
+ const SCAN_EXTENSIONS = [
7
+ '.js', '.ts', '.jsx', '.tsx', '.mjs', '.cjs',
8
+ '.py', '.rb', '.php', '.go', '.java', '.cs',
9
+ '.vue', '.svelte', '.astro', '.html', '.yml', '.yaml',
10
+ '.json', '.toml', '.cfg', '.conf', '.ini',
11
+ ];
12
+
13
+ const IGNORE_DIRS = [
14
+ 'node_modules', '.git', 'dist', 'build', '.next', '.nuxt',
15
+ '__pycache__', 'venv', '.venv', 'vendor', 'target',
16
+ '.cache', 'coverage', '.output',
17
+ ];
18
+
19
+ export async function auditSecrets(projectPath, spinner) {
20
+ spinner.text = 'Scanning for hardcoded secrets...';
21
+ const files = getFiles(projectPath);
22
+
23
+ for (const file of files) {
24
+ try {
25
+ const content = readFileSync(file, 'utf-8');
26
+ const lines = content.split('\n');
27
+
28
+ for (const [name, pattern] of Object.entries(SECRET_PATTERNS)) {
29
+ const regex = new RegExp(pattern.source, pattern.flags);
30
+ let match;
31
+ while ((match = regex.exec(content)) !== null) {
32
+ const lineNum = content.substring(0, match.index).split('\n').length;
33
+ const line = lines[lineNum - 1]?.trim() || '';
34
+
35
+ if (isComment(line)) continue;
36
+ if (isTestOrExample(file)) continue;
37
+
38
+ const severity = getSeverity(name);
39
+ const relativePath = file.replace(projectPath, '.');
40
+ addFinding(
41
+ severity,
42
+ 'Code Secrets',
43
+ `${name} found in source code`,
44
+ `File: ${relativePath}:${lineNum}\nValue: ${maskSecret(match[0])}`,
45
+ `Move this secret to environment variables. Use .env files (not committed) and access via process.env.`
46
+ );
47
+ }
48
+ }
49
+ } catch {}
50
+ }
51
+ }
52
+
53
+ function getFiles(dir, files = []) {
54
+ try {
55
+ const entries = readdirSync(dir);
56
+ for (const entry of entries) {
57
+ if (IGNORE_DIRS.includes(entry)) continue;
58
+ if (entry.startsWith('.') && entry !== '.env.example') continue;
59
+
60
+ const fullPath = join(dir, entry);
61
+ try {
62
+ const stat = statSync(fullPath);
63
+ if (stat.isDirectory()) {
64
+ getFiles(fullPath, files);
65
+ } else if (SCAN_EXTENSIONS.includes(extname(entry).toLowerCase())) {
66
+ if (stat.size < 1024 * 1024) files.push(fullPath);
67
+ }
68
+ } catch {}
69
+ }
70
+ } catch {}
71
+ return files;
72
+ }
73
+
74
+ function isComment(line) {
75
+ return line.startsWith('//') || line.startsWith('#') || line.startsWith('*') || line.startsWith('/*');
76
+ }
77
+
78
+ function isTestOrExample(file) {
79
+ return /\.(test|spec|example|sample|mock)\./i.test(file) || /__(tests?|mocks?)__/i.test(file);
80
+ }
81
+
82
+ function getSeverity(name) {
83
+ if (/private key|service.role|secret.key|aws.secret/i.test(name)) return 'CRITICAL';
84
+ if (/stripe|database|openai|anthropic/i.test(name)) return 'HIGH';
85
+ if (/password|token/i.test(name)) return 'HIGH';
86
+ return 'MEDIUM';
87
+ }
88
+
89
+ function maskSecret(value) {
90
+ if (value.length <= 8) return '***';
91
+ return value.substring(0, 4) + '...' + value.substring(value.length - 4);
92
+ }
@@ -0,0 +1,70 @@
1
+ import { readFileSync, readdirSync, statSync } from 'fs';
2
+ import { join, extname } from 'path';
3
+ import { addFinding } from '../core/findings.js';
4
+ import { SSRF_PATTERNS } from '../utils/patterns.js';
5
+
6
+ const SCAN_EXTENSIONS = ['.js', '.ts', '.jsx', '.tsx', '.mjs', '.py', '.rb', '.php', '.go', '.java'];
7
+ const IGNORE_DIRS = ['node_modules', '.git', 'dist', 'build', '.next', '__pycache__', 'vendor'];
8
+
9
+ export async function auditSsrf(projectPath, spinner) {
10
+ spinner.text = 'Scanning for SSRF vulnerabilities (HackTricks)...';
11
+ const files = getFiles(projectPath);
12
+
13
+ for (const file of files) {
14
+ try {
15
+ const content = readFileSync(file, 'utf-8');
16
+ const relativePath = file.replace(projectPath, '.');
17
+ const lines = content.split('\n');
18
+
19
+ for (const pattern of SSRF_PATTERNS) {
20
+ const regex = new RegExp(pattern.source, pattern.flags);
21
+ let match;
22
+ while ((match = regex.exec(content)) !== null) {
23
+ const lineNum = content.substring(0, match.index).split('\n').length;
24
+ addFinding(
25
+ 'CRITICAL',
26
+ 'SSRF (HackTricks)',
27
+ 'Server-Side Request Forgery - user-controlled URL',
28
+ `File: ${relativePath}:${lineNum}\nCode: ${lines[lineNum - 1]?.trim().substring(0, 120)}`,
29
+ 'Validate and whitelist allowed URLs/domains. Block internal IPs (127.0.0.1, 10.x, 169.254.169.254). Use URL parsing to prevent bypasses like http://127.0.0.1@evil.com'
30
+ );
31
+ }
32
+ }
33
+
34
+ const urlFetchPatterns = [
35
+ /(?:url|uri|link|href|src|endpoint|webhook|callback|redirect|proxy|forward)\s*=\s*(?:req|params|query|body)/gi,
36
+ /(?:image|avatar|icon|logo|file)_?(?:url|uri)\s*=\s*(?:req|params|query|body)/gi,
37
+ ];
38
+
39
+ for (const pattern of urlFetchPatterns) {
40
+ let match;
41
+ while ((match = pattern.exec(content)) !== null) {
42
+ const lineNum = content.substring(0, match.index).split('\n').length;
43
+ addFinding(
44
+ 'HIGH',
45
+ 'SSRF (HackTricks)',
46
+ 'User-controlled URL parameter (potential SSRF)',
47
+ `File: ${relativePath}:${lineNum}\nPattern: ${match[0].substring(0, 80)}`,
48
+ 'Implement URL validation: whitelist allowed protocols (http/https only), block private IP ranges, use DNS resolution checks, consider using a proxy with egress filtering.'
49
+ );
50
+ }
51
+ }
52
+ } catch {}
53
+ }
54
+ }
55
+
56
+ function getFiles(dir, files = []) {
57
+ try {
58
+ const entries = readdirSync(dir);
59
+ for (const entry of entries) {
60
+ if (IGNORE_DIRS.includes(entry) || entry.startsWith('.')) continue;
61
+ const fullPath = join(dir, entry);
62
+ try {
63
+ const stat = statSync(fullPath);
64
+ if (stat.isDirectory()) getFiles(fullPath, files);
65
+ else if (SCAN_EXTENSIONS.includes(extname(entry).toLowerCase()) && stat.size < 512 * 1024) files.push(fullPath);
66
+ } catch {}
67
+ }
68
+ } catch {}
69
+ return files;
70
+ }