redgun-security 1.0.0 → 1.2.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,75 @@
1
+ import { readFileSync, readdirSync, statSync } from 'fs';
2
+ import { join, extname } from 'path';
3
+ import { addFinding } from '../core/findings.js';
4
+
5
+ const SCAN_EXTENSIONS = ['.js', '.ts', '.jsx', '.tsx', '.py', '.rb', '.php', '.go', '.java'];
6
+ const IGNORE_DIRS = ['node_modules', '.git', 'dist', 'build', '__pycache__', 'vendor'];
7
+
8
+ export async function auditCsrf(projectPath, spinner) {
9
+ spinner.text = 'Analyzing CSRF token implementation...';
10
+ const files = getFiles(projectPath);
11
+ let hasCsrfLib = false;
12
+ let hasSameSite = false;
13
+
14
+ for (const file of files) {
15
+ try {
16
+ const content = readFileSync(file, 'utf-8');
17
+ const relativePath = file.replace(projectPath, '.');
18
+ const lines = content.split('\n');
19
+
20
+ if (/csurf|csrf|lusca|csrf-token|csrf_token/i.test(content)) hasCsrfLib = true;
21
+ if (/sameSite\s*[:=]\s*['"](?:strict|lax)['"]/i.test(content)) hasSameSite = true;
22
+
23
+ const patterns = [
24
+ { pattern: /(?:csrf|_token).*\s*=\s*(?:Math\.random|Date\.now|uuid)/gi, name: 'CSRF token from predictable source', severity: 'HIGH' },
25
+ { pattern: /(?:csrf|token).*\s*=\s*['"][a-zA-Z0-9]{1,8}['"]/gi, name: 'Short CSRF token (< 8 chars, brute-forceable)', severity: 'HIGH' },
26
+ { pattern: /csrf\s*=\s*(?:null|undefined|false|skip|disabled?)/gi, name: 'CSRF protection disabled', severity: 'HIGH' },
27
+ { pattern: /(?:bypass|ignore|skip).*csrf/gi, name: 'CSRF bypass condition', severity: 'MEDIUM' },
28
+ { pattern: /csrf\s*\(\)\s*:\s*(?:false|disabled)/gi, name: 'CSRF middleware disabled', severity: 'HIGH' },
29
+ { pattern: /sameSite\s*[:=]\s*['"]none['"]/gi, name: 'SameSite=None with Secure flag?', severity: 'LOW' },
30
+ { pattern: /X-CSRF-Token|X-CSRFToken|X-XSRF-TOKEN/gi, name: 'CSRF token in custom header (SOP-safe)', severity: 'INFO' },
31
+ { pattern: /csrf\s*:\s*false/gi, name: 'CSRF explicitly disabled', severity: 'CRITICAL' },
32
+ { pattern: /csrf\s*\(\s*\)\s*\{[^}]*ignore:/gi, name: 'CSRF with ignore list', severity: 'MEDIUM' },
33
+ { pattern: /csrf.*ignoreMethods\s*:\s*\[['"]GET['"]\s*,\s*['"]HEAD['"]/gi, name: 'CSRF ignores GET/HEAD (standard)', severity: 'INFO' },
34
+ ];
35
+
36
+ for (const { pattern, name, severity } of patterns) {
37
+ let match;
38
+ while ((match = pattern.exec(content)) !== null) {
39
+ const lineNum = content.substring(0, match.index).split('\n').length;
40
+ addFinding(
41
+ severity,
42
+ 'CSRF Analysis',
43
+ name,
44
+ `File: ${relativePath}:${lineNum}\nCode: ${lines[lineNum - 1]?.trim().substring(0, 100)}`,
45
+ 'Use cryptographically secure random CSRF tokens (64+ bits). Use SameSite=Lax or Strict cookies. Send CSRF tokens in custom headers (not cookies). Use Double Submit Cookie pattern: match cookie & header token values.'
46
+ );
47
+ }
48
+ }
49
+ } catch {}
50
+ }
51
+
52
+ if (!hasCsrfLib) {
53
+ addFinding('HIGH', 'CSRF Analysis', 'No CSRF library detected', 'No CSRF protection package (csurf, csrf, lusca) found in code', 'Install csurf/lusca and add CSRF middleware to all state-changing routes');
54
+ }
55
+
56
+ if (!hasSameSite) {
57
+ addFinding('LOW', 'CSRF Analysis', 'No SameSite cookie attribute configured', 'SameSite not set on session cookies', 'Set SameSite=Lax or SameSite=Strict on session cookies');
58
+ }
59
+ }
60
+
61
+ function getFiles(dir, files = []) {
62
+ try {
63
+ const entries = readdirSync(dir);
64
+ for (const entry of entries) {
65
+ if (IGNORE_DIRS.includes(entry) || entry.startsWith('.')) continue;
66
+ const fullPath = join(dir, entry);
67
+ try {
68
+ const stat = statSync(fullPath);
69
+ if (stat.isDirectory()) getFiles(fullPath, files);
70
+ else if (SCAN_EXTENSIONS.includes(extname(entry).toLowerCase()) && stat.size < 512 * 1024) files.push(fullPath);
71
+ } catch {}
72
+ }
73
+ } catch {}
74
+ return files;
75
+ }
@@ -12,6 +12,18 @@ import { auditJwt } from './jwt.js';
12
12
  import { auditPathTraversal } from './path-traversal.js';
13
13
  import { auditCommandInjection } from './command-injection.js';
14
14
  import { auditCrypto } from './crypto.js';
15
+ import { auditXxe } from './xxe.js';
16
+ import { auditAccessControl } from './access-control.js';
17
+ import { auditOauth } from './oauth.js';
18
+ import { auditBusinessLogic } from './business-logic.js';
19
+ import { auditSaml } from './saml.js';
20
+ import { auditLdap } from './ldap.js';
21
+ import { auditCsrf } from './csrf.js';
22
+ import { auditAto } from './ato.js';
23
+ import { auditCloud } from './cloud.js';
24
+ import { auditCicd } from './cicd.js';
25
+ import { auditMobile } from './mobile.js';
26
+ import { auditWeb3 } from './web3.js';
15
27
 
16
28
  export const LOCAL_MODULES = [
17
29
  { name: 'Code Secrets', value: 'secrets', fn: auditSecrets },
@@ -20,14 +32,26 @@ export const LOCAL_MODULES = [
20
32
  { name: 'Code Vulnerabilities (SQLi, XSS)', value: 'codevuln', fn: auditCodeVulnerabilities },
21
33
  { name: 'Auth & Middleware', value: 'auth', fn: auditAuth },
22
34
  { 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 },
35
+ { name: 'SSRF Detection', value: 'ssrf', fn: auditSsrf },
36
+ { name: 'SSTI Detection', value: 'ssti', fn: auditSsti },
37
+ { name: 'Insecure Deserialization', value: 'deser', fn: auditDeserialization },
38
+ { name: 'Prototype Pollution', value: 'proto', fn: auditPrototypePollution },
39
+ { name: 'JWT Vulnerabilities', value: 'jwt', fn: auditJwt },
40
+ { name: 'Path Traversal / LFI', value: 'lfi', fn: auditPathTraversal },
41
+ { name: 'Command Injection', value: 'cmdi', fn: auditCommandInjection },
42
+ { name: 'Weak Cryptography', value: 'crypto', fn: auditCrypto },
43
+ { name: 'XXE - XML External Entity (PortSwigger)', value: 'xxe', fn: auditXxe },
44
+ { name: 'Access Control / IDOR (PortSwigger)', value: 'idor', fn: auditAccessControl },
45
+ { name: 'OAuth / OIDC Flaws (PortSwigger)', value: 'oauth', fn: auditOauth },
46
+ { name: 'Business Logic Flaws (PortSwigger)', value: 'bizlogic', fn: auditBusinessLogic },
47
+ { name: 'SAML / SSO Attacks', value: 'saml', fn: auditSaml },
48
+ { name: 'LDAP Injection', value: 'ldap', fn: auditLdap },
49
+ { name: 'CSRF Token Analysis', value: 'csrf', fn: auditCsrf },
50
+ { name: 'Account Takeover (ATO)', value: 'ato', fn: auditAto },
51
+ { name: 'Cloud Misconfig (S3/IAM)', value: 'cloud', fn: auditCloud },
52
+ { name: 'CI/CD Pipeline', value: 'cicd', fn: auditCicd },
53
+ { name: 'Mobile Security', value: 'mobile', fn: auditMobile },
54
+ { name: 'Web3 / Smart Contracts', value: 'web3', fn: auditWeb3 },
31
55
  ];
32
56
 
33
57
  export async function runLocalAudit(projectPath, spinner, modules = null) {
@@ -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
+
5
+ const SCAN_EXTENSIONS = ['.js', '.ts', '.jsx', '.tsx', '.py', '.rb', '.php', '.java', '.go', '.cs'];
6
+ const IGNORE_DIRS = ['node_modules', '.git', 'dist', 'build', '__pycache__', 'vendor'];
7
+
8
+ export async function auditLdap(projectPath, spinner) {
9
+ spinner.text = 'Scanning for LDAP injection...';
10
+ const files = getFiles(projectPath);
11
+
12
+ for (const file of files) {
13
+ try {
14
+ const content = readFileSync(file, 'utf-8');
15
+ const relativePath = file.replace(projectPath, '.');
16
+ const lines = content.split('\n');
17
+
18
+ const patterns = [
19
+ { pattern: /ldap\.(?:search|query|compare|modify)\s*\(\s*[^,]*,\s*(?:req|request|params|query|body|user|input|data)/gi, name: 'LDAP query with user input', severity: 'CRITICAL' },
20
+ { pattern: /LDAP.*(?:filter|query)\s*[:=]\s*(?:req|request|params|query|body|user)\s*\+/gi, name: 'LDAP filter concatenation with user input', severity: 'CRITICAL' },
21
+ { pattern: /ldap\.search\s*\(\s*[^,]*,\s*['"`][^'"`]*\$\{/gi, name: 'LDAP search with template literal user input', severity: 'CRITICAL' },
22
+ { pattern: /ldap\.search\s*\(\s*[^,]*,\s*['"`][^'"`]*\+/gi, name: 'LDAP search with concatenated input', severity: 'CRITICAL' },
23
+ { pattern: /(?:authenticate|ldap_auth|ldapauth|active.?directory)\s*\(\s*(?:req|request|params|query|body)/gi, name: 'LDAP auth with user-controlled filter', severity: 'CRITICAL' },
24
+ { pattern: /ldap\.escape\s*\(\s*\)|ldap\.escapeFilter/gi, name: 'LDAP escaping used (good practice)', severity: 'INFO' },
25
+ { pattern: /(?:ldap|AD|active.?directory).*(?:filter|query|search).*['"`]\s*\+/gi, name: 'LDAP filter concatenation detected', severity: 'HIGH' },
26
+ { pattern: /(?:uid|cn|mail|samaccountname)\s*=\s*(?:req|request|params|query|body)/gi, name: 'LDAP attribute from user input', severity: 'HIGH' },
27
+ { pattern: /ActiveDirectory\s*\(\s*\{/gi, name: 'Active Directory configuration', severity: 'INFO' },
28
+ { pattern: /ldapjs|passport-ldap|ldapts|node-ldap/gi, name: 'LDAP library usage', severity: 'INFO' },
29
+ { pattern: /(?:ldap|AD|active.?directory).*(?:URI|URL|host|server)\s*[:=]\s*['"][^'"]*['"]/gi, name: 'LDAP server config hardcoded', severity: 'LOW' },
30
+ { pattern: /(?:ssl|tls|starttls|ldaps).*(?:false|disabled?|no|none)/gi, name: 'LDAP without TLS/SSL', severity: 'HIGH' },
31
+ ];
32
+
33
+ for (const { pattern, name, severity } of patterns) {
34
+ let match;
35
+ while ((match = pattern.exec(content)) !== null) {
36
+ const lineNum = content.substring(0, match.index).split('\n').length;
37
+ const line = lines[lineNum - 1]?.trim() || '';
38
+ if (line.startsWith('//') || line.startsWith('#') || line.startsWith('*')) continue;
39
+
40
+ addFinding(
41
+ severity,
42
+ 'LDAP Injection',
43
+ name,
44
+ `File: ${relativePath}:${lineNum}\nCode: ${line.substring(0, 120)}`,
45
+ 'Never concatenate user input into LDAP filters. Use parameterized LDAP queries or a safe LDAP query builder. Escape special characters (*, (, ), \\, /, &, |, !, =, <, >, ~, #) with ldap.escapeFilter(). Use LDAPS (LDAP over TLS) instead of plain LDAP.'
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,71 @@
1
+ import { readFileSync, readdirSync, statSync } from 'fs';
2
+ import { join, extname } from 'path';
3
+ import { addFinding } from '../core/findings.js';
4
+
5
+ const SCAN_EXTENSIONS = ['.js', '.ts', '.jsx', '.tsx', '.mjs', '.xml', '.kt', '.swift', '.dart', '.json', '.plist'];
6
+ const IGNORE_DIRS = ['node_modules', '.git', 'dist', 'build', '__pycache__', 'vendor', 'android', 'ios'];
7
+
8
+ export async function auditMobile(projectPath, spinner) {
9
+ spinner.text = 'Scanning for mobile security issues...';
10
+ const files = getFiles(projectPath);
11
+
12
+ for (const file of files) {
13
+ try {
14
+ const content = readFileSync(file, 'utf-8');
15
+ const relativePath = file.replace(projectPath, '.');
16
+ const lines = content.split('\n');
17
+
18
+ const patterns = [
19
+ { pattern: /(?:firebase|supabase|API|SECRET|TOKEN|KEY).*\s*=\s*['"][A-Za-z0-9_\-\+=\/]{20,}['"]/gi, name: 'API key/secret exposed in mobile code', severity: 'CRITICAL' },
20
+ { pattern: /android:allowBackup\s*=\s*"true"/gi, name: 'Android allowBackup=true (data extraction)', severity: 'MEDIUM' },
21
+ { pattern: /android:debuggable\s*=\s*"true"/gi, name: 'Android debuggable=true in release', severity: 'HIGH' },
22
+ { pattern: /android:usesCleartextTraffic\s*=\s*"true"/gi, name: 'Android cleartext (HTTP) traffic allowed', severity: 'HIGH' },
23
+ { pattern: /NSAppTransportSecurity.*NSAllowsArbitraryLoads/gi, name: 'iOS ATS disabled (HTTP allowed)', severity: 'HIGH' },
24
+ { pattern: /android:networkSecurityConfig/gi, name: 'Android custom network security config', severity: 'MEDIUM' },
25
+ { pattern: /(?:NSAllowsLocalNetworking|NSTemporaryExceptionAllowsInsecureHTTPLoads)/gi, name: 'iOS ATS exceptions enabled', severity: 'MEDIUM' },
26
+ { pattern: /android:exported\s*=\s*"true"/gi, name: 'Android component exported (IPC risk)', severity: 'MEDIUM' },
27
+ { pattern: /android:protectionLevel\s*=\s*"normal"/gi, name: 'Android permission protectionLevel normal', severity: 'LOW' },
28
+ { pattern: /deeplink|intent-filter.*data.*android:scheme/gi, name: 'Deep link / intent filter configured', severity: 'MEDIUM' },
29
+ { pattern: /(?:React\.Native|flutter|ionic|cordova|capacitor)/gi, name: 'Cross-platform framework usage', severity: 'INFO' },
30
+ { pattern: /(?:AsyncStorage|SharedPreferences|NSUserDefaults|Keychain|Keystore)/gi, name: 'Local storage usage (check encryption)', severity: 'INFO' },
31
+ { pattern: /ssl.*pinning|certificate.*pinning|SSLPinningPlugin|TrustKit|certificatePinner/gi, name: 'Certificate pinning implemented', severity: 'INFO' },
32
+ { pattern: /firebase\.io\.com|google-services\.json|GoogleService-Info\.plist/gi, name: 'Firebase config file referenced', severity: 'MEDIUM' },
33
+ { pattern: /(?:root|jailbreak).*detect|isRooted|isJailbroken|rootbeer|safety.?net/gi, name: 'Root/jailbreak detection', severity: 'INFO' },
34
+ { pattern: /(?:WebView|WKWebView).*(?:setJavaScriptEnabled|javaScriptEnabled)/gi, name: 'WebView JS enabled (XSS surface)', severity: 'MEDIUM' },
35
+ ];
36
+
37
+ for (const { pattern, name, severity } of patterns) {
38
+ let match;
39
+ while ((match = pattern.exec(content)) !== null) {
40
+ const lineNum = content.substring(0, match.index).split('\n').length;
41
+ const line = lines[lineNum - 1]?.trim() || '';
42
+ if (line.startsWith('//') || line.startsWith('#') || line.startsWith('*') || line.startsWith('<!--')) continue;
43
+
44
+ addFinding(
45
+ severity,
46
+ 'Mobile Security',
47
+ name,
48
+ `File: ${relativePath}:${lineNum}\nCode: ${line.substring(0, 120)}`,
49
+ 'Never hardcode API keys/secrets in mobile apps. Use server-side proxying. Enable certificate pinning. Set debuggable=false for release builds. Disable allowBackup unless encrypted. Use Network Security Config to restrict cleartext traffic. Enable minify/obfuscation for production.'
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,68 @@
1
+ import { readFileSync, readdirSync, statSync } from 'fs';
2
+ import { join, extname } from 'path';
3
+ import { addFinding } from '../core/findings.js';
4
+
5
+ const SCAN_EXTENSIONS = ['.js', '.ts', '.jsx', '.tsx', '.mjs', '.py', '.rb', '.php', '.go', '.java', '.env'];
6
+ const IGNORE_DIRS = ['node_modules', '.git', 'dist', 'build', '__pycache__', 'vendor'];
7
+
8
+ export async function auditOauth(projectPath, spinner) {
9
+ spinner.text = 'Scanning for OAuth/OIDC vulnerabilities (PortSwigger)...';
10
+ const files = getFiles(projectPath);
11
+
12
+ for (const file of files) {
13
+ try {
14
+ const content = readFileSync(file, 'utf-8');
15
+ const relativePath = file.replace(projectPath, '.');
16
+ const lines = content.split('\n');
17
+
18
+ const patterns = [
19
+ { pattern: /redirect_uri\s*[:=]\s*(?:req|params|query|body|request)/gi, name: 'OAuth redirect_uri from user input (open redirect → token theft)', severity: 'CRITICAL' },
20
+ { pattern: /state\s*[:=]\s*(?:null|undefined|''|"")|(?:!state|state\s*===?\s*(?:null|undefined))/gi, name: 'OAuth state parameter missing/null (CSRF)', severity: 'HIGH' },
21
+ { pattern: /response_type\s*[:=]\s*['"]token['"]/gi, name: 'OAuth implicit flow (token in URL fragment)', severity: 'MEDIUM' },
22
+ { pattern: /client_secret\s*[:=]\s*['"][^'"]{5,}['"]/gi, name: 'OAuth client_secret hardcoded', severity: 'HIGH' },
23
+ { pattern: /grant_type\s*[:=]\s*['"]password['"]/gi, name: 'OAuth Resource Owner Password grant (insecure)', severity: 'MEDIUM' },
24
+ { pattern: /(?:GOOGLE|GITHUB|FACEBOOK|TWITTER|OAUTH)_CLIENT_SECRET\s*=\s*['"]?[A-Za-z0-9_-]{10,}/gi, name: 'OAuth provider secret in source', severity: 'HIGH' },
25
+ { pattern: /(?:openid|oauth|oidc).*(?:nonce|at_hash)\s*.*(?:skip|ignore|false)/gi, name: 'OAuth nonce/at_hash validation disabled', severity: 'HIGH' },
26
+ { pattern: /(?:verify|validate).*(?:state|nonce)\s*.*(?:false|skip|disabled)/gi, name: 'OAuth state/nonce verification disabled', severity: 'CRITICAL' },
27
+ { pattern: /scope\s*[:=]\s*['"].*(?:admin|write|delete|manage)/gi, name: 'OAuth requesting elevated scopes', severity: 'LOW' },
28
+ { pattern: /token_endpoint_auth_method\s*[:=]\s*['"]none['"]/gi, name: 'OAuth no client authentication', severity: 'HIGH' },
29
+ { pattern: /id_token.*(?:decode|parse).*(?:!verify|verify\s*[:=]\s*false)/gi, name: 'OIDC id_token not verified', severity: 'CRITICAL' },
30
+ { pattern: /(?:access_token|refresh_token)\s*[:=].*(?:localStorage|sessionStorage)/gi, name: 'OAuth tokens stored in browser storage (XSS theft)', severity: 'HIGH' },
31
+ { pattern: /PKCE|code_challenge|code_verifier/gi, name: 'PKCE usage detected (good practice)', severity: 'INFO' },
32
+ ];
33
+
34
+ for (const { pattern, name, severity } of patterns) {
35
+ let match;
36
+ while ((match = pattern.exec(content)) !== null) {
37
+ const lineNum = content.substring(0, match.index).split('\n').length;
38
+ const line = lines[lineNum - 1]?.trim() || '';
39
+ if (line.startsWith('//') || line.startsWith('#')) continue;
40
+
41
+ addFinding(
42
+ severity,
43
+ 'OAuth/OIDC (PortSwigger)',
44
+ name,
45
+ `File: ${relativePath}:${lineNum}\nCode: ${line.substring(0, 120)}`,
46
+ 'Use Authorization Code flow with PKCE. Validate state parameter to prevent CSRF. Whitelist redirect_uris server-side. Never expose client_secret in client-side code. Store tokens in httpOnly cookies, not localStorage.'
47
+ );
48
+ }
49
+ }
50
+ } catch {}
51
+ }
52
+ }
53
+
54
+ function getFiles(dir, files = []) {
55
+ try {
56
+ const entries = readdirSync(dir);
57
+ for (const entry of entries) {
58
+ if (IGNORE_DIRS.includes(entry) || entry.startsWith('.')) continue;
59
+ const fullPath = join(dir, entry);
60
+ try {
61
+ const stat = statSync(fullPath);
62
+ if (stat.isDirectory()) getFiles(fullPath, files);
63
+ else if (SCAN_EXTENSIONS.includes(extname(entry).toLowerCase()) && stat.size < 512 * 1024) files.push(fullPath);
64
+ } catch {}
65
+ }
66
+ } catch {}
67
+ return files;
68
+ }
@@ -0,0 +1,72 @@
1
+ import { readFileSync, readdirSync, statSync } from 'fs';
2
+ import { join, extname } from 'path';
3
+ import { addFinding } from '../core/findings.js';
4
+
5
+ const SCAN_EXTENSIONS = ['.js', '.ts', '.jsx', '.tsx', '.py', '.rb', '.php', '.java', '.go', '.xml', '.env', '.conf', '.yml', '.yaml'];
6
+ const IGNORE_DIRS = ['node_modules', '.git', 'dist', 'build', '__pycache__', 'vendor'];
7
+
8
+ export async function auditSaml(projectPath, spinner) {
9
+ spinner.text = 'Scanning for SAML/SSO vulnerabilities...';
10
+ const files = getFiles(projectPath);
11
+
12
+ for (const file of files) {
13
+ try {
14
+ const content = readFileSync(file, 'utf-8');
15
+ const relativePath = file.replace(projectPath, '.');
16
+ const lines = content.split('\n');
17
+
18
+ const patterns = [
19
+ { pattern: /signature\s*.*(?:skip|disabled?|false|none)/gi, name: 'SAML signature validation disabled/skipped', severity: 'CRITICAL' },
20
+ { pattern: /(?:validate|verify|check).*signature\s*.*(?:false|null|undefined|skip)/gi, name: 'Signature verification disabled', severity: 'CRITICAL' },
21
+ { pattern: /wantAssertionsSigned\s*[:=]\s*false/gi, name: 'Assertions signing not required', severity: 'HIGH' },
22
+ { pattern: /wantAuthnRequestsSigned\s*[:=]\s*false/gi, name: 'AuthnRequest signing not required', severity: 'HIGH' },
23
+ { pattern: /(?:IDP|idp|identityProvider).*(?:metadata|config|cert|certificate).*url\s*=.*(?:http:\/\/|request)/gi, name: 'IdP metadata from dynamic/untrusted URL', severity: 'HIGH' },
24
+ { pattern: /saml2\.validatePostResponse\s*\(/gi, name: 'SAML response validation (check for XSW)', severity: 'MEDIUM' },
25
+ { pattern: /saml2\.validateRedirect\s*\(/gi, name: 'SAML redirect binding', severity: 'INFO' },
26
+ { pattern: /(?:audience|AudienceRestriction)\s*.*(?:skip|disabled?|false)/gi, name: 'Audience restriction disabled', severity: 'HIGH' },
27
+ { pattern: /(?:entityID|issuer)\s*.*(?:check|validate)\s*.*(?:false|skip)/gi, name: 'Entity ID validation skipped', severity: 'MEDIUM' },
28
+ { pattern: /(?:notBefore|NotOnOrAfter|notOnOrAfter|clockSkew|skew)\s*[:=]\s*\d{4,}/gi, name: 'Large SAML clock skew (replay risk)', severity: 'LOW' },
29
+ { pattern: /NameID\s*.*(?:req|request|input|body|query|params|user)/gi, name: 'NameID from user input', severity: 'CRITICAL' },
30
+ { pattern: /(?:attributes?|AttributeStatement)\s*.*(?:req|request|input|body)/gi, name: 'SAML attributes from user input', severity: 'CRITICAL' },
31
+ { pattern: /InResponseTo\s*.*(?:skip|disabled?|false|null)/gi, name: 'InResponseTo validation skipped', severity: 'HIGH' },
32
+ { pattern: /(?:SP|serviceProvider|SAML).*(?:cert|certificate)\s*[:=]\s*['"]/gi, name: 'SAML certificate hardcoded in code', severity: 'LOW' },
33
+ { pattern: /XMLSignature\s*\(\s*\)/gi, name: 'XML signature object (check for XSW bypass)', severity: 'MEDIUM' },
34
+ { pattern: /(?:find|query|select)(?:Selector)?\s*\(\s*['"]\/*\/(?:\w+:)?Assertion['"]\s*\)/gi, name: 'XPath query for Assertion (XSW vulnerability)', severity: 'HIGH' },
35
+ { pattern: /(?:find|query|select)(?:Selector)?\s*\(\s*['"](?:\w+:)?(?:AttributeStatement|NameID|Subject)['"]\s*\)/gi, name: 'XPath query for SAML elements (check for XSW)', severity: 'MEDIUM' },
36
+ ];
37
+
38
+ for (const { pattern, name, severity } of patterns) {
39
+ let match;
40
+ while ((match = pattern.exec(content)) !== null) {
41
+ const lineNum = content.substring(0, match.index).split('\n').length;
42
+ const line = lines[lineNum - 1]?.trim() || '';
43
+ if (line.startsWith('//') || line.startsWith('#') || line.startsWith('*')) continue;
44
+
45
+ addFinding(
46
+ severity,
47
+ 'SAML/SSO Attacks',
48
+ name,
49
+ `File: ${relativePath}:${lineNum}\nCode: ${line.substring(0, 120)}`,
50
+ 'Validate SAML signatures with trusted Identity Provider certificates. Never accept unsigned assertions. Use XML Signature Wrapping (XSW) defenses: validate the exact XPath of signed elements. Always enforce audience restriction and InResponseTo. Do not accept NameID or attributes from user input.'
51
+ );
52
+ }
53
+ }
54
+ } catch {}
55
+ }
56
+ }
57
+
58
+ function getFiles(dir, files = []) {
59
+ try {
60
+ const entries = readdirSync(dir);
61
+ for (const entry of entries) {
62
+ if (IGNORE_DIRS.includes(entry) || entry.startsWith('.')) continue;
63
+ const fullPath = join(dir, entry);
64
+ try {
65
+ const stat = statSync(fullPath);
66
+ if (stat.isDirectory()) getFiles(fullPath, files);
67
+ else if (SCAN_EXTENSIONS.includes(extname(entry).toLowerCase()) && stat.size < 1024 * 1024) files.push(fullPath);
68
+ } catch {}
69
+ }
70
+ } catch {}
71
+ return files;
72
+ }
@@ -0,0 +1,73 @@
1
+ import { readFileSync, readdirSync, statSync } from 'fs';
2
+ import { join, extname } from 'path';
3
+ import { addFinding } from '../core/findings.js';
4
+
5
+ const SCAN_EXTENSIONS = ['.sol', '.rs', '.js', '.ts', '.jsx', '.tsx', '.py', '.vy', '.move'];
6
+ const IGNORE_DIRS = ['node_modules', '.git', 'dist', 'build', '__pycache__', 'vendor', 'out'];
7
+
8
+ export async function auditWeb3(projectPath, spinner) {
9
+ spinner.text = 'Scanning for Web3/smart contract vulnerabilities...';
10
+ const files = getFiles(projectPath);
11
+
12
+ for (const file of files) {
13
+ try {
14
+ const content = readFileSync(file, 'utf-8');
15
+ const relativePath = file.replace(projectPath, '.');
16
+ const lines = content.split('\n');
17
+
18
+ const patterns = [
19
+ { pattern: /reentrancy|re-?entrant|nonReentrant/gi, name: 'Reentrancy guard pattern', severity: 'INFO' },
20
+ { pattern: /\.call\s*\(\s*\{\s*value:|\.transfer\s*\(|\.send\s*\(/gi, name: 'ETH transfer method (check reentrancy)', severity: 'HIGH' },
21
+ { pattern: /(?:transfer|send)\s*\(\s*(?:msg\.value|address\(this\)\.balance)/gi, name: 'Full balance transfer (reentrancy risk)', severity: 'CRITICAL' },
22
+ { pattern: /(?:balances?|mapping).*\s*\[.*\s*\]\s*[-+]=?\s*/gi, name: 'Balance update (check CEI pattern)', severity: 'MEDIUM' },
23
+ { pattern: /delegatecall|delegatecall\s*\(/gi, name: 'delegatecall usage (storage collision risk)', severity: 'CRITICAL' },
24
+ { pattern: /selfdestruct|suicide\s*\(/gi, name: 'selfdestruct usage', severity: 'HIGH' },
25
+ { pattern: /tx\.origin\s*={2,3}|require\s*\(\s*tx\.origin/gi, name: 'tx.origin used for auth (phishing risk)', severity: 'CRITICAL' },
26
+ { pattern: /block\.timestamp.*==|block\.timestamp.*<=|block\.timestamp.*>=/gi, name: 'block.timestamp in strict comparison', severity: 'MEDIUM' },
27
+ { pattern: /blockhash\s*\(|block\.blockhash/gi, name: 'Blockhash usage (predictable in multi-block context)', severity: 'MEDIUM' },
28
+ { pattern: /onlyOwner|Ownable|owner\s*=\s*msg\.sender/gi, name: 'Ownable pattern (single point of failure)', severity: 'MEDIUM' },
29
+ { pattern: /mint\s*\(\s*.*address|_mint\s*\(/gi, name: 'Mint function (check access control)', severity: 'HIGH' },
30
+ { pattern: /proxy|implementation|upgrade|initialize/gi, name: 'Upgradeable/proxy pattern', severity: 'INFO' },
31
+ { pattern: /storage\s+|assembly\s*\{.*sload|sstore/gi, name: 'Inline assembly with storage access', severity: 'HIGH' },
32
+ { pattern: /unchecked\s*\{|unchecked\s*\(/gi, name: 'unchecked block (integer overflow risk)', severity: 'MEDIUM' },
33
+ { pattern: /uint\d+\s*\+\s*|uint\d+\s*-\s*|uint\d+\s*\*\s*/gi, name: 'Integer arithmetic (check overflow/underflow if Solidity < 0.8)', severity: 'MEDIUM' },
34
+ { pattern: /msg\.value\s*>=\s*0|msg\.value\s*==\s*0/gi, name: 'msg.value comparison (check edge cases)', severity: 'LOW' },
35
+ { pattern: /(?:constructor|init)\s*\([^)]*(?:_proxy|_impl|_implementation)/gi, name: 'Proxy initialization (check for double-init)', severity: 'HIGH' },
36
+ { pattern: /(?:deadline|expiry|expires).*(?:require|if|assert)\s*\(/gi, name: 'Deadline check (frontrunning protection)', severity: 'MEDIUM' },
37
+ ];
38
+
39
+ for (const { pattern, name, severity } of patterns) {
40
+ let match;
41
+ while ((match = pattern.exec(content)) !== null) {
42
+ const lineNum = content.substring(0, match.index).split('\n').length;
43
+ const line = lines[lineNum - 1]?.trim() || '';
44
+ if (line.startsWith('//') || line.startsWith('#') || line.startsWith('*')) continue;
45
+
46
+ addFinding(
47
+ severity,
48
+ 'Web3 / Smart Contracts',
49
+ name,
50
+ `File: ${relativePath}:${lineNum}\nCode: ${line.substring(0, 120)}`,
51
+ 'Use Checks-Effects-Interactions pattern. Use ReentrancyGuard from OpenZeppelin. Verify proxy initializer is only called once. Use msg.sender instead of tx.origin for auth. Use block.timestamp >= for time comparisons. Run slither/aderyn for deeper analysis.'
52
+ );
53
+ }
54
+ }
55
+ } catch {}
56
+ }
57
+ }
58
+
59
+ function getFiles(dir, files = []) {
60
+ try {
61
+ const entries = readdirSync(dir);
62
+ for (const entry of entries) {
63
+ if (IGNORE_DIRS.includes(entry) || entry.startsWith('.')) continue;
64
+ const fullPath = join(dir, entry);
65
+ try {
66
+ const stat = statSync(fullPath);
67
+ if (stat.isDirectory()) getFiles(fullPath, files);
68
+ else if (SCAN_EXTENSIONS.includes(extname(entry).toLowerCase()) && stat.size < 256 * 1024) files.push(fullPath);
69
+ } catch {}
70
+ }
71
+ } catch {}
72
+ return files;
73
+ }
@@ -0,0 +1,74 @@
1
+ import { readFileSync, readdirSync, statSync } from 'fs';
2
+ import { join, extname } from 'path';
3
+ import { addFinding } from '../core/findings.js';
4
+
5
+ const SCAN_EXTENSIONS = ['.js', '.ts', '.jsx', '.tsx', '.py', '.rb', '.php', '.java', '.cs', '.go', '.xml', '.svg'];
6
+ const IGNORE_DIRS = ['node_modules', '.git', 'dist', 'build', '__pycache__', 'vendor', 'target'];
7
+
8
+ export async function auditXxe(projectPath, spinner) {
9
+ spinner.text = 'Scanning for XXE vulnerabilities (PortSwigger)...';
10
+ const files = getFiles(projectPath);
11
+
12
+ for (const file of files) {
13
+ try {
14
+ const content = readFileSync(file, 'utf-8');
15
+ const relativePath = file.replace(projectPath, '.');
16
+ const lines = content.split('\n');
17
+
18
+ const xxePatterns = [
19
+ { pattern: /DOMParser\s*\(\s*\)/gi, name: 'DOMParser without disabling entities', severity: 'HIGH' },
20
+ { pattern: /parseString\s*\(\s*(?:req|params|query|body|user|data|input)/gi, name: 'XML parsing user input (xml2js)', severity: 'CRITICAL' },
21
+ { pattern: /xml2js|fast-xml-parser|libxmljs|sax\s*\./gi, name: 'XML parser library usage', severity: 'INFO' },
22
+ { pattern: /DocumentBuilderFactory/gi, name: 'Java XML DocumentBuilderFactory', severity: 'MEDIUM' },
23
+ { pattern: /SAXParserFactory/gi, name: 'Java SAXParserFactory', severity: 'MEDIUM' },
24
+ { pattern: /XMLReader/gi, name: 'XMLReader usage', severity: 'MEDIUM' },
25
+ { pattern: /etree\.parse\s*\(\s*(?:req|request|data|input|file)/gi, name: 'Python lxml parse user input', severity: 'CRITICAL' },
26
+ { pattern: /etree\.fromstring\s*\(\s*(?:req|request|data|input)/gi, name: 'Python lxml fromstring user input', severity: 'CRITICAL' },
27
+ { pattern: /simplexml_load_string\s*\(\s*\$_(?:GET|POST|REQUEST)/gi, name: 'PHP simplexml_load_string user input', severity: 'CRITICAL' },
28
+ { pattern: /DOMDocument.*loadXML\s*\(\s*\$_(?:GET|POST|REQUEST)/gi, name: 'PHP DOMDocument loadXML user input', severity: 'CRITICAL' },
29
+ { pattern: /LIBXML_NOENT/gi, name: 'PHP LIBXML_NOENT (entity substitution enabled)', severity: 'HIGH' },
30
+ { pattern: /resolve_entities\s*[:=]\s*true/gi, name: 'Entity resolution enabled', severity: 'HIGH' },
31
+ { pattern: /external_entities\s*[:=]\s*true/gi, name: 'External entities enabled', severity: 'CRITICAL' },
32
+ { pattern: /setFeature\s*\(\s*['"]http:\/\/xml\.org\/sax\/features\/external-general-entities['"]\s*,\s*true/gi, name: 'Java external entities enabled', severity: 'CRITICAL' },
33
+ { pattern: /DOCTYPE|ENTITY|SYSTEM/g, name: 'DOCTYPE/ENTITY declaration in file', severity: 'LOW' },
34
+ ];
35
+
36
+ for (const { pattern, name, severity } of xxePatterns) {
37
+ let match;
38
+ while ((match = pattern.exec(content)) !== null) {
39
+ const lineNum = content.substring(0, match.index).split('\n').length;
40
+ const line = lines[lineNum - 1]?.trim() || '';
41
+ if (isComment(line)) continue;
42
+
43
+ addFinding(
44
+ severity,
45
+ 'XXE (PortSwigger)',
46
+ `${name}`,
47
+ `File: ${relativePath}:${lineNum}\nCode: ${line.substring(0, 120)}`,
48
+ 'Disable external entity processing: set disallow-doctype-decl=true, external-general-entities=false. Use JSON instead of XML where possible. For Java: factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true)'
49
+ );
50
+ }
51
+ }
52
+ } catch {}
53
+ }
54
+ }
55
+
56
+ function isComment(line) {
57
+ return line.startsWith('//') || line.startsWith('#') || line.startsWith('*') || line.startsWith('<!--');
58
+ }
59
+
60
+ function getFiles(dir, files = []) {
61
+ try {
62
+ const entries = readdirSync(dir);
63
+ for (const entry of entries) {
64
+ if (IGNORE_DIRS.includes(entry) || entry.startsWith('.')) continue;
65
+ const fullPath = join(dir, entry);
66
+ try {
67
+ const stat = statSync(fullPath);
68
+ if (stat.isDirectory()) getFiles(fullPath, files);
69
+ else if (SCAN_EXTENSIONS.includes(extname(entry).toLowerCase()) && stat.size < 512 * 1024) files.push(fullPath);
70
+ } catch {}
71
+ }
72
+ } catch {}
73
+ return files;
74
+ }