xploitscan 1.0.1 → 1.0.2

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/dist/index.js CHANGED
@@ -1,14 +1,17 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  checkUsage,
4
+ clearProRulesCache,
4
5
  clearToken,
6
+ downloadProRulesBundle,
5
7
  getStoredToken,
6
8
  incrementUsage,
7
9
  isAuthenticated,
10
+ loadCachedProRules,
8
11
  storeToken,
9
12
  syncUser,
10
13
  uploadScanResults
11
- } from "./chunk-CBDFSACC.js";
14
+ } from "./chunk-IHRV7UHG.js";
12
15
 
13
16
  // src/index.ts
14
17
  import { Command } from "commander";
@@ -666,58 +669,6 @@ var nextPublicSecret = {
666
669
  return matches;
667
670
  }
668
671
  };
669
- var firebaseClientConfig = {
670
- id: "VC012",
671
- title: "Firebase Config with API Key in Client Code",
672
- severity: "medium",
673
- category: "Configuration",
674
- description: "Firebase config objects in client code expose your API key. While Firebase API keys aren't secret, they should be restricted in the Firebase console.",
675
- check(content, filePath) {
676
- if (!/firebase/i.test(content)) return [];
677
- const patterns = [
678
- /firebaseConfig\s*=\s*\{[^}]*apiKey\s*:/gi,
679
- /initializeApp\s*\(\s*\{[^}]*apiKey\s*:/gi
680
- ];
681
- const matches = [];
682
- for (const p of patterns) {
683
- matches.push(...findMatches(
684
- content,
685
- p,
686
- firebaseClientConfig,
687
- filePath,
688
- () => "Move Firebase config to environment variables. Restrict the API key in Firebase Console > Project Settings > API restrictions."
689
- ));
690
- }
691
- return matches;
692
- }
693
- };
694
- var supabaseAnonAdmin = {
695
- id: "VC013",
696
- title: "Supabase Anon Key Used for Admin Operations",
697
- severity: "high",
698
- category: "Authorization",
699
- description: "Using the Supabase anon key for operations that require elevated privileges is insecure.",
700
- check(content, filePath) {
701
- if (!/supabase/i.test(content)) return [];
702
- if (!/anon/i.test(content)) return [];
703
- if (/service_role/i.test(content)) return [];
704
- const patterns = [
705
- /supabase[^.]*\.auth\.admin/gi,
706
- /supabase[^.]*\.rpc\s*\(/gi
707
- ];
708
- const matches = [];
709
- for (const p of patterns) {
710
- matches.push(...findMatches(
711
- content,
712
- p,
713
- supabaseAnonAdmin,
714
- filePath,
715
- () => "Use the service_role key on the server side for admin operations. Never expose it to the client."
716
- ));
717
- }
718
- return matches;
719
- }
720
- };
721
672
  var envNotGitignored = {
722
673
  id: "VC014",
723
674
  title: ".env File Not in .gitignore",
@@ -874,46 +825,6 @@ var exposedAuthSecret = {
874
825
  return matches;
875
826
  }
876
827
  };
877
- var insecureElectronWindow = {
878
- id: "VC019",
879
- title: "Insecure Electron BrowserWindow Configuration",
880
- severity: "high",
881
- category: "Configuration",
882
- description: "Electron BrowserWindow with nodeIntegration enabled, contextIsolation disabled, or sandbox disabled allows renderer processes to access Node.js APIs, enabling remote code execution.",
883
- check(content, filePath) {
884
- if (isTestFile(filePath)) return [];
885
- if (!/BrowserWindow/i.test(content)) return [];
886
- const matches = [];
887
- const patterns = [
888
- /nodeIntegration\s*:\s*true/g,
889
- /contextIsolation\s*:\s*false/g,
890
- /sandbox\s*:\s*false/g,
891
- /webSecurity\s*:\s*false/g,
892
- /allowRunningInsecureContent\s*:\s*true/g
893
- ];
894
- for (const p of patterns) {
895
- matches.push(...findMatches(
896
- content,
897
- p,
898
- insecureElectronWindow,
899
- filePath,
900
- (m) => `Set ${m[0].split(":")[0].trim()}: ${m[0].includes("true") ? "false" : "true"}. Enable contextIsolation, sandbox, and webSecurity; disable nodeIntegration and allowRunningInsecureContent.`
901
- ));
902
- }
903
- if (/new\s+BrowserWindow\s*\(/g.test(content)) {
904
- if (!/sandbox\s*:/i.test(content)) {
905
- matches.push(...findMatches(
906
- content,
907
- /new\s+BrowserWindow\s*\(/g,
908
- { ...insecureElectronWindow, title: "Electron BrowserWindow Missing sandbox:true" },
909
- filePath,
910
- () => "Add sandbox: true to BrowserWindow webPreferences for defense in depth."
911
- ));
912
- }
913
- }
914
- return matches;
915
- }
916
- };
917
828
  var missingCSP = {
918
829
  id: "VC020",
919
830
  title: "Missing Content Security Policy (CSP)",
@@ -938,311 +849,6 @@ var missingCSP = {
938
849
  return [];
939
850
  }
940
851
  };
941
- var ipcPathTraversal = {
942
- id: "VC021",
943
- title: "IPC/File Handler Without Path Validation",
944
- severity: "medium",
945
- category: "Injection",
946
- description: "IPC handlers that read or write files based on renderer-supplied paths without validation allow path traversal attacks, potentially exposing sensitive files like .ssh keys or .env files.",
947
- check(content, filePath) {
948
- if (!/ipcMain\.handle|ipcMain\.on/i.test(content)) return [];
949
- const matches = [];
950
- const hasFileOps = /readFile|writeFile|readFileSync|writeFileSync|createReadStream|createWriteStream/i.test(content);
951
- if (!hasFileOps) return [];
952
- const hasPathValidation = /(?:path\.resolve|path\.normalize|startsWith|isAbsolute|\.includes\s*\(\s*["'`]\.\.["'`]\s*\)|allowedPaths|safePath|validatePath|sanitizePath)/i.test(content);
953
- if (!hasPathValidation) {
954
- matches.push(...findMatches(
955
- content,
956
- /ipcMain\.(?:handle|on)\s*\(\s*["'`][^"'`]*(?:read|write|file|save|load|open|export)[^"'`]*["'`]/gi,
957
- ipcPathTraversal,
958
- filePath,
959
- () => "Validate file paths in IPC handlers: ensure paths are within an allowed directory (e.g., app.getPath('userData')), reject paths containing '..', and block access to sensitive directories (.ssh, .env, etc)."
960
- ));
961
- }
962
- return matches;
963
- }
964
- };
965
- var unsanitizedHTMLExport = {
966
- id: "VC022",
967
- title: "HTML Export/Render Without Sanitization",
968
- severity: "critical",
969
- category: "Injection",
970
- description: "Generating HTML from user content without sanitization (e.g., DOMPurify) allows stored XSS attacks. Malicious content saved in documents could execute scripts when exported or previewed.",
971
- check(content, filePath) {
972
- if (isTestFile(filePath)) return [];
973
- if (/DOMPurify|dompurify/i.test(content)) return [];
974
- if (filePath.match(/\.(jsx|tsx|vue|svelte)$/) && !/innerHTML|document\.write|\.html\s*=/i.test(content)) return [];
975
- const hasSanitizer = /sanitize|escapeHtml|escapeHTML|xss|htmlEncode|purify/i.test(content);
976
- if (hasSanitizer) return [];
977
- if (!/(?:export|download|save|write|send).*(?:html|HTML)|\.innerHTML\s*=|document\.write|res\.send\s*\(/i.test(content)) return [];
978
- const matches = [];
979
- const htmlBuildPatterns = [
980
- /`<[^`]*\$\{[^}]*(?:content|title|body|text|name|message|description|input|value|data)[^}]*\}[^`]*>`/gi,
981
- /["']<[^"']*['"]\s*\+\s*(?:content|title|body|text|message|data|doc\.|post\.|article\.)/gi
982
- ];
983
- for (const p of htmlBuildPatterns) {
984
- matches.push(...findMatches(
985
- content,
986
- p,
987
- unsanitizedHTMLExport,
988
- filePath,
989
- () => "Sanitize user content before embedding in HTML. Use DOMPurify: DOMPurify.sanitize(content). For plain text, use a function to escape HTML entities (<, >, &, quotes)."
990
- ));
991
- }
992
- return matches;
993
- }
994
- };
995
- var prototypePollution = {
996
- id: "VC023",
997
- title: "Prototype Pollution Risk",
998
- severity: "high",
999
- category: "Injection",
1000
- description: "Parsing JSON from localStorage, URL params, or external sources and merging it into objects without validation can lead to prototype pollution, allowing attackers to inject __proto__ or constructor properties.",
1001
- check(content, filePath) {
1002
- const matches = [];
1003
- const storageParsePatterns = [
1004
- /JSON\.parse\s*\(\s*(?:localStorage|sessionStorage)\.getItem/g,
1005
- /JSON\.parse\s*\(\s*window\.localStorage/g
1006
- ];
1007
- const hasValidation = /schema|validate|sanitize|whitelist|allowedKeys|pick\(|Object\.freeze|zod|yup|joi|ajv/i.test(content);
1008
- if (hasValidation) return [];
1009
- const hasUnsafeMerge = /Object\.assign\s*\([^)]*JSON\.parse|\.\.\.JSON\.parse|\{.*\.\.\.(?:stored|saved|cached|parsed|data)/i.test(content);
1010
- if (hasUnsafeMerge) {
1011
- matches.push(...findMatches(
1012
- content,
1013
- /Object\.assign\s*\([^)]*JSON\.parse|\.\.\.JSON\.parse/g,
1014
- prototypePollution,
1015
- filePath,
1016
- () => "Validate parsed data against an expected schema before merging into objects. Use Object.freeze(), a validation library (Zod, Yup), or manually check for __proto__ and constructor keys."
1017
- ));
1018
- }
1019
- for (const p of storageParsePatterns) {
1020
- matches.push(...findMatches(
1021
- content,
1022
- p,
1023
- prototypePollution,
1024
- filePath,
1025
- () => "Validate localStorage data against an expected schema before using it. Malicious extensions or XSS can modify localStorage values."
1026
- ));
1027
- }
1028
- return matches;
1029
- }
1030
- };
1031
- var missingFileSizeLimits = {
1032
- id: "VC024",
1033
- title: "File Write/Save Without Size Limit",
1034
- severity: "medium",
1035
- category: "Availability",
1036
- description: "File save or upload handlers without size validation can lead to denial-of-service via disk exhaustion or memory exhaustion.",
1037
- check(content, filePath) {
1038
- if (!/(?:writeFile|save|upload|export)/i.test(filePath) && !/(?:writeFile|writeFileSync|createWriteStream)/i.test(content)) return [];
1039
- const hasWriteOps = /(?:ipcMain|app\.(?:post|put)|router\.(?:post|put)).*(?:writeFile|save|export)/is.test(content) || /(?:writeFile|writeFileSync)\s*\(/g.test(content);
1040
- if (!hasWriteOps) return [];
1041
- const hasSizeCheck = /(?:size|length|byteLength|bytes)\s*(?:>|>=|<|<=|===)\s*\d|maxSize|MAX_SIZE|sizeLimit|content-length/i.test(content);
1042
- if (hasSizeCheck) return [];
1043
- return findMatches(
1044
- content,
1045
- /(?:writeFile|writeFileSync)\s*\(/g,
1046
- missingFileSizeLimits,
1047
- filePath,
1048
- () => "Add file size validation before writing. Check content.length or Buffer.byteLength() against a maximum (e.g., 10MB) to prevent disk exhaustion."
1049
- );
1050
- }
1051
- };
1052
- var unsanitizedFilenames = {
1053
- id: "VC025",
1054
- title: "Unsanitized Filename in File Operations",
1055
- severity: "medium",
1056
- category: "Injection",
1057
- description: "Using user-supplied filenames without sanitization in file operations can enable path traversal, overwriting system files, or executing commands via special characters.",
1058
- check(content, filePath) {
1059
- const matches = [];
1060
- const patterns = [
1061
- /(?:writeFile|writeFileSync|createWriteStream|rename|copyFile)\s*\(\s*(?:`[^`]*\$\{|[^"'`\s,]+\s*\+)/g,
1062
- /(?:dialog\.showSaveDialog|saveDialog).*(?:defaultPath|fileName)\s*:\s*(?!["'`])/g,
1063
- /\.download\s*=\s*(?!["'`])/g
1064
- ];
1065
- const hasSanitization = /sanitize|cleanFilename|safeFilename|replace\s*\(\s*\/\[.*\]\//i.test(content);
1066
- if (hasSanitization) return [];
1067
- for (const p of patterns) {
1068
- matches.push(...findMatches(
1069
- content,
1070
- p,
1071
- unsanitizedFilenames,
1072
- filePath,
1073
- () => "Sanitize filenames before use: strip path separators (/ \\), special chars, and '..' sequences. Example: name.replace(/[^a-zA-Z0-9._-]/g, '_')"
1074
- ));
1075
- }
1076
- return matches;
1077
- }
1078
- };
1079
- var electronNavigationUnrestricted = {
1080
- id: "VC026",
1081
- title: "Electron: External Navigation Not Blocked",
1082
- severity: "medium",
1083
- category: "Configuration",
1084
- description: "Electron apps that don't block navigation to external URLs or new window creation are vulnerable to phishing and drive-by downloads. Malicious links in app content can redirect the entire app to an attacker's site.",
1085
- check(content, filePath) {
1086
- if (!/BrowserWindow|electron/i.test(content)) return [];
1087
- if (!/main|index/i.test(filePath)) return [];
1088
- const hasNavBlock = /will-navigate|new-window|setWindowOpenHandler|webContents\.on.*navigate/i.test(content);
1089
- if (hasNavBlock) return [];
1090
- if (/new\s+BrowserWindow/i.test(content)) {
1091
- return findMatches(
1092
- content,
1093
- /new\s+BrowserWindow\s*\(/g,
1094
- electronNavigationUnrestricted,
1095
- filePath,
1096
- () => "Block external navigation: win.webContents.on('will-navigate', (e, url) => { if (!url.startsWith('file://')) e.preventDefault(); }); and use setWindowOpenHandler to block new windows."
1097
- );
1098
- }
1099
- return [];
1100
- }
1101
- };
1102
- var missingSecurityMeta = {
1103
- id: "VC027",
1104
- title: "Missing Security Meta Tags / Headers",
1105
- severity: "medium",
1106
- category: "Configuration",
1107
- description: "HTML pages without X-Content-Type-Options, referrer policy, or other security meta tags are more susceptible to MIME-sniffing attacks and information leakage.",
1108
- check(content, filePath) {
1109
- if (!filePath.match(/\.(html|htm)$/)) return [];
1110
- const matches = [];
1111
- if (!/X-Content-Type-Options/i.test(content) && !/<meta[^>]*nosniff/i.test(content)) {
1112
- matches.push({
1113
- rule: "VC027",
1114
- title: "Missing X-Content-Type-Options Header",
1115
- severity: "medium",
1116
- category: "Configuration",
1117
- file: filePath,
1118
- line: 1,
1119
- snippet: getSnippet(content, 1),
1120
- fix: 'Add <meta http-equiv="X-Content-Type-Options" content="nosniff"> to prevent MIME-type sniffing.'
1121
- });
1122
- }
1123
- if (!/referrer/i.test(content)) {
1124
- matches.push({
1125
- rule: "VC027",
1126
- title: "Missing Referrer Policy",
1127
- severity: "medium",
1128
- category: "Configuration",
1129
- file: filePath,
1130
- line: 1,
1131
- snippet: getSnippet(content, 1),
1132
- fix: 'Add <meta name="referrer" content="no-referrer"> or "strict-origin-when-cross-origin" to limit referrer leakage.'
1133
- });
1134
- }
1135
- return matches;
1136
- }
1137
- };
1138
- var unvalidatedAPIParams = {
1139
- id: "VC028",
1140
- title: "Unvalidated API Request Parameters",
1141
- severity: "high",
1142
- category: "Injection",
1143
- description: "API requests constructed with unvalidated user input (API keys, model names, URLs) can be exploited for injection attacks or unauthorized access to different API models/endpoints.",
1144
- check(content, filePath) {
1145
- const matches = [];
1146
- const apiKeyPatterns = [
1147
- /(?:apiKey|api_key|authorization)\s*[:=]\s*(?:req\.body|req\.query|params|input|formData|body)\./gi,
1148
- /headers\s*:\s*\{[^}]*Authorization\s*:\s*(?!["'`]Bearer\s)/gi
1149
- ];
1150
- const hasValidation = /validate|sanitize|regex|test\(|match\(|pattern|allowList|whitelist|enum|includes\(/i.test(content);
1151
- if (hasValidation) return [];
1152
- if (/model\s*[:=]\s*(?:req\.body|params|input|body)\./i.test(content) || /model\s*[:=]\s*(?!["'`])[a-z]/i.test(content)) {
1153
- const hasModelValidation = /allowedModels|validModels|models\s*\.\s*includes|model.*(?:===|!==|includes)/i.test(content);
1154
- if (!hasModelValidation && /(?:openai|anthropic|claude|gpt|llm)/i.test(content)) {
1155
- matches.push(...findMatches(
1156
- content,
1157
- /model\s*[:=]\s*(?:req\.body|params|input|body)\./gi,
1158
- unvalidatedAPIParams,
1159
- filePath,
1160
- () => "Validate model selection against an allowlist of approved models. Example: const ALLOWED_MODELS = ['gpt-4', 'claude-3']; if (!ALLOWED_MODELS.includes(model)) throw new Error('Invalid model');"
1161
- ));
1162
- }
1163
- }
1164
- for (const p of apiKeyPatterns) {
1165
- matches.push(...findMatches(
1166
- content,
1167
- p,
1168
- unvalidatedAPIParams,
1169
- filePath,
1170
- () => "Validate API key format before using it (e.g., check prefix and length). Never pass user-supplied API keys directly to third-party services without validation."
1171
- ));
1172
- }
1173
- return matches;
1174
- }
1175
- };
1176
- var unvalidatedEventData = {
1177
- id: "VC029",
1178
- title: "Unvalidated Event or PostMessage Data",
1179
- severity: "medium",
1180
- category: "Injection",
1181
- description: "Custom events, postMessage, or IPC message data used without type-checking can lead to injection attacks or unexpected behavior when malicious data is sent through event channels.",
1182
- check(content, filePath) {
1183
- const matches = [];
1184
- if (/addEventListener\s*\(\s*["'`]message["'`]/i.test(content)) {
1185
- if (!/event\.origin|e\.origin|message\.origin/i.test(content)) {
1186
- matches.push(...findMatches(
1187
- content,
1188
- /addEventListener\s*\(\s*["'`]message["'`]/g,
1189
- unvalidatedEventData,
1190
- filePath,
1191
- () => "Always verify event.origin in message event handlers to prevent cross-origin attacks. Example: if (event.origin !== 'https://trusted.com') return;"
1192
- ));
1193
- }
1194
- }
1195
- if (/new\s+CustomEvent\s*\(/i.test(content) || /ipcRenderer\.send/i.test(content)) {
1196
- const hasTypeCheck = /typeof\s|instanceof|z\.|schema|validate|Number\.isFinite|parseInt|parseFloat/i.test(content);
1197
- if (!hasTypeCheck) {
1198
- matches.push(...findMatches(
1199
- content,
1200
- /new\s+CustomEvent\s*\(/g,
1201
- unvalidatedEventData,
1202
- filePath,
1203
- () => "Type-check custom event data before using it. Validate that data.detail contains expected types to prevent injection."
1204
- ));
1205
- }
1206
- }
1207
- return matches;
1208
- }
1209
- };
1210
- var insecureDeserialization = {
1211
- id: "VC030",
1212
- title: "Insecure Deserialization",
1213
- severity: "critical",
1214
- category: "Injection",
1215
- description: "Deserializing untrusted data (pickle, unserialize, yaml.load) can execute arbitrary code. Attackers craft malicious payloads to gain remote code execution.",
1216
- check(content, filePath) {
1217
- const matches = [];
1218
- const patterns = [
1219
- // Python pickle
1220
- /pickle\.loads?\s*\(/g,
1221
- /cPickle\.loads?\s*\(/g,
1222
- // PHP unserialize
1223
- /unserialize\s*\(/g,
1224
- // Ruby Marshal
1225
- /Marshal\.load\s*\(/g,
1226
- // YAML unsafe load (Python)
1227
- /yaml\.load\s*\([^)]*(?!Loader\s*=\s*yaml\.SafeLoader)/g,
1228
- /yaml\.unsafe_load\s*\(/g,
1229
- // Java ObjectInputStream
1230
- /ObjectInputStream\s*\(/g,
1231
- // Node.js node-serialize
1232
- /serialize\.unserialize\s*\(/g
1233
- ];
1234
- for (const p of patterns) {
1235
- matches.push(...findMatches(
1236
- content,
1237
- p,
1238
- insecureDeserialization,
1239
- filePath,
1240
- () => "Never deserialize untrusted data. Use JSON instead of pickle/Marshal/unserialize. For YAML, use yaml.safe_load(). Validate and sanitize all input before deserialization."
1241
- ));
1242
- }
1243
- return matches;
1244
- }
1245
- };
1246
852
  var hardcodedJWTSecret = {
1247
853
  id: "VC031",
1248
854
  title: "Hardcoded JWT Secret",
@@ -1359,33 +965,6 @@ var insecureRandomness = {
1359
965
  return matches;
1360
966
  }
1361
967
  };
1362
- var openRedirectParams = {
1363
- id: "VC035",
1364
- title: "Open Redirect via URL Parameters",
1365
- severity: "high",
1366
- category: "Injection",
1367
- description: "Redirect parameters like ?redirect_url=, ?return_to=, ?next= passed directly to redirects enable phishing attacks.",
1368
- check(content, filePath) {
1369
- const matches = [];
1370
- const patterns = [
1371
- // Reading redirect-like query params and using in redirect
1372
- /(?:redirect_url|redirect_uri|return_to|return_url|next|callback_url|continue|goto|target|dest|destination|forward|redir)\s*(?:=|:)\s*(?:req\.query|req\.params|searchParams|query|params)\./gi,
1373
- /redirect\s*\(\s*(?:req\.query|req\.params|searchParams\.get)\s*\(\s*["'`](?:redirect|return|next|callback|url|goto)/gi
1374
- ];
1375
- const hasValidation = /allowedUrls|allowedDomains|allowedHosts|validUrl|safeDomain|whitelist|startsWith.*https|new URL.*hostname/i.test(content);
1376
- if (hasValidation) return [];
1377
- for (const p of patterns) {
1378
- matches.push(...findMatches(
1379
- content,
1380
- p,
1381
- openRedirectParams,
1382
- filePath,
1383
- () => "Validate redirect URLs against an allowlist of trusted domains. Use: const url = new URL(input); if (!ALLOWED_HOSTS.includes(url.hostname)) reject."
1384
- ));
1385
- }
1386
- return matches;
1387
- }
1388
- };
1389
968
  var missingErrorBoundary = {
1390
969
  id: "VC036",
1391
970
  title: "React App Missing Error Boundary",
@@ -1449,29 +1028,6 @@ var exposedStackTraces = {
1449
1028
  return matches;
1450
1029
  }
1451
1030
  };
1452
- var insecureFileUpload = {
1453
- id: "VC038",
1454
- title: "Insecure File Upload Validation",
1455
- severity: "high",
1456
- category: "Injection",
1457
- description: "File uploads validated only by extension (not MIME type or content) allow attackers to upload executable files disguised as images or documents.",
1458
- check(content, filePath) {
1459
- if (!/upload|multer|formidable|busboy|multipart/i.test(content)) return [];
1460
- const matches = [];
1461
- const hasExtCheck = /\.(?:endsWith|match|test)\s*\([^)]*(?:\.jpg|\.png|\.pdf|\.doc|ext)/i.test(content);
1462
- const hasMimeCheck = /mimetype|content-type|file\.type|mime|magic\.detect|file-type/i.test(content);
1463
- if (hasExtCheck && !hasMimeCheck) {
1464
- matches.push(...findMatches(
1465
- content,
1466
- /upload|multer|formidable|busboy/gi,
1467
- insecureFileUpload,
1468
- filePath,
1469
- () => "Validate file uploads by MIME type AND magic bytes, not just extension. Use the 'file-type' package to detect actual file type from content. Also enforce size limits."
1470
- ));
1471
- }
1472
- return matches;
1473
- }
1474
- };
1475
1031
  var missingLockFile = {
1476
1032
  id: "VC039",
1477
1033
  title: "Missing Dependency Lock File",
@@ -1493,2711 +1049,332 @@ var missingLockFile = {
1493
1049
  return [];
1494
1050
  }
1495
1051
  };
1496
- var exposedGitDir = {
1497
- id: "VC040",
1498
- title: "Exposed .git Directory via Web Server",
1499
- severity: "critical",
1500
- category: "Information Leakage",
1501
- description: "Web server configs that don't block access to .git directories expose your entire source code, commit history, secrets, and credentials.",
1502
- check(content, filePath) {
1503
- if (!filePath.match(/(?:nginx|apache|httpd|caddy|\.htaccess|vercel\.json|netlify\.toml|server\.[jt]s)/i)) return [];
1504
- if (/(?:static|serve|express\.static|serveStatic|public)/i.test(content)) {
1505
- const blocksGit = /\.git|dotfiles|hidden/i.test(content);
1506
- if (!blocksGit) {
1507
- return findMatches(
1508
- content,
1509
- /(?:static|serve|express\.static|serveStatic)\s*\(/g,
1510
- exposedGitDir,
1511
- filePath,
1512
- () => "Block access to .git and other dotfiles in your static file server config. For Express: app.use('/.git', (req, res) => res.status(403).end()). For Nginx: location ~ /\\.git { deny all; }"
1513
- );
1514
- }
1515
- }
1516
- return [];
1517
- }
1518
- };
1519
- var ssrfVulnerability = {
1520
- id: "VC041",
1521
- title: "Potential Server-Side Request Forgery (SSRF)",
1052
+ var weakHashing = {
1053
+ id: "VC060",
1054
+ title: "Weak Hashing Algorithm for Passwords",
1522
1055
  severity: "critical",
1523
- category: "Injection",
1524
- description: "Fetching URLs from user input without validation allows attackers to access internal services, cloud metadata endpoints (169.254.169.254), and private networks.",
1056
+ category: "Cryptography",
1057
+ description: "MD5 and SHA1/SHA256 are too fast for password hashing \u2014 they can be brute-forced at billions of attempts per second. Use bcrypt, scrypt, or argon2 instead.",
1525
1058
  check(content, filePath) {
1526
- if (isTestFile(filePath)) return [];
1527
- const isServerFile = /(?:\/api\/|routes?\/|controllers?\/|server\.|handler|middleware)/i.test(filePath);
1528
- if (!isServerFile) return [];
1059
+ if (filePath.includes("test") || filePath.includes("mock")) return [];
1060
+ if (/bcrypt|scrypt|argon2/i.test(content)) return [];
1529
1061
  const matches = [];
1530
1062
  const patterns = [
1531
- /(?:fetch|axios\.get|axios\.post|axios|got|request|http\.get|https\.get)\s*\(\s*(?:req\.(?:body|query|params))\./gi
1532
- ];
1533
- const hasValidation = /allowedHosts|allowedDomains|allowedUrls|safeDomain|whitelist|urlValidator|new URL.*hostname.*includes|isAllowedUrl|validateUrl|isValidUrl/i.test(content);
1534
- if (hasValidation) return [];
1535
- for (const p of patterns) {
1536
- const rawMatches = findMatches(
1537
- content,
1538
- p,
1539
- ssrfVulnerability,
1540
- filePath,
1541
- () => "Validate URLs against an allowlist before fetching. Block internal IPs: 127.0.0.1, 10.x, 172.16-31.x, 192.168.x, 169.254.169.254 (cloud metadata). Use: const url = new URL(input); if (!ALLOWED_HOSTS.includes(url.hostname)) throw new Error('Blocked');"
1542
- );
1543
- for (const rm3 of rawMatches) {
1544
- const lineText = content.split("\n")[rm3.line - 1] || "";
1545
- const varMatch = lineText.match(/(?:fetch|axios\.\w+|got|request|https?\.get)\s*\(\s*(\w+)/);
1546
- if (varMatch) {
1547
- const varName = varMatch[1];
1548
- const constDef = new RegExp(`const\\s+${varName}\\s*=\\s*["'\`]https?://`, "i");
1549
- if (constDef.test(content)) continue;
1550
- }
1551
- matches.push(rm3);
1552
- }
1553
- }
1554
- return matches;
1555
- }
1556
- };
1557
- var massAssignment = {
1558
- id: "VC042",
1559
- title: "Mass Assignment Vulnerability",
1560
- severity: "high",
1561
- category: "Authorization",
1562
- description: "Spreading or assigning request body directly into database models allows attackers to set fields they shouldn't (e.g., isAdmin, role, verified).",
1563
- check(content, filePath) {
1564
- const isApiFile = /(?:\/api\/|routes?\/|controllers?\/|server\.|handler)/i.test(filePath);
1565
- if (!isApiFile) return [];
1566
- const matches = [];
1567
- const patterns = [
1568
- // Object.assign(model, req.body)
1569
- /Object\.assign\s*\(\s*(?:user|account|profile|record|doc|model|entity)[^,]*,\s*(?:req\.body|body|input|data)\s*\)/gi,
1570
- // Spread req.body into create/update
1571
- /(?:create|update|insert|save|findOneAndUpdate|updateOne|upsert)\s*\(\s*\{[^}]*\.\.\.(?:req\.body|body|input|data)/gi,
1572
- // Direct req.body into DB
1573
- /(?:create|insert|save)\s*\(\s*(?:req\.body|body)\s*\)/gi
1063
+ /(?:md5|sha1|sha256|sha512)\s*\([^)]*(?:password|passwd|pwd)/gi,
1064
+ /createHash\s*\(\s*["'`](?:md5|sha1|sha256)["'`]\).*(?:password|passwd|pwd)/gi,
1065
+ /(?:password|passwd|pwd).*createHash\s*\(\s*["'`](?:md5|sha1|sha256)["'`]\)/gi,
1066
+ /hashlib\.(?:md5|sha1|sha256)\s*\([^)]*(?:password|passwd|pwd)/gi,
1067
+ /Digest::(?:MD5|SHA1|SHA256).*(?:password|passwd|pwd)/gi
1574
1068
  ];
1575
- const hasSanitization = /pick\(|omit\(|allowedFields|sanitize|whitelist|permit|strong_params/i.test(content);
1576
- if (hasSanitization) return [];
1577
1069
  for (const p of patterns) {
1578
1070
  matches.push(...findMatches(
1579
1071
  content,
1580
1072
  p,
1581
- massAssignment,
1073
+ weakHashing,
1582
1074
  filePath,
1583
- () => "Never pass req.body directly to database operations. Explicitly pick allowed fields: const { name, email } = req.body; await db.create({ name, email });"
1075
+ () => "Use bcrypt, scrypt, or argon2 for password hashing \u2014 they're intentionally slow. Example: const hash = await bcrypt.hash(password, 12);"
1584
1076
  ));
1585
1077
  }
1586
1078
  return matches;
1587
1079
  }
1588
1080
  };
1589
- var timingAttack = {
1590
- id: "VC043",
1591
- title: "Timing-Unsafe Secret Comparison",
1592
- severity: "medium",
1081
+ var disabledTLSVerification = {
1082
+ id: "VC061",
1083
+ title: "Disabled TLS Certificate Verification",
1084
+ severity: "critical",
1593
1085
  category: "Cryptography",
1594
- description: "Using === to compare secrets, tokens, or hashes leaks information via timing side-channels. Attackers can determine the correct value one character at a time.",
1086
+ description: "Disabling TLS certificate verification (NODE_TLS_REJECT_UNAUTHORIZED=0 or rejectUnauthorized:false) makes all HTTPS connections vulnerable to man-in-the-middle attacks.",
1595
1087
  check(content, filePath) {
1596
- if (filePath.includes("test") || filePath.includes("mock")) return [];
1597
1088
  const matches = [];
1598
1089
  const patterns = [
1599
- /(?:token|secret|hash|digest|signature|hmac|apiKey|api_key)\s*(?:===|!==)\s*(?:req\.|body\.|params\.|query\.|input)/gi,
1600
- /(?:req\.|body\.|params\.|query\.|input)[\w.]*(?:token|secret|hash|digest|signature|hmac)\s*(?:===|!==)/gi
1090
+ /NODE_TLS_REJECT_UNAUTHORIZED\s*[:=]\s*["'`]?0["'`]?/g,
1091
+ /rejectUnauthorized\s*:\s*false/g,
1092
+ /verify\s*[:=]\s*false.*(?:ssl|tls|cert|https)/gi,
1093
+ /PYTHONHTTPSVERIFY\s*[:=]\s*["'`]?0["'`]?/g,
1094
+ /ssl_verify\s*[:=]\s*false/gi,
1095
+ /InsecureSkipVerify\s*:\s*true/g
1601
1096
  ];
1602
- const hasTimingSafe = /timingSafeEqual|constantTimeEqual|safeCompare|secureCompare/i.test(content);
1603
- if (hasTimingSafe) return [];
1604
1097
  for (const p of patterns) {
1605
1098
  matches.push(...findMatches(
1606
1099
  content,
1607
1100
  p,
1608
- timingAttack,
1101
+ disabledTLSVerification,
1609
1102
  filePath,
1610
- () => "Use crypto.timingSafeEqual() for comparing secrets: crypto.timingSafeEqual(Buffer.from(a), Buffer.from(b)). This prevents timing-based side-channel attacks."
1103
+ () => "Never disable TLS certificate verification in production. Fix the root cause: install the correct CA certificate, or use NODE_EXTRA_CA_CERTS for custom CAs."
1611
1104
  ));
1612
1105
  }
1613
1106
  return matches;
1614
1107
  }
1615
1108
  };
1616
- var logInjection = {
1617
- id: "VC044",
1618
- title: "Potential Log Injection",
1619
- severity: "medium",
1109
+ var dangerousInnerHTML = {
1110
+ id: "VC063",
1111
+ title: "Unsanitized dangerouslySetInnerHTML",
1112
+ severity: "critical",
1620
1113
  category: "Injection",
1621
- description: "Logging unsanitized user input allows attackers to forge log entries, inject malicious content, or exploit log aggregation systems via newlines and special characters.",
1622
- check(content, filePath) {
1623
- if (filePath.includes("test") || filePath.includes("mock")) return [];
1624
- const isServerFile = /(?:\/api\/|routes?\/|controllers?\/|server\.|middleware|handler)/i.test(filePath);
1625
- if (!isServerFile) return [];
1626
- const matches = [];
1627
- const patterns = [
1628
- /console\.(?:log|warn|error|info)\s*\([^)]*(?:req\.body|req\.query|req\.params|req\.headers)\s*\)/gi,
1629
- /(?:logger|log)\.(?:info|warn|error|debug)\s*\([^)]*(?:req\.body|req\.query|req\.params)\s*\)/gi
1630
- ];
1631
- const hasSanitization = /sanitize|escape|JSON\.stringify|replace.*\\n/i.test(content);
1632
- if (hasSanitization) return [];
1633
- for (const p of patterns) {
1634
- matches.push(...findMatches(
1635
- content,
1636
- p,
1637
- logInjection,
1638
- filePath,
1639
- () => "Sanitize user input before logging: strip newlines and control characters. Use JSON.stringify() or a structured logger (e.g., pino, winston) that escapes values automatically."
1640
- ));
1641
- }
1642
- return matches;
1643
- }
1644
- };
1645
- var weakPasswordRequirements = {
1646
- id: "VC045",
1647
- title: "Weak Password Requirements",
1648
- severity: "high",
1649
- category: "Authentication",
1650
- description: "Registration or password-change endpoints without minimum length or complexity validation allow weak passwords that are easily brute-forced.",
1651
- check(content, filePath) {
1652
- if (isTestFile(filePath)) return [];
1653
- if (!/(?:password|passwd|pwd)/i.test(content)) return [];
1654
- if (!/(?:register|signup|sign.up|createUser|create.user|changePassword|resetPassword|set.password)/i.test(content) && !/(?:\/api\/|routes?\/|controllers?\/)/i.test(filePath)) return [];
1655
- const hasValidation = /(?:password|pwd).*(?:\.length|minLength|minlength|min_length)\s*(?:>=?|<|>)\s*\d|(?:password|pwd).*(?:match|test|regex|pattern)|zxcvbn|password-validator|passwordStrength|isStrongPassword|joi\.|yup\.|zod\.|validate|schema/i.test(content);
1656
- if (hasValidation) return [];
1657
- const hasPasswordHandling = /(?:password|pwd)\s*[:=]\s*(?:req\.body|body|input|params|args)\./i.test(content);
1658
- if (!hasPasswordHandling) return [];
1659
- const rawMatches = findMatches(
1660
- content,
1661
- /(?:password|pwd)\s*[:=]\s*(?:req\.body|body|input|params|args)\./gi,
1662
- weakPasswordRequirements,
1663
- filePath,
1664
- () => "Enforce minimum password requirements: at least 8 characters, mix of letters/numbers/symbols. Use a library like zxcvbn for strength estimation."
1665
- );
1666
- const lines = content.split("\n");
1667
- const validationPattern = /\.length|minLength|minlength|min_length|match|test|regex|pattern|validate|schema|zxcvbn|isStrongPassword/i;
1668
- return rawMatches.filter((rm3) => {
1669
- const start = Math.max(0, rm3.line - 1 - 10);
1670
- const end = Math.min(lines.length, rm3.line - 1 + 10);
1671
- const nearby = lines.slice(start, end).join("\n");
1672
- return !validationPattern.test(nearby);
1673
- });
1674
- }
1675
- };
1676
- var sessionFixation = {
1677
- id: "VC046",
1678
- title: "Session Fixation Risk",
1679
- severity: "high",
1680
- category: "Authentication",
1681
- description: "Not regenerating session IDs after login allows attackers to pre-set a session ID and hijack the authenticated session.",
1682
- check(content, filePath) {
1683
- if (!/(?:login|signin|sign.in|authenticate)/i.test(content)) return [];
1684
- if (!/session/i.test(content)) return [];
1685
- const hasRegenerate = /regenerate|destroy.*create|req\.session\.id\s*=|session\.regenerateId|rotateSession/i.test(content);
1686
- if (hasRegenerate) return [];
1687
- const hasLogin = /(?:login|signin|authenticate)\s*(?:=|:|\()/i.test(content);
1688
- if (!hasLogin) return [];
1689
- return findMatches(
1690
- content,
1691
- /(?:login|signin|authenticate)\s*(?:=|:|\()/gi,
1692
- sessionFixation,
1693
- filePath,
1694
- () => "Regenerate the session ID after successful login: req.session.regenerate() (Express) or equivalent. This prevents session fixation attacks."
1695
- );
1696
- }
1697
- };
1698
- var missingBruteForce = {
1699
- id: "VC047",
1700
- title: "Login Without Brute Force Protection",
1701
- severity: "high",
1702
- category: "Authentication",
1703
- description: "Login endpoints without rate limiting, account lockout, or progressive delays are vulnerable to credential stuffing and brute force attacks.",
1114
+ description: "Using dangerouslySetInnerHTML without sanitization (DOMPurify) enables XSS attacks. User-controlled content injected as raw HTML can execute arbitrary JavaScript.",
1704
1115
  check(content, filePath) {
1705
- const isLoginFile = /(?:login|signin|sign.in|auth)/i.test(filePath) || /(?:login|signin|authenticate).*(?:post|handler|route)/i.test(content);
1706
- if (!isLoginFile) return [];
1707
- if (!/(?:password|credential)/i.test(content)) return [];
1708
- const hasBruteForce = /rate.?limit|throttle|lockout|maxAttempts|max_attempts|failedAttempts|loginAttempts|brute|express-brute|express-rate-limit|slowDown/i.test(content);
1709
- if (hasBruteForce) return [];
1116
+ if (!filePath.match(/\.(jsx|tsx)$/)) return [];
1117
+ if (!/dangerouslySetInnerHTML/i.test(content)) return [];
1118
+ const hasSanitize = /DOMPurify|sanitize|purify|xss|sanitizeHtml|isomorphic-dompurify/i.test(content);
1119
+ if (hasSanitize) return [];
1710
1120
  return findMatches(
1711
1121
  content,
1712
- /\.(post|handler)\s*\([^)]*(?:login|signin|auth)/gi,
1713
- missingBruteForce,
1122
+ /dangerouslySetInnerHTML\s*=\s*\{\s*\{/g,
1123
+ dangerousInnerHTML,
1714
1124
  filePath,
1715
- () => "Add brute force protection to login endpoints: rate limiting (5 attempts/minute), progressive delays, or account lockout after N failures. Use express-rate-limit or similar."
1125
+ () => "Sanitize HTML before using dangerouslySetInnerHTML: dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(content) }}. Install: npm install dompurify"
1716
1126
  );
1717
1127
  }
1718
1128
  };
1719
- var nosqlInjection = {
1720
- id: "VC048",
1721
- title: "Potential NoSQL Injection",
1722
- severity: "critical",
1723
- category: "Injection",
1724
- description: "Passing unsanitized user input directly into MongoDB/NoSQL queries allows attackers to bypass authentication, extract data, or modify queries using operators like $gt, $ne, $regex.",
1725
- check(content, filePath) {
1726
- if (!/(?:mongo|mongoose|findOne|findById|find\(|collection|aggregate)/i.test(content)) return [];
1727
- const matches = [];
1728
- const patterns = [
1729
- // Direct req.body in MongoDB queries
1730
- /\.find(?:One)?\s*\(\s*(?:req\.body|body|input|params)\s*\)/gi,
1731
- /\.find(?:One)?\s*\(\s*\{[^}]*:\s*(?:req\.body|body|input|params)\./gi,
1732
- // $where with user input
1733
- /\$where\s*:\s*(?!["'`])/g,
1734
- // Direct variable in query without sanitization
1735
- /\.(?:findOne|findById|deleteOne|updateOne|findOneAndUpdate)\s*\(\s*\{[^}]*:\s*(?:req\.(?:body|query|params))\./gi
1736
- ];
1737
- const hasSanitization = /sanitize|escape|mongo-sanitize|express-mongo-sanitize|validator|typeof.*===.*string/i.test(content);
1738
- if (hasSanitization) return [];
1739
- for (const p of patterns) {
1740
- matches.push(...findMatches(
1741
- content,
1742
- p,
1743
- nosqlInjection,
1744
- filePath,
1745
- () => "Sanitize MongoDB query inputs: use express-mongo-sanitize, validate types (ensure strings aren't objects), and avoid $where. Example: if (typeof input !== 'string') throw new Error('Invalid input');"
1746
- ));
1747
- }
1748
- return matches;
1749
- }
1750
- };
1751
- var exposedDBCredentials = {
1752
- id: "VC049",
1753
- title: "Database Credentials in Config File",
1754
- severity: "critical",
1755
- category: "Secrets",
1756
- description: "Database connection strings with embedded usernames and passwords in committed config files expose credentials to anyone with repo access.",
1757
- check(content, filePath) {
1758
- if (filePath.endsWith(".example") || filePath.endsWith(".template")) return [];
1759
- if (!filePath.match(/(?:config|setting|database|db|knexfile|sequelize|drizzle|prisma)/i) && !filePath.match(/\.(json|yaml|yml|toml|js|ts)$/)) return [];
1760
- if (filePath.match(/\.env/)) return [];
1761
- const patterns = [
1762
- // Connection strings with credentials
1763
- /(?:host|server|database|db).*(?:password|passwd|pwd)\s*[:=]\s*["'`][^"'`]{3,}["'`]/gi,
1764
- // Inline connection URLs with credentials
1765
- /(?:connection|database|db).*(?:postgres|mysql|mongodb|redis):\/\/[^:]+:[^@]+@/gi
1766
- ];
1767
- const matches = [];
1768
- for (const p of patterns) {
1769
- matches.push(...findMatches(
1770
- content,
1771
- p,
1772
- exposedDBCredentials,
1773
- filePath,
1774
- () => "Move database credentials to environment variables. Use: process.env.DATABASE_URL instead of hardcoding connection strings in config files."
1775
- ));
1776
- }
1777
- return matches;
1778
- }
1779
- };
1780
- var missingDBEncryption = {
1781
- id: "VC050",
1782
- title: "Database Connection Without SSL/TLS",
1783
- severity: "high",
1784
- category: "Configuration",
1785
- description: "Database connections without SSL/TLS encryption transmit credentials and data in plaintext, allowing eavesdropping on the network.",
1786
- check(content, filePath) {
1787
- if (!/(?:createConnection|createPool|createClient|connect|new.*Client|knex|sequelize|drizzle)/i.test(content)) return [];
1788
- if (!/(?:postgres|mysql|mariadb|pg|mongo)/i.test(content)) return [];
1789
- const matches = [];
1790
- const sslDisabled = [
1791
- /ssl\s*:\s*false/gi,
1792
- /sslmode\s*[:=]\s*["'`]?disable["'`]?/gi,
1793
- /rejectUnauthorized\s*:\s*false/gi
1794
- ];
1795
- for (const p of sslDisabled) {
1796
- matches.push(...findMatches(
1797
- content,
1798
- p,
1799
- missingDBEncryption,
1800
- filePath,
1801
- () => "Enable SSL/TLS for database connections: { ssl: { rejectUnauthorized: true } }. In production, always verify server certificates."
1802
- ));
1803
- }
1804
- return matches;
1805
- }
1806
- };
1807
- var graphqlIntrospection = {
1808
- id: "VC051",
1809
- title: "GraphQL Introspection Enabled in Production",
1810
- severity: "medium",
1811
- category: "Information Leakage",
1812
- description: "GraphQL introspection exposes your entire API schema, types, queries, and mutations to attackers, making it easy to find attack vectors.",
1813
- check(content, filePath) {
1814
- if (!/graphql/i.test(content) && !/graphql/i.test(filePath)) return [];
1815
- const matches = [];
1816
- if (/introspection\s*:\s*true/i.test(content)) {
1817
- matches.push(...findMatches(
1818
- content,
1819
- /introspection\s*:\s*true/gi,
1820
- graphqlIntrospection,
1821
- filePath,
1822
- () => "Disable GraphQL introspection in production: introspection: process.env.NODE_ENV !== 'production'. This prevents schema exposure."
1823
- ));
1824
- }
1825
- if (/(?:ApolloServer|GraphQLServer|createYoga|buildSchema|makeExecutableSchema)\s*\(/i.test(content)) {
1826
- if (!/introspection/i.test(content)) {
1827
- matches.push(...findMatches(
1828
- content,
1829
- /(?:ApolloServer|GraphQLServer|createYoga)\s*\(/gi,
1830
- graphqlIntrospection,
1831
- filePath,
1832
- () => "Explicitly disable introspection in production: new ApolloServer({ introspection: process.env.NODE_ENV !== 'production' })"
1833
- ));
1834
- }
1835
- }
1836
- return matches;
1837
- }
1838
- };
1839
- var missingRequestSizeLimit = {
1840
- id: "VC052",
1841
- title: "Missing Request Body Size Limit",
1842
- severity: "medium",
1843
- category: "Availability",
1844
- description: "Express/Hono/Fastify servers without request body size limits are vulnerable to denial-of-service via oversized payloads that exhaust memory.",
1845
- check(content, filePath) {
1846
- if (!/(?:server|app|index|main)\.[jt]sx?$/.test(filePath)) return [];
1847
- if (!/(?:express|hono|fastify|koa)/i.test(content)) return [];
1848
- const matches = [];
1849
- if (/express\.json\s*\(\s*\)/g.test(content)) {
1850
- matches.push(...findMatches(
1851
- content,
1852
- /express\.json\s*\(\s*\)/g,
1853
- missingRequestSizeLimit,
1854
- filePath,
1855
- () => "Set a body size limit: express.json({ limit: '1mb' }). Without this, attackers can send huge payloads to crash your server."
1856
- ));
1857
- }
1858
- if (/bodyParser\.json\s*\(\s*\)/g.test(content)) {
1859
- matches.push(...findMatches(
1860
- content,
1861
- /bodyParser\.json\s*\(\s*\)/g,
1862
- missingRequestSizeLimit,
1863
- filePath,
1864
- () => "Set a body size limit: bodyParser.json({ limit: '1mb' })."
1865
- ));
1866
- }
1867
- return matches;
1868
- }
1869
- };
1870
- var hardcodedIPAllowlist = {
1871
- id: "VC053",
1872
- title: "Hardcoded IP or Host Allowlist",
1873
- severity: "medium",
1874
- category: "Configuration",
1875
- description: "Hardcoded IP addresses or hostnames in allowlists are brittle and hard to update. They should be in environment variables or configuration files.",
1876
- check(content, filePath) {
1877
- if (filePath.includes("test") || filePath.includes("mock") || filePath.match(/\.(md|txt)$/)) return [];
1878
- const matches = [];
1879
- const patterns = [
1880
- /(?:allowedIPs|allowed_ips|whitelist|allowlist|trustedHosts)\s*[:=]\s*\[\s*["'`]\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/gi,
1881
- /(?:allowedIPs|allowed_ips|whitelist|allowlist|trustedHosts)\s*[:=]\s*\[\s*["'`][\w.-]+\.(?:com|net|org|io)/gi
1882
- ];
1883
- for (const p of patterns) {
1884
- matches.push(...findMatches(
1885
- content,
1886
- p,
1887
- hardcodedIPAllowlist,
1888
- filePath,
1889
- () => "Move IP/host allowlists to environment variables or a config file: const allowed = process.env.ALLOWED_IPS?.split(',') || [];"
1890
- ));
1891
- }
1892
- return matches;
1129
+ function detectFramework(files) {
1130
+ const frameworks = /* @__PURE__ */ new Set();
1131
+ for (const { path, content } of files) {
1132
+ if (path.match(/next\.config/i) || /from\s+["']next/i.test(content)) frameworks.add("next.js");
1133
+ if (/from\s+["']react-native/i.test(content) || path.match(/react-native\.config/i)) frameworks.add("react-native");
1134
+ else if (/from\s+["']react/i.test(content) || /import\s+React/i.test(content)) frameworks.add("react");
1135
+ if (/from\s+["']express/i.test(content) || /require\s*\(\s*["']express/i.test(content)) frameworks.add("express");
1136
+ if (/from\s+["']hono/i.test(content)) frameworks.add("hono");
1137
+ if (/from\s+["']fastify/i.test(content)) frameworks.add("fastify");
1138
+ if (/from\s+["']electron/i.test(content) || path.match(/electron/i)) frameworks.add("electron");
1139
+ if (path.match(/settings\.py$/) || /from\s+django/i.test(content)) frameworks.add("django");
1140
+ if (/from\s+flask/i.test(content) || /Flask\s*\(/i.test(content)) frameworks.add("flask");
1141
+ if (/from\s+["']vue/i.test(content) || path.match(/vue\.config/i)) frameworks.add("vue");
1142
+ if (/from\s+["']svelte/i.test(content) || path.match(/svelte\.config/i)) frameworks.add("svelte");
1893
1143
  }
1894
- };
1895
- var sensitiveLocalStorage = {
1896
- id: "VC054",
1897
- title: "Sensitive Data in localStorage",
1898
- severity: "high",
1899
- category: "Secrets",
1900
- description: "Storing tokens, passwords, or secrets in localStorage is insecure \u2014 it's accessible to any JavaScript on the page (XSS) and persists indefinitely. Use httpOnly cookies instead.",
1901
- check(content, filePath) {
1902
- if (!filePath.match(/\.(jsx?|tsx?|vue|svelte)$/)) return [];
1903
- if (isTestFile(filePath)) return [];
1904
- if (/mock|spec/i.test(filePath)) return [];
1905
- if (!/localStorage\.setItem|localStorage\[/i.test(content)) return [];
1906
- const matches = [];
1907
- const patterns = [
1908
- /localStorage\.setItem\s*\(\s*["'`](?:token|access_token|auth_token|jwt|session|refresh_token|api_key|password|secret)/gi,
1909
- /localStorage\s*\[\s*["'`](?:token|access_token|auth_token|jwt|session|refresh_token|api_key|password|secret)/gi
1910
- ];
1911
- for (const p of patterns) {
1912
- matches.push(...findMatches(
1913
- content,
1914
- p,
1915
- sensitiveLocalStorage,
1916
- filePath,
1917
- () => "Don't store tokens/secrets in localStorage \u2014 use httpOnly cookies instead. localStorage is accessible to any XSS attack. For session tokens, set them as httpOnly, secure, sameSite cookies."
1918
- ));
1919
- }
1920
- return matches;
1144
+ if (frameworks.size === 0) frameworks.add("unknown");
1145
+ return [...frameworks];
1146
+ }
1147
+ function calculateGrade(findings, totalFiles) {
1148
+ if (findings.length === 0) {
1149
+ return { grade: "A+", score: 100, summary: "No security issues detected. Excellent!" };
1921
1150
  }
1922
- };
1923
- var exposedSourceMaps = {
1924
- id: "VC055",
1925
- title: "Source Maps Exposed in Production",
1926
- severity: "medium",
1927
- category: "Information Leakage",
1928
- description: "Source map files (.map) in production expose your original source code, comments, and internal logic to anyone who downloads them.",
1929
- check(content, filePath) {
1930
- if (!filePath.match(/(?:webpack|vite|rollup|next)\.config|tsconfig/i)) return [];
1931
- const matches = [];
1932
- if (/(?:sourceMap|source-map|sourcemap)\s*[:=]\s*true/i.test(content)) {
1933
- const hasEnvCheck = /process\.env\.NODE_ENV|NODE_ENV|production/i.test(content);
1934
- if (!hasEnvCheck) {
1935
- matches.push(...findMatches(
1936
- content,
1937
- /(?:sourceMap|source-map|sourcemap)\s*[:=]\s*true/gi,
1938
- exposedSourceMaps,
1939
- filePath,
1940
- () => "Disable source maps in production builds: sourceMap: process.env.NODE_ENV !== 'production'. Or use 'hidden-source-map' to generate maps without exposing them."
1941
- ));
1942
- }
1943
- }
1944
- if (/productionSourceMap\s*:\s*true/i.test(content)) {
1945
- matches.push(...findMatches(
1946
- content,
1947
- /productionSourceMap\s*:\s*true/gi,
1948
- exposedSourceMaps,
1949
- filePath,
1950
- () => "Set productionSourceMap: false to avoid exposing source code in production."
1951
- ));
1151
+ let deductions = 0;
1152
+ for (const f of findings) {
1153
+ switch (f.severity) {
1154
+ case "critical":
1155
+ deductions += 15;
1156
+ break;
1157
+ case "high":
1158
+ deductions += 8;
1159
+ break;
1160
+ case "medium":
1161
+ deductions += 4;
1162
+ break;
1163
+ case "low":
1164
+ deductions += 1;
1165
+ break;
1952
1166
  }
1953
- return matches;
1954
1167
  }
1955
- };
1956
- var clickjacking = {
1957
- id: "VC056",
1958
- title: "Clickjacking \u2014 Missing X-Frame-Options",
1959
- severity: "medium",
1960
- category: "Configuration",
1961
- description: "Without X-Frame-Options or frame-ancestors CSP directive, your page can be embedded in an attacker's iframe for UI redress (clickjacking) attacks.",
1962
- check(content, filePath) {
1963
- if (filePath.match(/\.(html|htm)$/)) {
1964
- if (!/X-Frame-Options|frame-ancestors/i.test(content)) {
1965
- return [{
1966
- rule: "VC056",
1967
- title: clickjacking.title,
1968
- severity: "medium",
1969
- category: "Configuration",
1970
- file: filePath,
1971
- line: 1,
1972
- snippet: getSnippet(content, 1),
1973
- fix: 'Add <meta http-equiv="X-Frame-Options" content="DENY"> or set frame-ancestors in CSP to prevent clickjacking.'
1974
- }];
1975
- }
1976
- }
1977
- if (/(?:server|app|index|main)\.[jt]sx?$/.test(filePath)) {
1978
- if (/(?:express|hono|fastify|koa)/i.test(content)) {
1979
- if (!/X-Frame-Options|frame-ancestors|helmet/i.test(content)) {
1980
- return findMatches(
1981
- content,
1982
- /(?:express|hono|fastify|koa)\s*\(/gi,
1983
- clickjacking,
1984
- filePath,
1985
- () => "Add X-Frame-Options header: res.setHeader('X-Frame-Options', 'DENY'). Or use helmet: app.use(helmet()) which sets this and other security headers."
1986
- );
1987
- }
1988
- }
1989
- }
1990
- return [];
1991
- }
1992
- };
1993
- var overlyPermissiveIAM = {
1994
- id: "VC057",
1995
- title: "Overly Permissive IAM/Cloud Permissions",
1996
- severity: "critical",
1997
- category: "Authorization",
1998
- description: "Wildcard (*) permissions in AWS IAM, GCP, or Terraform configs grant unrestricted access, violating the principle of least privilege.",
1999
- check(content, filePath) {
2000
- if (!filePath.match(/\.(tf|hcl|json|yaml|yml)$/) && !filePath.match(/(?:iam|policy|role|permission)/i)) return [];
2001
- const matches = [];
2002
- const patterns = [
2003
- // AWS IAM wildcard
2004
- /["'`]Action["'`]\s*:\s*["'`]\*["'`]/g,
2005
- /["'`]Resource["'`]\s*:\s*["'`]\*["'`]/g,
2006
- // Terraform aws_iam
2007
- /actions\s*=\s*\[\s*["'`]\*["'`]\s*\]/g,
2008
- /resources\s*=\s*\[\s*["'`]\*["'`]\s*\]/g,
2009
- // GCP bindings
2010
- /role\s*[:=]\s*["'`]roles\/(?:owner|editor)["'`]/g
2011
- ];
2012
- for (const p of patterns) {
2013
- matches.push(...findMatches(
2014
- content,
2015
- p,
2016
- overlyPermissiveIAM,
2017
- filePath,
2018
- () => "Follow the principle of least privilege: replace wildcard (*) with specific actions and resources. Example: 'Action': 's3:GetObject' instead of '*'."
2019
- ));
2020
- }
2021
- return matches;
2022
- }
2023
- };
2024
- var dockerRunAsRoot = {
2025
- id: "VC058",
2026
- title: "Docker Container Running as Root",
2027
- severity: "high",
2028
- category: "Configuration",
2029
- description: "Containers running as root give attackers full system access if they escape the container. Always run as a non-root user.",
2030
- check(content, filePath) {
2031
- if (!filePath.match(/Dockerfile$/i)) return [];
2032
- const hasUser = /^\s*USER\s+/m.test(content);
2033
- if (hasUser) return [];
2034
- return [{
2035
- rule: "VC058",
2036
- title: dockerRunAsRoot.title,
2037
- severity: "high",
2038
- category: "Configuration",
2039
- file: filePath,
2040
- line: 1,
2041
- snippet: getSnippet(content, 1),
2042
- fix: "Add a USER directive: RUN addgroup -S app && adduser -S app -G app\\nUSER app. Place it after installing dependencies but before COPY/CMD."
2043
- }];
2044
- }
2045
- };
2046
- var exposedDockerPorts = {
2047
- id: "VC059",
2048
- title: "Docker Compose Binding to All Interfaces",
2049
- severity: "medium",
2050
- category: "Configuration",
2051
- description: "Binding ports to 0.0.0.0 (default) in Docker Compose exposes services to the entire network. Bind to 127.0.0.1 for local-only access.",
2052
- check(content, filePath) {
2053
- if (!filePath.match(/docker-compose|compose\.(yaml|yml)$/i)) return [];
2054
- const matches = [];
2055
- const portPattern = /ports:\s*\n(?:\s*-\s*["'`]?\d+:\d+["'`]?\s*\n?)+/g;
2056
- if (portPattern.test(content) && !/127\.0\.0\.1:/i.test(content)) {
2057
- matches.push(...findMatches(
2058
- content,
2059
- /^\s*-\s*["'`]?\d+:\d+["'`]?/gm,
2060
- exposedDockerPorts,
2061
- filePath,
2062
- () => "Bind to localhost only: '127.0.0.1:3000:3000' instead of '3000:3000'. This prevents external network access to the service."
2063
- ));
2064
- }
2065
- return matches;
2066
- }
2067
- };
2068
- var weakHashing = {
2069
- id: "VC060",
2070
- title: "Weak Hashing Algorithm for Passwords",
2071
- severity: "critical",
2072
- category: "Cryptography",
2073
- description: "MD5 and SHA1/SHA256 are too fast for password hashing \u2014 they can be brute-forced at billions of attempts per second. Use bcrypt, scrypt, or argon2 instead.",
2074
- check(content, filePath) {
2075
- if (filePath.includes("test") || filePath.includes("mock")) return [];
2076
- if (/bcrypt|scrypt|argon2/i.test(content)) return [];
2077
- const matches = [];
2078
- const patterns = [
2079
- /(?:md5|sha1|sha256|sha512)\s*\([^)]*(?:password|passwd|pwd)/gi,
2080
- /createHash\s*\(\s*["'`](?:md5|sha1|sha256)["'`]\).*(?:password|passwd|pwd)/gi,
2081
- /(?:password|passwd|pwd).*createHash\s*\(\s*["'`](?:md5|sha1|sha256)["'`]\)/gi,
2082
- /hashlib\.(?:md5|sha1|sha256)\s*\([^)]*(?:password|passwd|pwd)/gi,
2083
- /Digest::(?:MD5|SHA1|SHA256).*(?:password|passwd|pwd)/gi
2084
- ];
2085
- for (const p of patterns) {
2086
- matches.push(...findMatches(
2087
- content,
2088
- p,
2089
- weakHashing,
2090
- filePath,
2091
- () => "Use bcrypt, scrypt, or argon2 for password hashing \u2014 they're intentionally slow. Example: const hash = await bcrypt.hash(password, 12);"
2092
- ));
2093
- }
2094
- return matches;
2095
- }
2096
- };
2097
- var disabledTLSVerification = {
2098
- id: "VC061",
2099
- title: "Disabled TLS Certificate Verification",
2100
- severity: "critical",
2101
- category: "Cryptography",
2102
- description: "Disabling TLS certificate verification (NODE_TLS_REJECT_UNAUTHORIZED=0 or rejectUnauthorized:false) makes all HTTPS connections vulnerable to man-in-the-middle attacks.",
2103
- check(content, filePath) {
2104
- const matches = [];
2105
- const patterns = [
2106
- /NODE_TLS_REJECT_UNAUTHORIZED\s*[:=]\s*["'`]?0["'`]?/g,
2107
- /rejectUnauthorized\s*:\s*false/g,
2108
- /verify\s*[:=]\s*false.*(?:ssl|tls|cert|https)/gi,
2109
- /PYTHONHTTPSVERIFY\s*[:=]\s*["'`]?0["'`]?/g,
2110
- /ssl_verify\s*[:=]\s*false/gi,
2111
- /InsecureSkipVerify\s*:\s*true/g
2112
- ];
2113
- for (const p of patterns) {
2114
- matches.push(...findMatches(
2115
- content,
2116
- p,
2117
- disabledTLSVerification,
2118
- filePath,
2119
- () => "Never disable TLS certificate verification in production. Fix the root cause: install the correct CA certificate, or use NODE_EXTRA_CA_CERTS for custom CAs."
2120
- ));
2121
- }
2122
- return matches;
2123
- }
2124
- };
2125
- var hardcodedEncryptionKey = {
2126
- id: "VC062",
2127
- title: "Hardcoded Encryption Key or IV",
2128
- severity: "critical",
2129
- category: "Cryptography",
2130
- description: "Hardcoded encryption keys and initialization vectors (IVs) in source code can be extracted to decrypt all data. IVs must be random per encryption operation.",
2131
- check(content, filePath) {
2132
- if (filePath.endsWith(".example") || filePath.endsWith(".template")) return [];
2133
- if (isTestFile(filePath)) return [];
2134
- if (filePath.match(/\.(html|htm|xml|plist|svg|xhtml)$/)) return [];
2135
- if (!/(?:cipher|encrypt|decrypt|crypto|aes|createCipher)/i.test(content)) return [];
2136
- const matches = [];
2137
- const patterns = [
2138
- // Encryption key as string literal
2139
- /(?:encryption_key|encryptionKey|cipher_key|cipherKey|aes_key|AES_KEY|ENCRYPTION_KEY)\s*[:=]\s*["'`][^"'`]{8,}["'`]/g,
2140
- // createCipheriv with hardcoded key
2141
- /createCipher(?:iv)?\s*\(\s*["'`][^"'`]+["'`]\s*,\s*["'`][^"'`]+["'`]/g,
2142
- // Buffer.from with hardcoded key near cipher context
2143
- /(?:key|iv|nonce)\s*[:=]\s*Buffer\.from\s*\(\s*["'`][^"'`]{8,}["'`]/gi,
2144
- // Static IV (should be random) — only in crypto context
2145
- /(?:^|[\s,({])(?:iv|nonce|initialVector)\s*[:=]\s*["'`][0-9a-fA-F]{16,}["'`]/gi
2146
- ];
2147
- for (const p of patterns) {
2148
- const rawMatches = findMatches(
2149
- content,
2150
- p,
2151
- hardcodedEncryptionKey,
2152
- filePath,
2153
- () => "Move encryption keys to environment variables. Generate IVs randomly per operation: crypto.randomBytes(16). Never reuse IVs."
2154
- );
2155
- for (const rm3 of rawMatches) {
2156
- const lineStart = content.lastIndexOf("\n", content.split("\n").slice(0, rm3.line - 1).join("\n").length) + 1;
2157
- const lineEnd = content.indexOf("\n", lineStart + 1);
2158
- const lineText = content.substring(lineStart, lineEnd === -1 ? content.length : lineEnd);
2159
- if (/meta\s+http-equiv|Content-Security-Policy|X-Frame-Options|X-Content-Type/i.test(lineText)) continue;
2160
- matches.push(rm3);
2161
- }
2162
- }
2163
- return matches;
2164
- }
2165
- };
2166
- var dangerousInnerHTML = {
2167
- id: "VC063",
2168
- title: "Unsanitized dangerouslySetInnerHTML",
2169
- severity: "critical",
2170
- category: "Injection",
2171
- description: "Using dangerouslySetInnerHTML without sanitization (DOMPurify) enables XSS attacks. User-controlled content injected as raw HTML can execute arbitrary JavaScript.",
2172
- check(content, filePath) {
2173
- if (!filePath.match(/\.(jsx|tsx)$/)) return [];
2174
- if (!/dangerouslySetInnerHTML/i.test(content)) return [];
2175
- const hasSanitize = /DOMPurify|sanitize|purify|xss|sanitizeHtml|isomorphic-dompurify/i.test(content);
2176
- if (hasSanitize) return [];
2177
- return findMatches(
2178
- content,
2179
- /dangerouslySetInnerHTML\s*=\s*\{\s*\{/g,
2180
- dangerousInnerHTML,
2181
- filePath,
2182
- () => "Sanitize HTML before using dangerouslySetInnerHTML: dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(content) }}. Install: npm install dompurify"
2183
- );
2184
- }
2185
- };
2186
- var exposedServerActions = {
2187
- id: "VC064",
2188
- title: "Next.js Server Action Without Auth Check",
2189
- severity: "high",
2190
- category: "Authorization",
2191
- description: "Next.js Server Actions ('use server') are publicly callable endpoints. Without authentication checks, anyone can invoke them directly.",
2192
- check(content, filePath) {
2193
- if (!filePath.match(/\.(jsx?|tsx?)$/)) return [];
2194
- if (!/["']use server["']/i.test(content)) return [];
2195
- const hasAuth = /getServerSession|auth\(\)|currentUser|getUser|requireAuth|session|clerk|getAuth/i.test(content);
2196
- if (hasAuth) return [];
2197
- if (/export\s+async\s+function/i.test(content)) {
2198
- return findMatches(
2199
- content,
2200
- /export\s+async\s+function\s+\w+/g,
2201
- exposedServerActions,
2202
- filePath,
2203
- () => "Add authentication to Server Actions: const session = await getServerSession(); if (!session) throw new Error('Unauthorized');"
2204
- );
2205
- }
2206
- return [];
2207
- }
2208
- };
2209
- var unprotectedAPIRoutes = {
2210
- id: "VC065",
2211
- title: "Unprotected Next.js API Route",
2212
- severity: "high",
2213
- category: "Authorization",
2214
- description: "Next.js API routes under /api/ without authentication middleware can be called by anyone, exposing data or mutations.",
2215
- check(content, filePath) {
2216
- if (!filePath.match(/\/api\/.*\.(jsx?|tsx?)$/) && !filePath.match(/\/app\/api\/.*route\.(jsx?|tsx?)$/)) return [];
2217
- if (/health|status|public|webhook/i.test(filePath)) return [];
2218
- const hasAuth = /getServerSession|auth\(\)|currentUser|getUser|requireAuth|session|clerk|getAuth|verifyToken|authenticate|middleware/i.test(content);
2219
- if (hasAuth) return [];
2220
- const hasHandler = /export\s+(?:async\s+)?function\s+(?:GET|POST|PUT|DELETE|PATCH)|export\s+default/i.test(content);
2221
- if (hasHandler) {
2222
- return findMatches(
2223
- content,
2224
- /export\s+(?:async\s+)?function\s+(?:GET|POST|PUT|DELETE|PATCH)/g,
2225
- unprotectedAPIRoutes,
2226
- filePath,
2227
- () => "Add authentication to API routes: const session = await getServerSession(authOptions); if (!session) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });"
2228
- );
2229
- }
2230
- return [];
2231
- }
2232
- };
2233
- var clientComponentSecret = {
2234
- id: "VC066",
2235
- title: "Secret Used in Client Component",
2236
- severity: "critical",
2237
- category: "Secrets",
2238
- description: "Using server-side secrets (process.env without NEXT_PUBLIC_) in 'use client' components exposes them in the browser bundle.",
2239
- check(content, filePath) {
2240
- if (!filePath.match(/\.(jsx?|tsx?)$/)) return [];
2241
- if (!/["']use client["']/i.test(content)) return [];
2242
- const pattern = /process\.env\.(?!NEXT_PUBLIC_)[A-Z_]{3,}/g;
2243
- if (pattern.test(content)) {
2244
- return findMatches(
2245
- content,
2246
- /process\.env\.(?!NEXT_PUBLIC_)[A-Z_]{3,}/g,
2247
- clientComponentSecret,
2248
- filePath,
2249
- () => "Server-side env vars are not available in client components and may leak in builds. Move this logic to a Server Component or API route."
2250
- );
2251
- }
2252
- return [];
2253
- }
2254
- };
2255
- var insecureDeepLink = {
2256
- id: "VC067",
2257
- title: "Insecure Deep Link Handling",
2258
- severity: "high",
2259
- category: "Injection",
2260
- description: "Deep links that navigate or execute actions without validating the URL scheme, host, or parameters can be exploited for phishing or unauthorized actions.",
2261
- check(content, filePath) {
2262
- if (!/(?:Linking|DeepLinking|deep.?link|handleURL|openURL|url.?scheme)/i.test(content)) return [];
2263
- const matches = [];
2264
- const patterns = [
2265
- /Linking\.addEventListener\s*\([^)]*(?:url|link)/gi,
2266
- /Linking\.getInitialURL\s*\(\)/g,
2267
- /handleOpenURL|handleDeepLink|onDeepLink/gi
2268
- ];
2269
- const hasValidation = /allowedSchemes|allowedHosts|validateURL|isAllowedURL|whitelist|URL.*hostname.*includes/i.test(content);
2270
- if (hasValidation) return [];
2271
- for (const p of patterns) {
2272
- matches.push(...findMatches(
2273
- content,
2274
- p,
2275
- insecureDeepLink,
2276
- filePath,
2277
- () => "Validate deep link URLs: check the scheme and host against an allowlist before navigating or executing actions."
2278
- ));
2279
- }
2280
- return matches;
2281
- }
2282
- };
2283
- var sensitiveAsyncStorage = {
2284
- id: "VC068",
2285
- title: "Sensitive Data in AsyncStorage",
2286
- severity: "high",
2287
- category: "Secrets",
2288
- description: "React Native AsyncStorage is unencrypted. Storing tokens, passwords, or secrets there makes them readable by other apps or anyone with device access.",
2289
- check(content, filePath) {
2290
- if (!/AsyncStorage/i.test(content)) return [];
2291
- const patterns = [
2292
- /AsyncStorage\.setItem\s*\(\s*["'`](?:token|access_token|auth_token|jwt|session|refresh_token|api_key|password|secret|private_key)/gi
2293
- ];
2294
- const matches = [];
2295
- for (const p of patterns) {
2296
- matches.push(...findMatches(
2297
- content,
2298
- p,
2299
- sensitiveAsyncStorage,
2300
- filePath,
2301
- () => "Use react-native-keychain or expo-secure-store instead of AsyncStorage for sensitive data. These use the OS keychain (encrypted)."
2302
- ));
2303
- }
2304
- return matches;
2305
- }
2306
- };
2307
- var missingCertPinning = {
2308
- id: "VC069",
2309
- title: "Missing Certificate Pinning in Mobile App",
2310
- severity: "medium",
2311
- category: "Cryptography",
2312
- description: "Mobile apps without SSL certificate pinning are vulnerable to MITM attacks via compromised or rogue CAs. Pin your API server's certificate.",
2313
- check(content, filePath) {
2314
- if (!/(?:react.native|expo|android|ios|mobile)/i.test(filePath) && !/(?:React.*Native|expo)/i.test(content)) return [];
2315
- if (!/(?:fetch|axios|http|api|request)/i.test(content)) return [];
2316
- if (/axios\.create|new\s+(?:HttpClient|ApiClient)/i.test(content)) {
2317
- const hasPinning = /pinning|certificate|cert|ssl|TrustKit|react-native-ssl-pinning|cert-pinner/i.test(content);
2318
- if (!hasPinning) {
2319
- return findMatches(
2320
- content,
2321
- /axios\.create|new\s+(?:HttpClient|ApiClient)/gi,
2322
- missingCertPinning,
2323
- filePath,
2324
- () => "Add SSL certificate pinning: use react-native-ssl-pinning or TrustKit. This prevents MITM attacks via rogue certificates."
2325
- );
2326
- }
2327
- }
2328
- return [];
2329
- }
2330
- };
2331
- var androidDebuggable = {
2332
- id: "VC070",
2333
- title: "Android App Debuggable in Production",
2334
- severity: "high",
2335
- category: "Configuration",
2336
- description: "android:debuggable='true' in AndroidManifest.xml allows attackers to attach debuggers, inspect memory, and bypass security controls.",
2337
- check(content, filePath) {
2338
- if (!filePath.match(/AndroidManifest\.xml$/i)) return [];
2339
- if (/android:debuggable\s*=\s*["']true["']/i.test(content)) {
2340
- return findMatches(
2341
- content,
2342
- /android:debuggable\s*=\s*["']true["']/gi,
2343
- androidDebuggable,
2344
- filePath,
2345
- () => "Remove android:debuggable='true' or set it to false. Debug builds should use build variants, not manifest flags."
2346
- );
2347
- }
2348
- return [];
2349
- }
2350
- };
2351
- var djangoDebug = {
2352
- id: "VC071",
2353
- title: "Django DEBUG Mode Enabled",
2354
- severity: "critical",
2355
- category: "Configuration",
2356
- description: "Django with DEBUG=True exposes detailed error pages with source code, database queries, environment variables, and installed apps to anyone.",
2357
- check(content, filePath) {
2358
- if (!filePath.match(/settings\.py$/i) && !filePath.match(/config.*\.py$/i)) return [];
2359
- if (/^\s*DEBUG\s*=\s*True\s*$/m.test(content)) {
2360
- const hasEnvCheck = /os\.environ|env\(|config\(|getenv/i.test(content);
2361
- if (!hasEnvCheck) {
2362
- return findMatches(
2363
- content,
2364
- /^\s*DEBUG\s*=\s*True/gm,
2365
- djangoDebug,
2366
- filePath,
2367
- () => "Use environment variable: DEBUG = os.environ.get('DEBUG', 'False') == 'True'. Never hardcode DEBUG=True."
2368
- );
2369
- }
2370
- }
2371
- return [];
2372
- }
2373
- };
2374
- var flaskSecretKey = {
2375
- id: "VC072",
2376
- title: "Flask Secret Key Hardcoded",
2377
- severity: "critical",
2378
- category: "Secrets",
2379
- description: "Hardcoded Flask secret_key allows attackers to forge sessions, CSRF tokens, and signed cookies. Must be a random value from environment variables.",
2380
- check(content, filePath) {
2381
- if (!filePath.match(/\.py$/)) return [];
2382
- const patterns = [
2383
- /(?:app\.)?secret_key\s*=\s*["'`][^"'`]{3,}["'`]/g,
2384
- /(?:app\.config)\s*\[\s*["']SECRET_KEY["']\s*\]\s*=\s*["'`][^"'`]{3,}["'`]/g
2385
- ];
2386
- const hasEnv = /os\.environ|env\(|config\(|getenv/i.test(content);
2387
- if (hasEnv) return [];
2388
- const matches = [];
2389
- for (const p of patterns) {
2390
- matches.push(...findMatches(
2391
- content,
2392
- p,
2393
- flaskSecretKey,
2394
- filePath,
2395
- () => `Use environment variable: app.secret_key = os.environ['SECRET_KEY']. Generate with: python -c "import secrets; print(secrets.token_hex(32))"`
2396
- ));
2397
- }
2398
- return matches;
2399
- }
2400
- };
2401
- var pickleDeserialization = {
2402
- id: "VC073",
2403
- title: "Unsafe Pickle Deserialization",
2404
- severity: "critical",
2405
- category: "Injection",
2406
- description: "pickle.loads() on untrusted data allows arbitrary code execution. An attacker can craft a pickle payload that runs system commands on your server.",
2407
- check(content, filePath) {
2408
- if (!filePath.match(/\.py$/)) return [];
2409
- const matches = [];
2410
- const patterns = [
2411
- /pickle\.loads?\s*\(/g,
2412
- /cPickle\.loads?\s*\(/g,
2413
- /shelve\.open\s*\(/g,
2414
- /yaml\.load\s*\([^)]*(?!Loader\s*=\s*yaml\.SafeLoader)/g
2415
- ];
2416
- const hasSafe = /restricted_loads|SafeUnpickler|safe_load|yaml\.safe_load/i.test(content);
2417
- if (hasSafe) return [];
2418
- for (const p of patterns) {
2419
- matches.push(...findMatches(
2420
- content,
2421
- p,
2422
- pickleDeserialization,
2423
- filePath,
2424
- () => "Never unpickle untrusted data \u2014 it allows arbitrary code execution. Use JSON for data exchange, or yaml.safe_load() for YAML."
2425
- ));
2426
- }
2427
- return matches;
2428
- }
2429
- };
2430
- var missingCSRF = {
2431
- id: "VC074",
2432
- title: "CSRF Protection Disabled",
2433
- severity: "high",
2434
- category: "Authorization",
2435
- description: "Using @csrf_exempt on state-changing views (POST/PUT/DELETE) allows attackers to forge requests from other sites, performing actions as authenticated users.",
2436
- check(content, filePath) {
2437
- if (!filePath.match(/\.py$/)) return [];
2438
- const matches = [];
2439
- if (/csrf_exempt/i.test(content)) {
2440
- matches.push(...findMatches(
2441
- content,
2442
- /@csrf_exempt/g,
2443
- missingCSRF,
2444
- filePath,
2445
- () => "Remove @csrf_exempt and use proper CSRF tokens. For APIs, use token-based auth (JWT) instead of session cookies."
2446
- ));
2447
- }
2448
- if (/MIDDLEWARE.*=.*\[/s.test(content) && !/CsrfViewMiddleware/i.test(content) && /django/i.test(content)) {
2449
- matches.push(...findMatches(
2450
- content,
2451
- /MIDDLEWARE\s*=/g,
2452
- missingCSRF,
2453
- filePath,
2454
- () => "Re-add 'django.middleware.csrf.CsrfViewMiddleware' to MIDDLEWARE. CSRF protection is essential for session-based auth."
2455
- ));
2456
- }
2457
- return matches;
2458
- }
2459
- };
2460
- var githubActionsInjection = {
2461
- id: "VC075",
2462
- title: "GitHub Actions Script Injection",
2463
- severity: "critical",
2464
- category: "Injection",
2465
- description: "Using ${{ github.event.* }} directly in 'run:' steps allows attackers to inject shell commands via PR titles, issue bodies, or branch names.",
2466
- check(content, filePath) {
2467
- if (!filePath.match(/\.github\/workflows\/.*\.(yml|yaml)$/i)) return [];
2468
- const matches = [];
2469
- const patterns = [
2470
- /run:.*\$\{\{\s*github\.event\.(?:issue|pull_request|comment|review|head_commit)\.(?:title|body|message)/gi,
2471
- /run:.*\$\{\{\s*github\.event\.(?:inputs|head_ref|base_ref)/gi
2472
- ];
2473
- for (const p of patterns) {
2474
- matches.push(...findMatches(
2475
- content,
2476
- p,
2477
- githubActionsInjection,
2478
- filePath,
2479
- () => "Never use ${{ github.event.* }} directly in 'run:'. Pass it as an environment variable: env: TITLE: ${{ github.event.issue.title }} then use $TITLE in the script."
2480
- ));
2481
- }
2482
- return matches;
2483
- }
2484
- };
2485
- var secretsInCI = {
2486
- id: "VC076",
2487
- title: "Hardcoded Secrets in CI/CD Config",
2488
- severity: "critical",
2489
- category: "Secrets",
2490
- description: "Hardcoded tokens, passwords, or API keys in CI/CD workflow files are visible to anyone with repo access. Use encrypted secrets instead.",
2491
- check(content, filePath) {
2492
- if (!filePath.match(/\.github\/workflows\/|\.gitlab-ci|Jenkinsfile|\.circleci|bitbucket-pipelines/i)) return [];
2493
- const matches = [];
2494
- const patterns = [
2495
- /(?:password|token|key|secret|api_key|apikey)\s*[:=]\s*["'`][A-Za-z0-9+/=_-]{20,}["'`]/gi,
2496
- /(?:DOCKER_PASSWORD|NPM_TOKEN|AWS_SECRET_ACCESS_KEY|GH_TOKEN|GITHUB_TOKEN)\s*[:=]\s*["'`][^"'`$]+["'`]/gi
2497
- ];
2498
- for (const p of patterns) {
2499
- matches.push(...findMatches(
2500
- content,
2501
- p,
2502
- secretsInCI,
2503
- filePath,
2504
- () => "Use repository secrets: ${{ secrets.MY_TOKEN }} (GitHub) or CI/CD variable settings. Never hardcode credentials in workflow files."
2505
- ));
2506
- }
2507
- return matches;
2508
- }
2509
- };
2510
- var corsServerless = {
2511
- id: "VC077",
2512
- title: "CORS Wildcard in Serverless Config",
2513
- severity: "high",
2514
- category: "Configuration",
2515
- description: "Setting Access-Control-Allow-Origin: * in serverless.yml, vercel.json, or similar configs allows any website to make authenticated requests to your API.",
2516
- check(content, filePath) {
2517
- if (!filePath.match(/serverless\.(yml|yaml)|vercel\.json|netlify\.toml|amplify\.yml/i)) return [];
2518
- const matches = [];
2519
- if (/(?:Access-Control-Allow-Origin|allowOrigin|cors).*['"]\*['"]/i.test(content)) {
2520
- matches.push(...findMatches(
2521
- content,
2522
- /(?:Access-Control-Allow-Origin|allowOrigin|cors).*['"]\*['"]/gi,
2523
- corsServerless,
2524
- filePath,
2525
- () => "Replace wildcard CORS with specific origins: allowOrigin: ['https://yourdomain.com']. Wildcard allows any site to call your API."
2526
- ));
2527
- }
2528
- return matches;
2529
- }
2530
- };
2531
- var k8sPrivileged = {
2532
- id: "VC078",
2533
- title: "Kubernetes Privileged Container",
2534
- severity: "critical",
2535
- category: "Configuration",
2536
- description: "Running containers with privileged: true or as root in Kubernetes gives full host access, making container escapes trivial.",
2537
- check(content, filePath) {
2538
- if (!filePath.match(/\.(yaml|yml)$/) || !/(?:kind|apiVersion|container)/i.test(content)) return [];
2539
- const matches = [];
2540
- const patterns = [
2541
- /privileged\s*:\s*true/g,
2542
- /runAsUser\s*:\s*0\b/g,
2543
- /runAsNonRoot\s*:\s*false/g,
2544
- /allowPrivilegeEscalation\s*:\s*true/g
2545
- ];
2546
- for (const p of patterns) {
2547
- matches.push(...findMatches(
2548
- content,
2549
- p,
2550
- k8sPrivileged,
2551
- filePath,
2552
- () => "Set securityContext: { privileged: false, runAsNonRoot: true, allowPrivilegeEscalation: false }. Never run containers as root in production."
2553
- ));
2554
- }
2555
- return matches;
2556
- }
2557
- };
2558
- function detectFramework(files) {
2559
- const frameworks = /* @__PURE__ */ new Set();
2560
- for (const { path, content } of files) {
2561
- if (path.match(/next\.config/i) || /from\s+["']next/i.test(content)) frameworks.add("next.js");
2562
- if (/from\s+["']react-native/i.test(content) || path.match(/react-native\.config/i)) frameworks.add("react-native");
2563
- else if (/from\s+["']react/i.test(content) || /import\s+React/i.test(content)) frameworks.add("react");
2564
- if (/from\s+["']express/i.test(content) || /require\s*\(\s*["']express/i.test(content)) frameworks.add("express");
2565
- if (/from\s+["']hono/i.test(content)) frameworks.add("hono");
2566
- if (/from\s+["']fastify/i.test(content)) frameworks.add("fastify");
2567
- if (/from\s+["']electron/i.test(content) || path.match(/electron/i)) frameworks.add("electron");
2568
- if (path.match(/settings\.py$/) || /from\s+django/i.test(content)) frameworks.add("django");
2569
- if (/from\s+flask/i.test(content) || /Flask\s*\(/i.test(content)) frameworks.add("flask");
2570
- if (/from\s+["']vue/i.test(content) || path.match(/vue\.config/i)) frameworks.add("vue");
2571
- if (/from\s+["']svelte/i.test(content) || path.match(/svelte\.config/i)) frameworks.add("svelte");
2572
- }
2573
- if (frameworks.size === 0) frameworks.add("unknown");
2574
- return [...frameworks];
2575
- }
2576
- function calculateGrade(findings, totalFiles) {
2577
- if (findings.length === 0) {
2578
- return { grade: "A+", score: 100, summary: "No security issues detected. Excellent!" };
2579
- }
2580
- let deductions = 0;
2581
- for (const f of findings) {
2582
- switch (f.severity) {
2583
- case "critical":
2584
- deductions += 15;
2585
- break;
2586
- case "high":
2587
- deductions += 8;
2588
- break;
2589
- case "medium":
2590
- deductions += 4;
2591
- break;
2592
- case "low":
2593
- deductions += 1;
2594
- break;
2595
- }
2596
- }
2597
- const sizeBuffer = Math.min(Math.log2(Math.max(totalFiles, 1)) * 2, 15);
2598
- const score = Math.max(0, Math.min(100, 100 - deductions + sizeBuffer));
2599
- let grade;
2600
- let summary;
2601
- if (score >= 95) {
2602
- grade = "A+";
2603
- summary = "Excellent security posture with minimal issues.";
2604
- } else if (score >= 85) {
2605
- grade = "A";
2606
- summary = "Strong security with a few minor concerns.";
2607
- } else if (score >= 70) {
2608
- grade = "B";
2609
- summary = "Good security but some issues need attention.";
2610
- } else if (score >= 55) {
2611
- grade = "C";
2612
- summary = "Fair security \u2014 several vulnerabilities should be fixed.";
2613
- } else if (score >= 35) {
2614
- grade = "D";
2615
- summary = "Poor security \u2014 critical issues require immediate attention.";
2616
- } else {
2617
- grade = "F";
2618
- summary = "Failing \u2014 serious vulnerabilities present. Fix critical issues immediately.";
2619
- }
2620
- return { grade, score: Math.round(score), summary };
2621
- }
2622
- var jwtAlgConfusion = {
2623
- id: "VC079",
2624
- title: "JWT Algorithm Confusion (alg:none)",
2625
- severity: "critical",
2626
- category: "Authentication",
2627
- description: "Accepting 'none' as a JWT algorithm allows attackers to forge tokens by removing the signature entirely.",
2628
- check(content, filePath) {
2629
- if (!/jwt|jsonwebtoken|jose/i.test(content)) return [];
2630
- const matches = [];
2631
- const patterns = [
2632
- /algorithms\s*:\s*\[.*["']none["']/gi,
2633
- /algorithm\s*[:=]\s*["']none["']/gi
2634
- ];
2635
- for (const p of patterns) {
2636
- matches.push(...findMatches(
2637
- content,
2638
- p,
2639
- jwtAlgConfusion,
2640
- filePath,
2641
- () => "Never allow algorithm 'none'. Explicitly specify: algorithms: ['RS256'] or algorithms: ['HS256']. Reject tokens with alg:none."
2642
- ));
2643
- }
2644
- if (/jwt\.verify\s*\([^)]*\)\s*(?!.*algorithms)/i.test(content) && !/algorithms/i.test(content)) {
2645
- matches.push(...findMatches(
2646
- content,
2647
- /jwt\.verify\s*\(/g,
2648
- jwtAlgConfusion,
2649
- filePath,
2650
- () => "Specify allowed algorithms in jwt.verify: jwt.verify(token, secret, { algorithms: ['HS256'] })."
2651
- ));
2652
- }
2653
- return matches;
2654
- }
2655
- };
2656
- var regexDos = {
2657
- id: "VC080",
2658
- title: "Potential Regular Expression DoS (ReDoS)",
2659
- severity: "high",
2660
- category: "Availability",
2661
- description: "Nested quantifiers like (a+)+ or (a*){2,} cause catastrophic backtracking, allowing attackers to freeze your server with crafted input.",
2662
- check(content, filePath) {
2663
- if (filePath.includes("test") || filePath.includes("mock")) return [];
2664
- const matches = [];
2665
- const patterns = [
2666
- /new\s+RegExp\s*\(\s*["'`].*\([^)]*[+*]\)[+*{]/g,
2667
- /\/.*\([^)]*[+*]\)[+*{].*\//g
2668
- ];
2669
- for (const p of patterns) {
2670
- matches.push(...findMatches(
2671
- content,
2672
- p,
2673
- regexDos,
2674
- filePath,
2675
- () => "Avoid nested quantifiers in regex. Use atomic groups, possessive quantifiers, or the 're2' library for safe regex execution."
2676
- ));
2677
- }
2678
- return matches;
2679
- }
2680
- };
2681
- var xxeVulnerability = {
2682
- id: "VC081",
2683
- title: "XML External Entity (XXE) Injection",
2684
- severity: "critical",
2685
- category: "Injection",
2686
- description: "XML parsers that process external entities allow attackers to read files, perform SSRF, or cause DoS via billion-laughs attacks.",
2687
- check(content, filePath) {
2688
- if (!/xml|parseXML|DOMParser|SAXParser|etree|lxml/i.test(content)) return [];
2689
- const matches = [];
2690
- const patterns = [
2691
- /\.parseXML\s*\(/g,
2692
- /new\s+DOMParser\s*\(\)/g,
2693
- /etree\.parse\s*\(/g,
2694
- /lxml\.etree/g,
2695
- /SAXParserFactory/g,
2696
- /XMLReaderFactory/g
2697
- ];
2698
- const hasProtection = /noent.*false|resolveExternals.*false|FEATURE_EXTERNAL.*false|defusedxml|disallow-doctype-decl/i.test(content);
2699
- if (hasProtection) return [];
2700
- for (const p of patterns) {
2701
- matches.push(...findMatches(
2702
- content,
2703
- p,
2704
- xxeVulnerability,
2705
- filePath,
2706
- () => "Disable external entities: set noent: false, or use defusedxml (Python). For Java: factory.setFeature('http://apache.org/xml/features/disallow-doctype-decl', true)."
2707
- ));
2708
- }
2709
- return matches;
2710
- }
2711
- };
2712
- var ssti = {
2713
- id: "VC082",
2714
- title: "Server-Side Template Injection (SSTI)",
2715
- severity: "critical",
2716
- category: "Injection",
2717
- description: "Rendering templates from user-controlled strings allows attackers to execute arbitrary code on the server.",
2718
- check(content, filePath) {
2719
- if (filePath.includes("test") || filePath.includes("mock")) return [];
2720
- const matches = [];
2721
- const patterns = [
2722
- /render_template_string\s*\(\s*(?!["'`])/g,
2723
- /Template\s*\(\s*(?:req\.|body\.|input|params|args|user)/gi,
2724
- /engine\.render\s*\(\s*(?:req\.|body\.|input)/gi,
2725
- /nunjucks\.renderString\s*\(\s*(?:req\.|body\.|input)/gi,
2726
- /ejs\.render\s*\(\s*(?:req\.|body\.|input)/gi
2727
- ];
2728
- for (const p of patterns) {
2729
- matches.push(...findMatches(
2730
- content,
2731
- p,
2732
- ssti,
2733
- filePath,
2734
- () => "Never render templates from user input. Use pre-defined templates and pass data as context variables: render_template('template.html', data=user_data)."
2735
- ));
2736
- }
2737
- return matches;
2738
- }
2739
- };
2740
- var javaDeserialization = {
2741
- id: "VC083",
2742
- title: "Insecure Java Deserialization",
2743
- severity: "critical",
2744
- category: "Injection",
2745
- description: "ObjectInputStream.readObject() on untrusted data allows arbitrary code execution via gadget chains in the classpath.",
2746
- check(content, filePath) {
2747
- if (!filePath.match(/\.java$|\.kt$/)) return [];
2748
- const matches = [];
2749
- const patterns = [
2750
- /ObjectInputStream\s*\(/g,
2751
- /\.readObject\s*\(\)/g,
2752
- /XMLDecoder\s*\(/g,
2753
- /XStream\s*\(\)/g
2754
- ];
2755
- const hasSafe = /ValidatingObjectInputStream|ObjectInputFilter|SerialKiller|NotSerializableException/i.test(content);
2756
- if (hasSafe) return [];
2757
- for (const p of patterns) {
2758
- matches.push(...findMatches(
2759
- content,
2760
- p,
2761
- javaDeserialization,
2762
- filePath,
2763
- () => "Use ValidatingObjectInputStream with an allowlist of classes, or avoid Java serialization entirely. Use JSON instead."
2764
- ));
2765
- }
2766
- return matches;
2767
- }
2768
- };
2769
- var missingSRI = {
2770
- id: "VC084",
2771
- title: "Missing Subresource Integrity (SRI)",
2772
- severity: "medium",
2773
- category: "Configuration",
2774
- description: "External scripts and stylesheets loaded without integrity= attributes can be tampered with if the CDN is compromised.",
2775
- check(content, filePath) {
2776
- if (!filePath.match(/\.(html|htm|jsx|tsx|ejs|hbs)$/)) return [];
2777
- const matches = [];
2778
- const scriptPattern = /<script\s+[^>]*src\s*=\s*["']https?:\/\/[^"']+["'][^>]*>/gi;
2779
- let m;
2780
- const re = new RegExp(scriptPattern.source, scriptPattern.flags);
2781
- while ((m = re.exec(content)) !== null) {
2782
- if (!m[0].includes("integrity")) {
2783
- const lineNum = content.substring(0, m.index).split("\n").length;
2784
- matches.push({
2785
- rule: "VC084",
2786
- title: missingSRI.title,
2787
- severity: "medium",
2788
- category: "Configuration",
2789
- file: filePath,
2790
- line: lineNum,
2791
- snippet: getSnippet(content, lineNum),
2792
- fix: 'Add integrity and crossorigin attributes: <script src="..." integrity="sha384-..." crossorigin="anonymous">'
2793
- });
2794
- }
2795
- }
2796
- return matches;
2797
- }
2798
- };
2799
- var exposedAdminRoutes = {
2800
- id: "VC085",
2801
- title: "Exposed Admin or Debug Route",
2802
- severity: "high",
2803
- category: "Information Leakage",
2804
- description: "Routes like /admin, /debug, /phpinfo, or /actuator without authentication expose sensitive controls and information to attackers.",
2805
- check(content, filePath) {
2806
- if (!/(?:\/api\/|routes?\/|server\.|app\.|index\.[jt]s)/i.test(filePath)) return [];
2807
- const matches = [];
2808
- const patterns = [
2809
- /[.'"]\s*(?:get|use|all)\s*\(\s*["'`]\/(?:admin|debug|_debug|__debug__|phpinfo|actuator|graphiql|playground|swagger)["'`]/gi
2810
- ];
2811
- const hasAuth = /auth|requireAuth|isAdmin|requireAdmin|authenticate|middleware.*admin/i.test(content);
2812
- if (hasAuth) return [];
2813
- for (const p of patterns) {
2814
- matches.push(...findMatches(
2815
- content,
2816
- p,
2817
- exposedAdminRoutes,
2818
- filePath,
2819
- () => "Protect admin/debug routes with authentication middleware. In production, disable debug endpoints entirely."
2820
- ));
2821
- }
2822
- return matches;
2823
- }
2824
- };
2825
- var insecureWebSocket = {
2826
- id: "VC086",
2827
- title: "Insecure WebSocket Connection (ws://)",
2828
- severity: "medium",
2829
- category: "Configuration",
2830
- description: "Using ws:// instead of wss:// transmits data in plaintext, vulnerable to eavesdropping and man-in-the-middle attacks.",
2831
- check(content, filePath) {
2832
- if (filePath.includes("test") || filePath.includes("mock")) return [];
2833
- return findMatches(
2834
- content,
2835
- /new\s+WebSocket\s*\(\s*["'`]ws:\/\//g,
2836
- insecureWebSocket,
2837
- filePath,
2838
- () => "Use wss:// (WebSocket Secure) instead of ws:// for encrypted connections: new WebSocket('wss://...')."
2839
- );
2840
- }
2841
- };
2842
- var missingHSTS = {
2843
- id: "VC087",
2844
- title: "Missing HTTP Strict Transport Security (HSTS)",
2845
- severity: "medium",
2846
- category: "Configuration",
2847
- description: "Without HSTS headers, browsers allow downgrade attacks from HTTPS to HTTP, exposing traffic to interception.",
2848
- check(content, filePath) {
2849
- if (!/(?:server|app|index|main)\.[jt]sx?$/.test(filePath)) return [];
2850
- if (!/(?:express|hono|fastify|koa)/i.test(content)) return [];
2851
- if (/Strict-Transport-Security|hsts|helmet/i.test(content)) return [];
2852
- return findMatches(
2853
- content,
2854
- /(?:express|hono|fastify|koa)\s*\(/gi,
2855
- missingHSTS,
2856
- filePath,
2857
- () => "Add HSTS header: res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains'). Or use helmet()."
2858
- );
2859
- }
2860
- };
2861
- var sensitiveURLParams = {
2862
- id: "VC088",
2863
- title: "Sensitive Data in URL Parameters",
2864
- severity: "high",
2865
- category: "Information Leakage",
2866
- description: "Passing passwords, tokens, or API keys in URL query parameters exposes them in server logs, browser history, and referrer headers.",
2867
- check(content, filePath) {
2868
- if (filePath.includes("test") || filePath.includes("mock")) return [];
2869
- const matches = [];
2870
- const patterns = [
2871
- /\?\s*(?:password|token|secret|api_key|apiKey|access_token|ssn|credit_card)\s*=/gi,
2872
- /[&?](?:password|token|secret|api_key|apiKey|access_token)\s*=\s*\$\{/gi
2873
- ];
2874
- for (const p of patterns) {
2875
- matches.push(...findMatches(
2876
- content,
2877
- p,
2878
- sensitiveURLParams,
2879
- filePath,
2880
- () => "Never pass sensitive data in URL parameters. Use request headers (Authorization: Bearer ...) or POST body instead."
2881
- ));
2882
- }
2883
- return matches;
2884
- }
2885
- };
2886
- var missingContentDisposition = {
2887
- id: "VC089",
2888
- title: "File Download Missing Content-Disposition",
2889
- severity: "medium",
2890
- category: "Configuration",
2891
- description: "File download endpoints without Content-Disposition headers may render files inline, leading to XSS if the file contains HTML/JS.",
2892
- check(content, filePath) {
2893
- if (!/(?:download|sendFile|send_file|pipe|createReadStream)/i.test(content)) return [];
2894
- if (!/(?:\/api\/|routes?\/|controllers?\/|server\.|handler)/i.test(filePath)) return [];
2895
- if (/Content-Disposition|attachment|download/i.test(content)) return [];
2896
- return findMatches(
2897
- content,
2898
- /(?:sendFile|send_file|createReadStream|\.pipe)\s*\(/gi,
2899
- missingContentDisposition,
2900
- filePath,
2901
- () => `Set Content-Disposition: attachment header on file downloads: res.setHeader('Content-Disposition', 'attachment; filename="file.pdf"').`
2902
- );
2903
- }
2904
- };
2905
- var hostHeaderRedirect = {
2906
- id: "VC090",
2907
- title: "Open Redirect via Host Header",
2908
- severity: "high",
2909
- category: "Injection",
2910
- description: "Using req.headers.host to construct redirect URLs allows attackers to inject a malicious host header, redirecting users to phishing sites.",
2911
- check(content, filePath) {
2912
- if (filePath.includes("test") || filePath.includes("mock")) return [];
2913
- const matches = [];
2914
- if (/req\.headers\.host|req\.get\s*\(\s*["']host["']\)/i.test(content) && /redirect|location/i.test(content)) {
2915
- matches.push(...findMatches(
2916
- content,
2917
- /req\.headers\.host|req\.get\s*\(\s*["']host["']\)/gi,
2918
- hostHeaderRedirect,
2919
- filePath,
2920
- () => "Don't use req.headers.host for redirects \u2014 it's attacker-controlled. Use a hardcoded domain or environment variable."
2921
- ));
2922
- }
2923
- return matches;
2924
- }
2925
- };
2926
- var raceCondition = {
2927
- id: "VC091",
2928
- title: "Potential Race Condition (TOCTOU)",
2929
- severity: "high",
2930
- category: "Authorization",
2931
- description: "Check-then-act patterns (e.g., checking if a file exists then writing to it) are vulnerable to race conditions where state changes between the check and the action.",
2932
- check(content, filePath) {
2933
- if (filePath.includes("test") || filePath.includes("mock")) return [];
2934
- const matches = [];
2935
- const patterns = [
2936
- /(?:existsSync|exists)\s*\([^)]+\)[\s\S]{0,50}(?:writeFileSync|writeFile|unlinkSync|unlink|renameSync)\s*\(/g,
2937
- /os\.path\.exists\s*\([^)]+\)[\s\S]{0,50}open\s*\(/g,
2938
- /File\.exists\?\s*\([^)]+\)[\s\S]{0,50}File\.(?:write|delete)/g
2939
- ];
2940
- for (const p of patterns) {
2941
- matches.push(...findMatches(
2942
- content,
2943
- p,
2944
- raceCondition,
2945
- filePath,
2946
- () => "Use atomic operations instead of check-then-act. For files: use fs.open with 'wx' flag (exclusive create), or use file locks."
2947
- ));
2948
- }
2949
- return matches;
2950
- }
2951
- };
2952
- var unsafeObjectAssign = {
2953
- id: "VC092",
2954
- title: "Unsafe Object Spread from User Input",
2955
- severity: "medium",
2956
- category: "Injection",
2957
- description: "Spreading request body into a new object can copy __proto__, constructor, or other dangerous properties, enabling prototype pollution.",
2958
- check(content, filePath) {
2959
- if (filePath.includes("test") || filePath.includes("mock")) return [];
2960
- const matches = [];
2961
- const patterns = [
2962
- /Object\.assign\s*\(\s*\{\s*\}\s*,\s*(?:req\.(?:body|query|params)|body|input)\s*\)/gi,
2963
- /\{\s*\.\.\.(?:req\.(?:body|query|params)|body|input)\s*\}/gi
2964
- ];
2965
- const hasSafe = /omit.*__proto__|sanitize|pick\(|lodash\.pick|stripProto/i.test(content);
2966
- if (hasSafe) return [];
2967
- for (const p of patterns) {
2968
- matches.push(...findMatches(
2969
- content,
2970
- p,
2971
- unsafeObjectAssign,
2972
- filePath,
2973
- () => "Explicitly pick allowed properties instead of spreading: const { name, email } = req.body; const safe = { name, email };"
2974
- ));
2975
- }
2976
- return matches;
2977
- }
2978
- };
2979
- var unprotectedDownload = {
2980
- id: "VC093",
2981
- title: "File Download Without Path Validation",
2982
- severity: "medium",
2983
- category: "Authorization",
2984
- description: "File download endpoints that accept user-controlled filenames without path validation allow directory traversal to read arbitrary files.",
2985
- check(content, filePath) {
2986
- if (!/(?:download|sendFile|send_file)/i.test(content)) return [];
2987
- if (!/(?:\/api\/|routes?\/|controllers?\/|server\.|handler)/i.test(filePath)) return [];
2988
- const matches = [];
2989
- if (/(?:sendFile|download|send_file)\s*\([^)]*(?:req\.|params\.|query\.|body\.)/i.test(content)) {
2990
- const hasValidation = /path\.resolve|path\.normalize|path\.join.*__dirname|realpath|includes\s*\(\s*["']\.\./i.test(content);
2991
- if (!hasValidation) {
2992
- matches.push(...findMatches(
2993
- content,
2994
- /(?:sendFile|download|send_file)\s*\(/gi,
2995
- unprotectedDownload,
2996
- filePath,
2997
- () => "Validate file paths: const safePath = path.resolve(DOWNLOAD_DIR, filename); if (!safePath.startsWith(DOWNLOAD_DIR)) throw new Error('Invalid path');"
2998
- ));
2999
- }
3000
- }
3001
- return matches;
3002
- }
3003
- };
3004
- var commandInjection = {
3005
- id: "VC094",
3006
- title: "Potential Command Injection",
3007
- severity: "critical",
3008
- category: "Injection",
3009
- description: "Passing user input to shell commands (exec, system, child_process) allows attackers to execute arbitrary system commands.",
3010
- check(content, filePath) {
3011
- if (filePath.includes("test") || filePath.includes("mock")) return [];
3012
- const matches = [];
3013
- const patterns = [
3014
- // Node.js
3015
- /(?:exec|execSync)\s*\(\s*(?:`[^`]*\$\{|["'][^"']*\+\s*(?:req\.|body\.|input|params|args|user))/gi,
3016
- /child_process.*exec\s*\(\s*(?!["'`])/g,
3017
- // Python
3018
- /os\.system\s*\(\s*(?!["'`].*["'`]\s*\))/g,
3019
- /subprocess\.(?:call|run|Popen)\s*\([^)]*shell\s*=\s*True/gi,
3020
- // Ruby
3021
- /system\s*\(\s*["'].*#\{/g
3022
- ];
3023
- const hasSafe = /execFile|spawn|escapeshellarg|shlex\.quote|shellEscape/i.test(content);
3024
- if (hasSafe) return [];
3025
- for (const p of patterns) {
3026
- matches.push(...findMatches(
3027
- content,
3028
- p,
3029
- commandInjection,
3030
- filePath,
3031
- () => "Use execFile/spawn instead of exec (avoids shell). Never concatenate user input into shell commands. Use parameterized arguments."
3032
- ));
3033
- }
3034
- return matches;
3035
- }
3036
- };
3037
- var corsLocalhost = {
3038
- id: "VC095",
3039
- title: "Hardcoded Localhost CORS Origin",
3040
- severity: "medium",
3041
- category: "Configuration",
3042
- description: "Hardcoded localhost CORS origins in production code allow any local process to make authenticated requests to your API.",
3043
- check(content, filePath) {
3044
- if (filePath.includes("test") || filePath.includes("mock") || filePath.includes(".env")) return [];
3045
- if (!/origin/i.test(content)) return [];
3046
- return findMatches(
3047
- content,
3048
- /origin\s*[:=]\s*["'`]http:\/\/localhost/gi,
3049
- corsLocalhost,
3050
- filePath,
3051
- () => "Use environment variables for CORS origins: origin: process.env.ALLOWED_ORIGIN. Remove localhost from production configs."
3052
- );
3053
- }
3054
- };
3055
- var insecureGRPC = {
3056
- id: "VC096",
3057
- title: "Unencrypted gRPC Channel",
3058
- severity: "medium",
3059
- category: "Configuration",
3060
- description: "Using insecure gRPC channels transmits data including credentials in plaintext.",
3061
- check(content, filePath) {
3062
- if (!/grpc/i.test(content)) return [];
3063
- return findMatches(
3064
- content,
3065
- /(?:insecure_channel|createInsecure|grpc\.Insecure)/gi,
3066
- insecureGRPC,
3067
- filePath,
3068
- () => "Use encrypted gRPC channels: grpc.ssl_channel_credentials() or grpc.credentials.createSsl()."
3069
- );
3070
- }
3071
- };
3072
- var complianceMap = {
3073
- VC001: { owasp: "A07:2021", cwe: "CWE-798" },
3074
- VC002: { owasp: "A05:2021", cwe: "CWE-200" },
3075
- VC003: { owasp: "A01:2021", cwe: "CWE-862" },
3076
- VC004: { owasp: "A01:2021", cwe: "CWE-284" },
3077
- VC005: { owasp: "A08:2021", cwe: "CWE-345" },
3078
- VC006: { owasp: "A03:2021", cwe: "CWE-89" },
3079
- VC007: { owasp: "A03:2021", cwe: "CWE-79" },
3080
- VC008: { owasp: "A04:2021", cwe: "CWE-770" },
3081
- VC009: { owasp: "A05:2021", cwe: "CWE-942" },
3082
- VC010: { owasp: "A01:2021", cwe: "CWE-602" },
3083
- VC011: { owasp: "A07:2021", cwe: "CWE-798" },
3084
- VC012: { owasp: "A05:2021", cwe: "CWE-200" },
3085
- VC013: { owasp: "A01:2021", cwe: "CWE-269" },
3086
- VC014: { owasp: "A05:2021", cwe: "CWE-538" },
3087
- VC015: { owasp: "A03:2021", cwe: "CWE-95" },
3088
- VC016: { owasp: "A01:2021", cwe: "CWE-601" },
3089
- VC017: { owasp: "A05:2021", cwe: "CWE-614" },
3090
- VC018: { owasp: "A07:2021", cwe: "CWE-798" },
3091
- VC019: { owasp: "A05:2021", cwe: "CWE-693" },
3092
- VC020: { owasp: "A05:2021", cwe: "CWE-1021" },
3093
- VC021: { owasp: "A01:2021", cwe: "CWE-22" },
3094
- VC022: { owasp: "A03:2021", cwe: "CWE-79" },
3095
- VC023: { owasp: "A08:2021", cwe: "CWE-1321" },
3096
- VC024: { owasp: "A04:2021", cwe: "CWE-770" },
3097
- VC025: { owasp: "A03:2021", cwe: "CWE-22" },
3098
- VC026: { owasp: "A05:2021", cwe: "CWE-693" },
3099
- VC027: { owasp: "A05:2021", cwe: "CWE-693" },
3100
- VC028: { owasp: "A07:2021", cwe: "CWE-20" },
3101
- VC029: { owasp: "A08:2021", cwe: "CWE-20" },
3102
- VC030: { owasp: "A08:2021", cwe: "CWE-502" },
3103
- VC031: { owasp: "A02:2021", cwe: "CWE-321" },
3104
- VC032: { owasp: "A05:2021", cwe: "CWE-319" },
3105
- VC033: { owasp: "A05:2021", cwe: "CWE-215" },
3106
- VC034: { owasp: "A02:2021", cwe: "CWE-338" },
3107
- VC035: { owasp: "A01:2021", cwe: "CWE-601" },
3108
- VC036: { owasp: "A04:2021", cwe: "CWE-755" },
3109
- VC037: { owasp: "A09:2021", cwe: "CWE-209" },
3110
- VC038: { owasp: "A04:2021", cwe: "CWE-434" },
3111
- VC039: { owasp: "A06:2021", cwe: "CWE-1104" },
3112
- VC040: { owasp: "A05:2021", cwe: "CWE-538" },
3113
- VC041: { owasp: "A10:2021", cwe: "CWE-918" },
3114
- VC042: { owasp: "A01:2021", cwe: "CWE-915" },
3115
- VC043: { owasp: "A02:2021", cwe: "CWE-208" },
3116
- VC044: { owasp: "A09:2021", cwe: "CWE-117" },
3117
- VC045: { owasp: "A07:2021", cwe: "CWE-521" },
3118
- VC046: { owasp: "A07:2021", cwe: "CWE-384" },
3119
- VC047: { owasp: "A07:2021", cwe: "CWE-307" },
3120
- VC048: { owasp: "A03:2021", cwe: "CWE-943" },
3121
- VC049: { owasp: "A07:2021", cwe: "CWE-798" },
3122
- VC050: { owasp: "A02:2021", cwe: "CWE-319" },
3123
- VC051: { owasp: "A05:2021", cwe: "CWE-200" },
3124
- VC052: { owasp: "A04:2021", cwe: "CWE-770" },
3125
- VC053: { owasp: "A05:2021", cwe: "CWE-798" },
3126
- VC054: { owasp: "A07:2021", cwe: "CWE-922" },
3127
- VC055: { owasp: "A05:2021", cwe: "CWE-540" },
3128
- VC056: { owasp: "A05:2021", cwe: "CWE-1021" },
3129
- VC057: { owasp: "A01:2021", cwe: "CWE-269" },
3130
- VC058: { owasp: "A05:2021", cwe: "CWE-250" },
3131
- VC059: { owasp: "A05:2021", cwe: "CWE-284" },
3132
- VC060: { owasp: "A02:2021", cwe: "CWE-328" },
3133
- VC061: { owasp: "A02:2021", cwe: "CWE-295" },
3134
- VC062: { owasp: "A02:2021", cwe: "CWE-321" },
3135
- VC063: { owasp: "A03:2021", cwe: "CWE-79" },
3136
- VC064: { owasp: "A01:2021", cwe: "CWE-862" },
3137
- VC065: { owasp: "A01:2021", cwe: "CWE-862" },
3138
- VC066: { owasp: "A07:2021", cwe: "CWE-798" },
3139
- VC067: { owasp: "A01:2021", cwe: "CWE-601" },
3140
- VC068: { owasp: "A07:2021", cwe: "CWE-922" },
3141
- VC069: { owasp: "A02:2021", cwe: "CWE-295" },
3142
- VC070: { owasp: "A05:2021", cwe: "CWE-489" },
3143
- VC071: { owasp: "A05:2021", cwe: "CWE-215" },
3144
- VC072: { owasp: "A07:2021", cwe: "CWE-798" },
3145
- VC073: { owasp: "A08:2021", cwe: "CWE-502" },
3146
- VC074: { owasp: "A01:2021", cwe: "CWE-352" },
3147
- VC075: { owasp: "A03:2021", cwe: "CWE-78" },
3148
- VC076: { owasp: "A07:2021", cwe: "CWE-798" },
3149
- VC077: { owasp: "A05:2021", cwe: "CWE-942" },
3150
- VC078: { owasp: "A05:2021", cwe: "CWE-250" },
3151
- VC079: { owasp: "A02:2021", cwe: "CWE-327" },
3152
- VC080: { owasp: "A04:2021", cwe: "CWE-1333" },
3153
- VC081: { owasp: "A03:2021", cwe: "CWE-611" },
3154
- VC082: { owasp: "A03:2021", cwe: "CWE-94" },
3155
- VC083: { owasp: "A08:2021", cwe: "CWE-502" },
3156
- VC084: { owasp: "A06:2021", cwe: "CWE-353" },
3157
- VC085: { owasp: "A01:2021", cwe: "CWE-862" },
3158
- VC086: { owasp: "A02:2021", cwe: "CWE-319" },
3159
- VC087: { owasp: "A05:2021", cwe: "CWE-311" },
3160
- VC088: { owasp: "A07:2021", cwe: "CWE-598" },
3161
- VC089: { owasp: "A05:2021", cwe: "CWE-430" },
3162
- VC090: { owasp: "A01:2021", cwe: "CWE-601" },
3163
- VC091: { owasp: "A04:2021", cwe: "CWE-367" },
3164
- VC092: { owasp: "A08:2021", cwe: "CWE-1321" },
3165
- VC093: { owasp: "A01:2021", cwe: "CWE-22" },
3166
- VC094: { owasp: "A03:2021", cwe: "CWE-78" },
3167
- VC095: { owasp: "A05:2021", cwe: "CWE-942" },
3168
- VC096: { owasp: "A02:2021", cwe: "CWE-319" },
3169
- VC097: { owasp: "A09:2021", cwe: "CWE-532" },
3170
- VC098: { owasp: "A04:2021", cwe: "CWE-400" },
3171
- VC099: { owasp: "A04:2021", cwe: "CWE-401" },
3172
- VC100: { owasp: "A04:2021", cwe: "CWE-400" },
3173
- VC101: { owasp: "A04:2021", cwe: "CWE-400" },
3174
- VC102: { owasp: "A04:2021", cwe: "CWE-400" },
3175
- VC103: { owasp: "A04:2021", cwe: "CWE-710" },
3176
- VC104: { owasp: "A04:2021", cwe: "CWE-390" },
3177
- VC105: { owasp: "A04:2021", cwe: "CWE-710" },
3178
- VC106: { owasp: "A04:2021", cwe: "CWE-710" },
3179
- VC107: { owasp: "A02:2021", cwe: "CWE-311" },
3180
- VC108: { owasp: "A01:2021", cwe: "CWE-284" },
3181
- VC109: { owasp: "A01:2021", cwe: "CWE-284" },
3182
- VC110: { owasp: "A09:2021", cwe: "CWE-778" },
3183
- VC111: { owasp: "A05:2021", cwe: "CWE-16" },
3184
- VC112: { owasp: "A06:2021", cwe: "CWE-1104" },
3185
- VC113: { owasp: "A05:2021", cwe: "CWE-200" },
3186
- VC114: { owasp: "A05:2021", cwe: "CWE-16" },
3187
- VC115: { owasp: "A02:2021", cwe: "CWE-311" },
3188
- VC116: { owasp: "A05:2021", cwe: "CWE-770" },
3189
- VC117: { owasp: "A03:2021", cwe: "CWE-22" },
3190
- VC118: { owasp: "A09:2021", cwe: "CWE-532" },
3191
- VC119: { owasp: "A07:2021", cwe: "CWE-798" },
3192
- VC120: { owasp: "A07:2021", cwe: "CWE-352" },
3193
- VC121: { owasp: "A06:2021", cwe: "CWE-1104" },
3194
- VC122: { owasp: "A02:2021", cwe: "CWE-326" },
3195
- VC123: { owasp: "A02:2021", cwe: "CWE-326" },
3196
- VC124: { owasp: "A02:2021", cwe: "CWE-327" },
3197
- VC125: { owasp: "A07:2021", cwe: "CWE-640" },
3198
- VC126: { owasp: "A05:2021", cwe: "CWE-200" },
3199
- VC127: { owasp: "A01:2021", cwe: "CWE-862" },
3200
- VC128: { owasp: "A05:2021", cwe: "CWE-444" },
3201
- VC129: { owasp: "A02:2021", cwe: "CWE-311" },
3202
- VC130: { owasp: "A07:2021", cwe: "CWE-307" },
3203
- VC131: { owasp: "A06:2021", cwe: "CWE-1104" }
3204
- };
3205
- var consoleLogProduction = {
3206
- id: "VC097",
3207
- title: "Console.log Left in Production Code",
3208
- severity: "medium",
3209
- category: "Performance",
3210
- description: "console.log statements left in production code can leak sensitive data, slow down rendering, and clutter browser consoles.",
3211
- check(content, filePath) {
3212
- if (filePath.match(/test|spec|mock|__tests__|fixture|\.test\.|\.spec\./i)) return [];
3213
- if (!/console\.log\s*\(/g.test(content)) return [];
3214
- const lines = content.split("\n");
3215
- const matches = [];
3216
- for (let i = 0; i < lines.length; i++) {
3217
- const line = lines[i].trim();
3218
- if (/console\.log\s*\(/.test(line) && !line.startsWith("//") && !line.startsWith("*") && !/if\s*\(\s*(?:debug|process\.env)/i.test(lines[Math.max(0, i - 1)] + line)) {
3219
- matches.push({ rule: "VC097", title: consoleLogProduction.title, severity: "medium", category: "Performance", file: filePath, line: i + 1, snippet: getSnippet(content, i + 1), fix: "Remove console.log or use a logger that can be disabled in production." });
3220
- }
3221
- }
3222
- return matches.slice(0, 3);
3223
- }
3224
- };
3225
- var syncFileOps = {
3226
- id: "VC098",
3227
- title: "Synchronous File Operations",
3228
- severity: "medium",
3229
- category: "Performance",
3230
- description: "Synchronous file operations (readFileSync, writeFileSync) block the event loop, causing all other requests to wait.",
3231
- check(content, filePath) {
3232
- if (filePath.match(/test|spec|mock|__tests__|fixture|config|\.config\./i)) return [];
3233
- return findMatches(
3234
- content,
3235
- /(?:readFileSync|writeFileSync|appendFileSync|mkdirSync|rmdirSync|statSync)\s*\(/g,
3236
- syncFileOps,
3237
- filePath,
3238
- () => "Use async file operations (readFile, writeFile) to avoid blocking the event loop."
3239
- );
3240
- }
3241
- };
3242
- var eventListenerLeak = {
3243
- id: "VC099",
3244
- title: "Memory Leak: Event Listener Not Cleaned Up",
3245
- severity: "high",
3246
- category: "Performance",
3247
- description: "Adding event listeners in React useEffect without a cleanup function causes memory leaks as listeners accumulate on re-renders.",
3248
- check(content, filePath) {
3249
- if (!filePath.match(/\.(jsx|tsx)$/)) return [];
3250
- if (!/addEventListener/i.test(content)) return [];
3251
- if (/removeEventListener/i.test(content)) return [];
3252
- if (!/useEffect/i.test(content)) return [];
3253
- return findMatches(
3254
- content,
3255
- /addEventListener\s*\(/g,
3256
- eventListenerLeak,
3257
- filePath,
3258
- () => "Return a cleanup function from useEffect: useEffect(() => { window.addEventListener('resize', fn); return () => window.removeEventListener('resize', fn); }, []);"
3259
- );
3260
- }
3261
- };
3262
- var nPlusOneQuery = {
3263
- id: "VC100",
3264
- title: "N+1 Query Pattern Detected",
3265
- severity: "medium",
3266
- category: "Performance",
3267
- description: "Database queries inside loops cause N+1 performance problems \u2014 one query per iteration instead of a single batch query.",
3268
- check(content, filePath) {
3269
- if (filePath.match(/test|spec|mock/i)) return [];
3270
- const hasLoopWithQuery = /(?:for\s*\(|\.forEach\s*\(|\.map\s*\(|while\s*\()[^}]*(?:\.find\(|\.findOne\(|\.findById\(|\.query\(|\.execute\(|SELECT\s)/is.test(content);
3271
- if (!hasLoopWithQuery) return [];
3272
- return findMatches(
3273
- content,
3274
- /(?:for\s*\(|\.forEach\s*\(|\.map\s*\(|while\s*\()/g,
3275
- nPlusOneQuery,
3276
- filePath,
3277
- () => "Fetch all data in a single query using WHERE IN, JOIN, or batch operations instead of querying per item in a loop."
3278
- ).slice(0, 2);
3279
- }
3280
- };
3281
- var largeBundleImport = {
3282
- id: "VC101",
3283
- title: "Importing Entire Library (Large Bundle)",
3284
- severity: "medium",
3285
- category: "Performance",
3286
- description: "Importing entire libraries like lodash or moment.js adds hundreds of KB to your bundle. Import only the functions you need.",
3287
- check(content, filePath) {
3288
- if (!filePath.match(/\.(jsx?|tsx?)$/)) return [];
3289
- const matches = [];
3290
- const patterns = [
3291
- /import\s+_\s+from\s+['"]lodash['"]/g,
3292
- /import\s+\*\s+as\s+_\s+from\s+['"]lodash['"]/g,
3293
- /import\s+moment\s+from\s+['"]moment['"]/g,
3294
- /const\s+_\s*=\s*require\s*\(\s*['"]lodash['"]\s*\)/g,
3295
- /const\s+moment\s*=\s*require\s*\(\s*['"]moment['"]\s*\)/g
3296
- ];
3297
- for (const p of patterns) {
3298
- matches.push(...findMatches(
3299
- content,
3300
- p,
3301
- largeBundleImport,
3302
- filePath,
3303
- () => "Import only what you need: import { debounce } from 'lodash/debounce'. Or switch to lighter alternatives like date-fns instead of moment."
3304
- ));
3305
- }
3306
- return matches;
3307
- }
3308
- };
3309
- var blockingMainThread = {
3310
- id: "VC102",
3311
- title: "Blocking Main Thread with Heavy Computation",
3312
- severity: "medium",
3313
- category: "Performance",
3314
- description: "Infinite loops or deeply nested iterations on the main thread freeze the UI and cause unresponsiveness.",
3315
- check(content, filePath) {
3316
- if (filePath.match(/worker|test|spec|mock/i)) return [];
3317
- const matches = [];
3318
- if (/while\s*\(\s*true\s*\)/g.test(content)) {
3319
- matches.push(...findMatches(
3320
- content,
3321
- /while\s*\(\s*true\s*\)/g,
3322
- blockingMainThread,
3323
- filePath,
3324
- () => "Avoid while(true) on the main thread. Use Web Workers for heavy computation or requestIdleCallback for non-urgent work."
3325
- ));
3326
- }
3327
- return matches;
3328
- }
3329
- };
3330
- var todoLeftInCode = {
3331
- id: "VC103",
3332
- title: "TODO/FIXME Left in Code",
3333
- severity: "low",
3334
- category: "Code Quality",
3335
- description: "TODO, FIXME, HACK, and XXX comments indicate unfinished work that should be resolved before shipping to production.",
3336
- check(content, filePath) {
3337
- if (filePath.match(/test|spec|mock|__tests__|fixture|node_modules/i)) return [];
3338
- return findMatches(
3339
- content,
3340
- /\/\/\s*(?:TODO|FIXME|HACK|XXX)\b/gi,
3341
- todoLeftInCode,
3342
- filePath,
3343
- () => "Resolve TODO/FIXME comments before shipping. If it's intentional tech debt, track it in your issue tracker instead."
3344
- ).slice(0, 5);
3345
- }
3346
- };
3347
- var emptyCatchBlock = {
3348
- id: "VC104",
3349
- title: "Empty Catch Block",
3350
- severity: "medium",
3351
- category: "Code Quality",
3352
- description: "Empty catch blocks silently swallow errors, making bugs impossible to diagnose. At minimum, log the error.",
3353
- check(content, filePath) {
3354
- if (filePath.match(/test|spec|mock/i)) return [];
3355
- return findMatches(
3356
- content,
3357
- /catch\s*(?:\([^)]*\))?\s*\{\s*\}/g,
3358
- emptyCatchBlock,
3359
- filePath,
3360
- () => "Handle errors in catch blocks: catch(err) { console.error('Operation failed:', err); } or re-throw if appropriate."
3361
- );
3362
- }
3363
- };
3364
- var callbackHell = {
3365
- id: "VC105",
3366
- title: "Deeply Nested Callbacks (Promise Chain)",
3367
- severity: "medium",
3368
- category: "Code Quality",
3369
- description: "Long .then() chains or deeply nested callbacks are hard to read, debug, and maintain. Refactor to async/await.",
3370
- check(content, filePath) {
3371
- if (filePath.match(/test|spec|mock/i)) return [];
3372
- return findMatches(
3373
- content,
3374
- /\.then\s*\([^)]*\)\s*\.then\s*\([^)]*\)\s*\.then/g,
3375
- callbackHell,
3376
- filePath,
3377
- () => "Refactor .then() chains to async/await for cleaner, more readable code."
3378
- );
3379
- }
3380
- };
3381
- var magicNumbers = {
3382
- id: "VC106",
3383
- title: "Magic Numbers in Code",
3384
- severity: "low",
3385
- category: "Code Quality",
3386
- description: "Unnamed numeric constants in conditions or calculations make code hard to understand. Extract them into named constants.",
3387
- check(content, filePath) {
3388
- if (filePath.match(/test|spec|mock|config|\.config\.|constant|enum|migration/i)) return [];
3389
- if (!filePath.match(/\.(jsx?|tsx?)$/)) return [];
3390
- const matches = [];
3391
- const lines = content.split("\n");
3392
- for (let i = 0; i < lines.length; i++) {
3393
- const line = lines[i].trim();
3394
- if (line.startsWith("//") || line.startsWith("*")) continue;
3395
- if (/(?:===|!==|>=?|<=?)\s*\d{3,}/.test(line) || /(?:setTimeout|setInterval)\s*\([^,]+,\s*\d{4,}/.test(line)) {
3396
- matches.push({ rule: "VC106", title: magicNumbers.title, severity: "low", category: "Code Quality", file: filePath, line: i + 1, snippet: getSnippet(content, i + 1), fix: "Extract magic numbers into named constants: const MAX_RETRIES = 3; const TIMEOUT_MS = 5000;" });
3397
- }
3398
- }
3399
- return matches.slice(0, 3);
3400
- }
3401
- };
3402
- var s3BucketNoEncryption = {
3403
- id: "VC107",
3404
- title: "S3 Bucket Without Encryption",
3405
- severity: "high",
3406
- category: "Infrastructure",
3407
- description: "AWS S3 buckets without server-side encryption leave data at rest unprotected. Enable encryption to protect sensitive data.",
3408
- check(content, filePath) {
3409
- if (!filePath.match(/\.tf$/)) return [];
3410
- if (!/resource\s+"aws_s3_bucket"/.test(content)) return [];
3411
- const matches = [];
3412
- const bucketPattern = /resource\s+"aws_s3_bucket"\s+"(\w+)"/g;
3413
- let m;
3414
- while ((m = bucketPattern.exec(content)) !== null) {
3415
- if (isCommentLine(content, m.index)) continue;
3416
- const blockEnd = Math.min(m.index + 2e3, content.length);
3417
- const blockContent = content.substring(m.index, blockEnd);
3418
- if (!/server_side_encryption/.test(blockContent)) {
3419
- const lineNum = content.substring(0, m.index).split("\n").length;
3420
- matches.push({
3421
- rule: "VC107",
3422
- title: s3BucketNoEncryption.title,
3423
- severity: "high",
3424
- category: "Infrastructure",
3425
- file: filePath,
3426
- line: lineNum,
3427
- snippet: getSnippet(content, lineNum),
3428
- fix: "Enable S3 bucket encryption: server_side_encryption_configuration { rule { apply_server_side_encryption_by_default { sse_algorithm = 'aws:kms' } } }"
3429
- });
3430
- }
3431
- }
3432
- return matches;
3433
- }
3434
- };
3435
- var securityGroupAllInbound = {
3436
- id: "VC108",
3437
- title: "Security Group Allows All Inbound",
3438
- severity: "critical",
3439
- category: "Infrastructure",
3440
- description: "Security groups allowing all inbound traffic (0.0.0.0/0 on all ports) expose resources to the entire internet.",
3441
- check(content, filePath) {
3442
- if (!filePath.match(/\.(tf|json|yaml|yml)$/)) return [];
3443
- const matches = [];
3444
- if (filePath.match(/\.tf$/)) {
3445
- const ingressPattern = /ingress\s*\{[^}]*cidr_blocks\s*=\s*\[\s*"0\.0\.0\.0\/0"\s*\][^}]*\}/gs;
3446
- let m;
3447
- while ((m = ingressPattern.exec(content)) !== null) {
3448
- if (isCommentLine(content, m.index)) continue;
3449
- if (/from_port\s*=\s*0/.test(m[0]) || !/from_port/.test(m[0])) {
3450
- const lineNum = content.substring(0, m.index).split("\n").length;
3451
- matches.push({
3452
- rule: "VC108",
3453
- title: securityGroupAllInbound.title,
3454
- severity: "critical",
3455
- category: "Infrastructure",
3456
- file: filePath,
3457
- line: lineNum,
3458
- snippet: getSnippet(content, lineNum),
3459
- fix: "Restrict security group ingress to specific IP ranges and ports."
3460
- });
3461
- }
3462
- }
3463
- }
3464
- if (filePath.match(/\.(json|yaml|yml)$/)) {
3465
- const cfnPattern = /AWS::EC2::SecurityGroup/g;
3466
- if (cfnPattern.test(content) && /CidrIp\s*:\s*["']?0\.0\.0\.0\/0/.test(content)) {
3467
- matches.push(...findMatches(
3468
- content,
3469
- /CidrIp\s*:\s*["']?0\.0\.0\.0\/0/g,
3470
- securityGroupAllInbound,
3471
- filePath,
3472
- () => "Restrict security group ingress to specific IP ranges and ports."
3473
- ));
3474
- }
3475
- }
3476
- return matches;
3477
- }
3478
- };
3479
- var rdsPubliclyAccessible = {
3480
- id: "VC109",
3481
- title: "RDS Instance Publicly Accessible",
3482
- severity: "critical",
3483
- category: "Infrastructure",
3484
- description: "RDS instances with publicly_accessible = true are exposed to the internet, risking unauthorized database access.",
3485
- check(content, filePath) {
3486
- if (!filePath.match(/\.tf$/)) return [];
3487
- if (!/resource\s+"aws_db_instance"/.test(content)) return [];
3488
- return findMatches(
3489
- content,
3490
- /publicly_accessible\s*=\s*true/g,
3491
- rdsPubliclyAccessible,
3492
- filePath,
3493
- () => "Set publicly_accessible = false. Access RDS through VPC private subnets."
3494
- );
3495
- }
3496
- };
3497
- var missingCloudTrail = {
3498
- id: "VC110",
3499
- title: "Missing CloudTrail Logging",
3500
- severity: "high",
3501
- category: "Infrastructure",
3502
- description: "AWS environments without CloudTrail lack audit logging of API calls, making it difficult to detect unauthorized activity.",
3503
- check(content, filePath) {
3504
- if (!filePath.match(/\.tf$/)) return [];
3505
- if (!/provider\s+"aws"/.test(content)) return [];
3506
- if (/aws_cloudtrail/.test(content)) return [];
3507
- const lineNum = content.substring(0, content.search(/provider\s+"aws"/)).split("\n").length;
3508
- return [{
3509
- rule: "VC110",
3510
- title: missingCloudTrail.title,
3511
- severity: "high",
3512
- category: "Infrastructure",
3513
- file: filePath,
3514
- line: lineNum,
3515
- snippet: getSnippet(content, lineNum),
3516
- fix: "Enable CloudTrail for audit logging of all AWS API calls."
3517
- }];
3518
- }
3519
- };
3520
- var lambdaWithoutVPC = {
3521
- id: "VC111",
3522
- title: "Lambda Without VPC",
3523
- severity: "medium",
3524
- category: "Infrastructure",
3525
- description: "Lambda functions not placed in a VPC lack network isolation and cannot access VPC-only resources securely.",
3526
- check(content, filePath) {
3527
- if (!filePath.match(/\.tf$/)) return [];
3528
- if (!/resource\s+"aws_lambda_function"/.test(content)) return [];
3529
- const matches = [];
3530
- const lambdaPattern = /resource\s+"aws_lambda_function"\s+"(\w+)"/g;
3531
- let m;
3532
- while ((m = lambdaPattern.exec(content)) !== null) {
3533
- if (isCommentLine(content, m.index)) continue;
3534
- const blockEnd = Math.min(m.index + 2e3, content.length);
3535
- const blockContent = content.substring(m.index, blockEnd);
3536
- if (!/vpc_config\s*\{/.test(blockContent)) {
3537
- const lineNum = content.substring(0, m.index).split("\n").length;
3538
- matches.push({
3539
- rule: "VC111",
3540
- title: lambdaWithoutVPC.title,
3541
- severity: "medium",
3542
- category: "Infrastructure",
3543
- file: filePath,
3544
- line: lineNum,
3545
- snippet: getSnippet(content, lineNum),
3546
- fix: "Place Lambda functions in a VPC for network isolation."
3547
- });
3548
- }
3549
- }
3550
- return matches;
3551
- }
3552
- };
3553
- var dockerLatestTag = {
3554
- id: "VC112",
3555
- title: "Docker Image Using Latest Tag",
3556
- severity: "high",
3557
- category: "Configuration",
3558
- description: "Using :latest or no tag in Docker FROM directives leads to non-reproducible builds and potential security regressions.",
3559
- check(content, filePath) {
3560
- if (!filePath.match(/Dockerfile$/i)) return [];
3561
- const matches = [];
3562
- const fromPattern = /^FROM\s+(?!scratch)(\S+?)(?:\s+AS\s+\S+)?\s*$/gm;
3563
- let m;
3564
- while ((m = fromPattern.exec(content)) !== null) {
3565
- const image = m[1];
3566
- if (image.endsWith(":latest") || !image.includes(":") && !image.startsWith("$")) {
3567
- const lineNum = content.substring(0, m.index).split("\n").length;
3568
- matches.push({
3569
- rule: "VC112",
3570
- title: dockerLatestTag.title,
3571
- severity: "high",
3572
- category: "Configuration",
3573
- file: filePath,
3574
- line: lineNum,
3575
- snippet: getSnippet(content, lineNum),
3576
- fix: "Pin Docker image versions: FROM node:20-alpine instead of FROM node:latest"
3577
- });
3578
- }
3579
- }
3580
- return matches;
3581
- }
3582
- };
3583
- var dockerCopySensitive = {
3584
- id: "VC113",
3585
- title: "Docker COPY With Sensitive Files",
3586
- severity: "high",
3587
- category: "Configuration",
3588
- description: "Using COPY . . or ADD . . without a .dockerignore can leak .env files, .git history, and other sensitive data into the Docker image.",
3589
- check(content, filePath) {
3590
- if (!filePath.match(/Dockerfile$/i)) return [];
3591
- if (!/(?:COPY|ADD)\s+\.\s+\./.test(content)) return [];
3592
- return findMatches(
3593
- content,
3594
- /(?:COPY|ADD)\s+\.\s+\./g,
3595
- dockerCopySensitive,
3596
- filePath,
3597
- () => "Use .dockerignore to exclude .env, .git, node_modules, and sensitive files from the Docker build context."
3598
- );
3599
- }
3600
- };
3601
- var dockerTooManyPorts = {
3602
- id: "VC114",
3603
- title: "Docker Exposing Too Many Ports",
3604
- severity: "medium",
3605
- category: "Configuration",
3606
- description: "Exposing many ports or wide port ranges increases the attack surface of a container.",
3607
- check(content, filePath) {
3608
- if (!filePath.match(/Dockerfile$/i)) return [];
3609
- const matches = [];
3610
- const rangePattern = /^EXPOSE\s+\d+-\d+/gm;
3611
- matches.push(...findMatches(
3612
- content,
3613
- rangePattern,
3614
- dockerTooManyPorts,
3615
- filePath,
3616
- () => "Only expose necessary ports. Each EXPOSE should have a clear purpose."
3617
- ));
3618
- const exposeLines = content.split("\n").filter((l) => /^\s*EXPOSE\s+/.test(l));
3619
- if (exposeLines.length > 3) {
3620
- const firstExpose = content.search(/^\s*EXPOSE\s+/m);
3621
- if (firstExpose >= 0) {
3622
- const lineNum = content.substring(0, firstExpose).split("\n").length;
3623
- matches.push({
3624
- rule: "VC114",
3625
- title: dockerTooManyPorts.title,
3626
- severity: "medium",
3627
- category: "Configuration",
3628
- file: filePath,
3629
- line: lineNum,
3630
- snippet: getSnippet(content, lineNum),
3631
- fix: "Only expose necessary ports. Each EXPOSE should have a clear purpose."
3632
- });
3633
- }
3634
- }
3635
- return matches;
3636
- }
3637
- };
3638
- var k8sSecretNotEncrypted = {
3639
- id: "VC115",
3640
- title: "Kubernetes Secret Not Encrypted",
3641
- severity: "high",
3642
- category: "Infrastructure",
3643
- description: "Kubernetes Secrets stored as plain base64 in manifests are not encrypted and can be trivially decoded. Use sealed-secrets or external-secrets.",
3644
- check(content, filePath) {
3645
- if (!filePath.match(/\.(yaml|yml)$/)) return [];
3646
- if (!/kind\s*:\s*Secret/i.test(content)) return [];
3647
- if (/sealedsecrets\.bitnami\.com|external-secrets\.io/i.test(content)) return [];
3648
- return findMatches(
3649
- content,
3650
- /kind\s*:\s*Secret/g,
3651
- k8sSecretNotEncrypted,
3652
- filePath,
3653
- () => "Use sealed-secrets or external-secrets-operator. Never store plain secrets in manifests."
3654
- );
3655
- }
3656
- };
3657
- var k8sNoResourceLimits = {
3658
- id: "VC116",
3659
- title: "Kubernetes Pod Without Resource Limits",
3660
- severity: "medium",
3661
- category: "Infrastructure",
3662
- description: "Pods or deployments without resource limits can consume excessive CPU/memory, causing cluster instability.",
3663
- check(content, filePath) {
3664
- if (!filePath.match(/\.(yaml|yml)$/)) return [];
3665
- if (!/kind\s*:\s*(?:Pod|Deployment|StatefulSet|DaemonSet|Job|CronJob)/i.test(content)) return [];
3666
- if (/resources\s*:\s*\n\s+limits\s*:/m.test(content)) return [];
3667
- if (/resources\s*:.*limits/i.test(content)) return [];
3668
- const kindMatch = content.match(/kind\s*:\s*(?:Pod|Deployment|StatefulSet|DaemonSet|Job|CronJob)/i);
3669
- if (!kindMatch) return [];
3670
- const lineNum = content.substring(0, kindMatch.index).split("\n").length;
3671
- return [{
3672
- rule: "VC116",
3673
- title: k8sNoResourceLimits.title,
3674
- severity: "medium",
3675
- category: "Infrastructure",
3676
- file: filePath,
3677
- line: lineNum,
3678
- snippet: getSnippet(content, lineNum),
3679
- fix: "Set resource limits to prevent pods from consuming excessive CPU/memory."
3680
- }];
3681
- }
3682
- };
3683
- var pathTraversal = {
3684
- id: "VC117",
3685
- title: "Path Traversal Vulnerability",
3686
- severity: "critical",
3687
- category: "Injection",
3688
- description: "User input is used to construct file paths without sanitization, allowing attackers to read/write arbitrary files (e.g., ../../etc/passwd).",
3689
- check(content, filePath) {
3690
- if (isTestFile(filePath)) return [];
3691
- if (!filePath.match(/\.(js|ts|jsx|tsx|py|rb|go|php|java)$/)) return [];
3692
- const findings = [];
3693
- const patterns = [
3694
- /(?:readFile|readFileSync|createReadStream|writeFile|writeFileSync|appendFile|unlink|unlinkSync|access|stat|statSync|existsSync)\s*\(\s*(?:req\.|request\.|ctx\.|params\.|query\.)/gi,
3695
- /(?:path\.join|path\.resolve)\s*\([^)]*(?:req\.|request\.|ctx\.|params\.|query\.)[^)]*\)/gi,
3696
- /(?:res\.sendFile|res\.download)\s*\([^)]*(?:req\.|request\.)/gi,
3697
- /open\s*\(\s*(?:request\.|params\[|args\.)/gi
3698
- ];
3699
- for (const pat of patterns) {
3700
- let m;
3701
- while ((m = pat.exec(content)) !== null) {
3702
- if (isCommentLine(content, m.index)) continue;
3703
- const surrounding = content.substring(Math.max(0, m.index - 200), m.index + 200);
3704
- if (/path\.normalize|sanitize|\.replace\(\s*['"]\.\./i.test(surrounding)) continue;
3705
- const lineNum = content.substring(0, m.index).split("\n").length;
3706
- findings.push({
3707
- rule: "VC117",
3708
- title: pathTraversal.title,
3709
- severity: "critical",
3710
- category: "Injection",
3711
- file: filePath,
3712
- line: lineNum,
3713
- snippet: getSnippet(content, lineNum),
3714
- fix: "Sanitize file paths: use path.normalize(), reject paths containing '..', and validate against an allowed base directory."
3715
- });
3716
- }
3717
- }
3718
- return findings;
3719
- }
3720
- };
3721
- var piiInLogs = {
3722
- id: "VC118",
3723
- title: "Personally Identifiable Information in Logs",
3724
- severity: "high",
3725
- category: "Information Leakage",
3726
- description: "Logging statements that include email addresses, passwords, SSNs, credit card numbers, or other PII can leak sensitive data to log aggregation systems.",
3727
- check(content, filePath) {
3728
- if (isTestFile(filePath)) return [];
3729
- if (!filePath.match(/\.(js|ts|jsx|tsx|py|rb|go|java|php)$/)) return [];
3730
- const findings = [];
3731
- const logPattern = /(?:console\.(?:log|info|warn|error|debug)|logger\.(?:info|warn|error|debug|log)|log\.(?:info|warn|error|debug)|logging\.(?:info|warn|error|debug))\s*\([^)]*(?:password|passwd|secret|ssn|social.?security|credit.?card|cardNumber|cvv|token|bearer|authorization)\b/gi;
3732
- let m;
3733
- while ((m = logPattern.exec(content)) !== null) {
3734
- if (isCommentLine(content, m.index)) continue;
3735
- if (isInsideFixMessage(content, m.index)) continue;
3736
- const lineNum = content.substring(0, m.index).split("\n").length;
3737
- findings.push({
3738
- rule: "VC118",
3739
- title: piiInLogs.title,
3740
- severity: "high",
3741
- category: "Information Leakage",
3742
- file: filePath,
3743
- line: lineNum,
3744
- snippet: getSnippet(content, lineNum),
3745
- fix: "Never log sensitive data. Redact or mask PII before logging: log('user login', { email: maskEmail(email) })."
3746
- });
3747
- }
3748
- return findings;
3749
- }
3750
- };
3751
- var hardcodedOAuthSecret = {
3752
- id: "VC119",
3753
- title: "Hardcoded OAuth Client Secret",
3754
- severity: "critical",
3755
- category: "Secrets",
3756
- description: "OAuth client secrets hardcoded in source code can be extracted and used to impersonate your application.",
3757
- check(content, filePath) {
3758
- if (isTestFile(filePath)) return [];
3759
- if (!filePath.match(/\.(js|ts|jsx|tsx|py|rb|go|java|php|env|json|yaml|yml)$/)) return [];
3760
- if (/package-lock|yarn\.lock|pnpm-lock|composer\.lock/i.test(filePath)) return [];
3761
- const findings = [];
3762
- const patterns = [
3763
- /client[_-]?secret\s*[:=]\s*["'][a-zA-Z0-9_\-]{20,}["']/gi,
3764
- /GOOGLE_CLIENT_SECRET\s*[:=]\s*["'][^"']{10,}["']/gi,
3765
- /GITHUB_CLIENT_SECRET\s*[:=]\s*["'][^"']{10,}["']/gi,
3766
- /FACEBOOK_APP_SECRET\s*[:=]\s*["'][^"']{10,}["']/gi,
3767
- /AUTH0_CLIENT_SECRET\s*[:=]\s*["'][^"']{10,}["']/gi
3768
- ];
3769
- for (const pat of patterns) {
3770
- let m;
3771
- while ((m = pat.exec(content)) !== null) {
3772
- if (isCommentLine(content, m.index)) continue;
3773
- if (/process\.env|os\.environ|ENV\[|getenv|\$\{|<your|CHANGE_ME|xxx|placeholder/i.test(m[0])) continue;
3774
- const lineNum = content.substring(0, m.index).split("\n").length;
3775
- findings.push({
3776
- rule: "VC119",
3777
- title: hardcodedOAuthSecret.title,
3778
- severity: "critical",
3779
- category: "Secrets",
3780
- file: filePath,
3781
- line: lineNum,
3782
- snippet: getSnippet(content, lineNum),
3783
- fix: "Move OAuth client secrets to environment variables. Never commit secrets to source control."
3784
- });
3785
- }
3786
- }
3787
- return findings;
3788
- }
3789
- };
3790
- var missingOAuthState = {
3791
- id: "VC120",
3792
- title: "OAuth Flow Missing State Parameter",
3793
- severity: "high",
3794
- category: "Authentication",
3795
- description: "OAuth authorization requests without a state parameter are vulnerable to CSRF attacks, allowing attackers to link their account to a victim's session.",
3796
- check(content, filePath) {
3797
- if (isTestFile(filePath)) return [];
3798
- if (!filePath.match(/\.(js|ts|jsx|tsx|py|rb|go|java|php)$/)) return [];
3799
- const findings = [];
3800
- const oauthUrlPattern = /(?:authorize\?|\/oauth\/authorize|\/auth\?|authorization_endpoint)[^}]*(?:client_id|response_type)/gi;
3801
- let m;
3802
- while ((m = oauthUrlPattern.exec(content)) !== null) {
3803
- if (isCommentLine(content, m.index)) continue;
3804
- const surrounding = content.substring(m.index, Math.min(content.length, m.index + 500));
3805
- if (/state\s*[=:]/i.test(surrounding)) continue;
3806
- const lineNum = content.substring(0, m.index).split("\n").length;
3807
- findings.push({
3808
- rule: "VC120",
3809
- title: missingOAuthState.title,
3810
- severity: "high",
3811
- category: "Authentication",
3812
- file: filePath,
3813
- line: lineNum,
3814
- snippet: getSnippet(content, lineNum),
3815
- fix: "Always include a cryptographically random 'state' parameter in OAuth authorization requests and validate it on callback."
3816
- });
3817
- }
3818
- return findings;
3819
- }
3820
- };
3821
- var unpinnedGitHubAction = {
3822
- id: "VC121",
3823
- title: "Unpinned GitHub Actions Version",
3824
- severity: "high",
3825
- category: "Supply Chain",
3826
- description: "GitHub Actions using branch references (@main, @master) instead of commit SHAs can be compromised via supply-chain attacks.",
3827
- check(content, filePath) {
3828
- if (!filePath.match(/\.github\/workflows\/.*\.(yml|yaml)$/)) return [];
3829
- const findings = [];
3830
- const usesPattern = /uses\s*:\s*[\w\-]+\/[\w\-]+@(main|master|dev|develop|latest)\b/gi;
3831
- let m;
3832
- while ((m = usesPattern.exec(content)) !== null) {
3833
- if (isCommentLine(content, m.index)) continue;
3834
- const lineNum = content.substring(0, m.index).split("\n").length;
3835
- findings.push({
3836
- rule: "VC121",
3837
- title: unpinnedGitHubAction.title,
3838
- severity: "high",
3839
- category: "Supply Chain",
3840
- file: filePath,
3841
- line: lineNum,
3842
- snippet: getSnippet(content, lineNum),
3843
- fix: "Pin GitHub Actions to a specific commit SHA instead of a branch name: uses: owner/action@<full-sha>"
3844
- });
3845
- }
3846
- return findings;
3847
- }
3848
- };
3849
- var deprecatedTLS = {
3850
- id: "VC122",
3851
- title: "Deprecated TLS Version Configured",
3852
- severity: "high",
3853
- category: "Cryptography",
3854
- description: "TLS 1.0 and 1.1 are deprecated and have known vulnerabilities. Only TLS 1.2+ should be used.",
3855
- check(content, filePath) {
3856
- if (isTestFile(filePath)) return [];
3857
- if (!filePath.match(/\.(js|ts|py|rb|go|java|yaml|yml|json|conf|cfg|xml)$/)) return [];
3858
- const findings = [];
3859
- const patterns = [
3860
- /(?:TLSv1_METHOD|TLSv1\.0|TLSv1\.1|ssl\.PROTOCOL_TLSv1|SSLv3|SSLv2|TLS_1_0|TLS_1_1|minVersion\s*[:=]\s*["']TLSv1(?:\.1)?["'])/gi,
3861
- /(?:tls\.DEFAULT_MIN_VERSION|secureProtocol)\s*[:=]\s*["'](?:TLSv1_method|TLSv1_1_method|SSLv3_method)["']/gi,
3862
- /ssl_protocols\s+.*(?:TLSv1(?:\.1)?(?:\s|;))/gi
3863
- ];
3864
- for (const pat of patterns) {
3865
- let m;
3866
- while ((m = pat.exec(content)) !== null) {
3867
- if (isCommentLine(content, m.index)) continue;
3868
- const lineNum = content.substring(0, m.index).split("\n").length;
3869
- findings.push({
3870
- rule: "VC122",
3871
- title: deprecatedTLS.title,
3872
- severity: "high",
3873
- category: "Cryptography",
3874
- file: filePath,
3875
- line: lineNum,
3876
- snippet: getSnippet(content, lineNum),
3877
- fix: "Use TLS 1.2 or higher. Set minVersion to 'TLSv1.2' and remove support for TLS 1.0/1.1."
3878
- });
3879
- }
3880
- }
3881
- return findings;
3882
- }
3883
- };
3884
- var weakRSAKeySize = {
3885
- id: "VC123",
3886
- title: "Weak RSA Key Size",
3887
- severity: "high",
3888
- category: "Cryptography",
3889
- description: "RSA keys smaller than 2048 bits are considered insecure and can be factored with modern hardware.",
3890
- check(content, filePath) {
3891
- if (isTestFile(filePath)) return [];
3892
- if (!filePath.match(/\.(js|ts|py|rb|go|java|php)$/)) return [];
3893
- const findings = [];
3894
- const patterns = [
3895
- /generateKeyPair\s*\(\s*["']rsa["']\s*,\s*\{[^}]*modulusLength\s*:\s*(512|768|1024)\b/gi,
3896
- /RSA\.generate\s*\(\s*(512|768|1024)\b/gi,
3897
- /rsa\.GenerateKey\s*\([^,]*,\s*(512|768|1024)\b/gi,
3898
- /KeyPairGenerator.*initialize\s*\(\s*(512|768|1024)\b/gi
3899
- ];
3900
- for (const pat of patterns) {
3901
- let m;
3902
- while ((m = pat.exec(content)) !== null) {
3903
- if (isCommentLine(content, m.index)) continue;
3904
- const lineNum = content.substring(0, m.index).split("\n").length;
3905
- findings.push({
3906
- rule: "VC123",
3907
- title: weakRSAKeySize.title,
3908
- severity: "high",
3909
- category: "Cryptography",
3910
- file: filePath,
3911
- line: lineNum,
3912
- snippet: getSnippet(content, lineNum),
3913
- fix: "Use RSA key sizes of at least 2048 bits. 4096 bits is recommended for long-term security."
3914
- });
3915
- }
3916
- }
3917
- return findings;
3918
- }
3919
- };
3920
- var ecbModeEncryption = {
3921
- id: "VC124",
3922
- title: "Insecure ECB Mode Encryption",
3923
- severity: "high",
3924
- category: "Cryptography",
3925
- description: "ECB (Electronic Codebook) mode encrypts identical plaintext blocks to identical ciphertext, leaking patterns in the data.",
3926
- check(content, filePath) {
3927
- if (isTestFile(filePath)) return [];
3928
- if (!filePath.match(/\.(js|ts|py|rb|go|java|php)$/)) return [];
3929
- const findings = [];
3930
- const patterns = [
3931
- /createCipher(?:iv)?\s*\(\s*["'](?:aes-(?:128|192|256)-ecb|des-ecb)["']/gi,
3932
- /AES\.(?:new|MODE_ECB)|MODE_ECB/gi,
3933
- /Cipher\.getInstance\s*\(\s*["']AES\/ECB/gi,
3934
- /aes\.NewCipher|cipher\.NewECBEncrypter/gi
3935
- ];
3936
- for (const pat of patterns) {
3937
- let m;
3938
- while ((m = pat.exec(content)) !== null) {
3939
- if (isCommentLine(content, m.index)) continue;
3940
- const lineNum = content.substring(0, m.index).split("\n").length;
3941
- findings.push({
3942
- rule: "VC124",
3943
- title: ecbModeEncryption.title,
3944
- severity: "high",
3945
- category: "Cryptography",
3946
- file: filePath,
3947
- line: lineNum,
3948
- snippet: getSnippet(content, lineNum),
3949
- fix: "Use AES-GCM or AES-CBC with HMAC instead of ECB mode. GCM provides both encryption and authentication."
3950
- });
3951
- }
3952
- }
3953
- return findings;
3954
- }
3955
- };
3956
- var insecurePasswordReset = {
3957
- id: "VC125",
3958
- title: "Insecure Password Reset Implementation",
3959
- severity: "high",
3960
- category: "Authentication",
3961
- description: "Password reset using predictable tokens, no expiration, or user-enumeration leaks can be exploited to take over accounts.",
3962
- check(content, filePath) {
3963
- if (isTestFile(filePath)) return [];
3964
- if (!filePath.match(/\.(js|ts|jsx|tsx|py|rb|go|java|php)$/)) return [];
3965
- if (!/(?:reset|forgot).*(?:password|passwd)/i.test(content)) return [];
3966
- const findings = [];
3967
- const weakTokens = /(?:reset|forgot).*(?:token|code)\s*[:=].*(?:Date\.now|Math\.random|uuid\(\)|nanoid\(\))/gi;
3968
- let m;
3969
- while ((m = weakTokens.exec(content)) !== null) {
3970
- if (isCommentLine(content, m.index)) continue;
3971
- const lineNum = content.substring(0, m.index).split("\n").length;
3972
- findings.push({
3973
- rule: "VC125",
3974
- title: insecurePasswordReset.title,
3975
- severity: "high",
3976
- category: "Authentication",
3977
- file: filePath,
3978
- line: lineNum,
3979
- snippet: getSnippet(content, lineNum),
3980
- fix: "Use crypto.randomBytes(32).toString('hex') for reset tokens. Set expiration (15-60 minutes) and single-use enforcement."
3981
- });
3982
- }
3983
- const enumeration = /(?:user|email|account)\s*(?:not\s*found|does\s*not\s*exist|invalid)/gi;
3984
- while ((m = enumeration.exec(content)) !== null) {
3985
- if (isCommentLine(content, m.index)) continue;
3986
- const surrounding = content.substring(Math.max(0, m.index - 500), m.index);
3987
- if (!/(?:reset|forgot).*password/i.test(surrounding)) continue;
3988
- const lineNum = content.substring(0, m.index).split("\n").length;
3989
- findings.push({
3990
- rule: "VC125",
3991
- title: insecurePasswordReset.title,
3992
- severity: "high",
3993
- category: "Authentication",
3994
- file: filePath,
3995
- line: lineNum,
3996
- snippet: getSnippet(content, lineNum),
3997
- fix: "Always return the same response regardless of whether the email exists. Say 'If an account exists, a reset link was sent.'"
3998
- });
3999
- }
4000
- return findings;
4001
- }
4002
- };
4003
- var terraformStateExposed = {
4004
- id: "VC126",
4005
- title: "Terraform State File Committed",
4006
- severity: "critical",
4007
- category: "Secrets",
4008
- description: "Terraform state files contain sensitive infrastructure details, secrets, and access credentials in plaintext. They must never be committed to version control.",
4009
- check(content, filePath) {
4010
- const findings = [];
4011
- if (/terraform\.tfstate(\.backup)?$/.test(filePath)) {
4012
- findings.push({
4013
- rule: "VC126",
4014
- title: terraformStateExposed.title,
4015
- severity: "critical",
4016
- category: "Secrets",
4017
- file: filePath,
4018
- line: 1,
4019
- snippet: getSnippet(content, 1),
4020
- fix: "Add '*.tfstate' and '*.tfstate.backup' to .gitignore. Use remote state backends (S3, GCS, Terraform Cloud) instead."
4021
- });
4022
- }
4023
- if (filePath.endsWith(".gitignore") && !content.includes("tfstate")) {
4024
- if (/\.tf$/.test(filePath) || content.includes(".tf")) {
4025
- }
4026
- }
4027
- return findings;
4028
- }
4029
- };
4030
- var insecureHTTPMethods = {
4031
- id: "VC127",
4032
- title: "Dangerous HTTP Methods Without Auth",
4033
- severity: "high",
4034
- category: "Authorization",
4035
- description: "DELETE, PUT, and PATCH endpoints without authentication checks allow unauthorized data modification or deletion.",
4036
- check(content, filePath) {
4037
- if (isTestFile(filePath)) return [];
4038
- if (!filePath.match(/\.(js|ts|jsx|tsx|py|rb|go|java|php)$/)) return [];
4039
- const findings = [];
4040
- const routePatterns = [
4041
- /(?:app|router|server)\.(delete|put|patch)\s*\(\s*["']/gi
4042
- ];
4043
- for (const pat of routePatterns) {
4044
- let m;
4045
- while ((m = pat.exec(content)) !== null) {
4046
- if (isCommentLine(content, m.index)) continue;
4047
- const handlerBlock = content.substring(m.index, Math.min(content.length, m.index + 500));
4048
- if (/auth|authenticate|authorize|requireAuth|isAuthenticated|protect|guard|middleware|session|jwt|verify|clerk|getAuth/i.test(handlerBlock)) continue;
4049
- const lineNum = content.substring(0, m.index).split("\n").length;
4050
- findings.push({
4051
- rule: "VC127",
4052
- title: insecureHTTPMethods.title,
4053
- severity: "high",
4054
- category: "Authorization",
4055
- file: filePath,
4056
- line: lineNum,
4057
- snippet: getSnippet(content, lineNum),
4058
- fix: "Add authentication middleware to DELETE, PUT, and PATCH routes. Verify the user has permission to modify the resource."
4059
- });
4060
- }
4061
- }
4062
- return findings;
4063
- }
4064
- };
4065
- var httpRequestSmuggling = {
4066
- id: "VC128",
4067
- title: "HTTP Request Smuggling Risk",
4068
- severity: "high",
4069
- category: "Configuration",
4070
- description: "Manually parsing Content-Length or Transfer-Encoding headers can lead to request smuggling when behind a reverse proxy.",
4071
- check(content, filePath) {
4072
- if (isTestFile(filePath)) return [];
4073
- if (!filePath.match(/\.(js|ts|py|rb|go|java|php)$/)) return [];
4074
- const findings = [];
4075
- const patterns = [
4076
- /(?:headers?\[?|getHeader\s*\(\s*)["'](?:content-length|transfer-encoding)["']\s*\]?\s*\)/gi,
4077
- /parseInt\s*\(\s*(?:req|request)\.headers?\[?\s*["']content-length["']/gi
4078
- ];
4079
- for (const pat of patterns) {
4080
- let m;
4081
- while ((m = pat.exec(content)) !== null) {
4082
- if (isCommentLine(content, m.index)) continue;
4083
- const lineNum = content.substring(0, m.index).split("\n").length;
4084
- findings.push({
4085
- rule: "VC128",
4086
- title: httpRequestSmuggling.title,
4087
- severity: "high",
4088
- category: "Configuration",
4089
- file: filePath,
4090
- line: lineNum,
4091
- snippet: getSnippet(content, lineNum),
4092
- fix: "Let your framework handle Content-Length and Transfer-Encoding headers. Do not manually parse or trust these values."
4093
- });
4094
- }
4095
- }
4096
- return findings;
1168
+ const sizeBuffer = Math.min(Math.log2(Math.max(totalFiles, 1)) * 2, 15);
1169
+ const score = Math.max(0, Math.min(100, 100 - deductions + sizeBuffer));
1170
+ let grade;
1171
+ let summary;
1172
+ if (score >= 95) {
1173
+ grade = "A+";
1174
+ summary = "Excellent security posture with minimal issues.";
1175
+ } else if (score >= 85) {
1176
+ grade = "A";
1177
+ summary = "Strong security with a few minor concerns.";
1178
+ } else if (score >= 70) {
1179
+ grade = "B";
1180
+ summary = "Good security but some issues need attention.";
1181
+ } else if (score >= 55) {
1182
+ grade = "C";
1183
+ summary = "Fair security \u2014 several vulnerabilities should be fixed.";
1184
+ } else if (score >= 35) {
1185
+ grade = "D";
1186
+ summary = "Poor security \u2014 critical issues require immediate attention.";
1187
+ } else {
1188
+ grade = "F";
1189
+ summary = "Failing \u2014 serious vulnerabilities present. Fix critical issues immediately.";
4097
1190
  }
1191
+ return { grade, score: Math.round(score), summary };
1192
+ }
1193
+ var complianceMap = {
1194
+ VC001: { owasp: "A07:2021", cwe: "CWE-798" },
1195
+ VC002: { owasp: "A05:2021", cwe: "CWE-200" },
1196
+ VC003: { owasp: "A01:2021", cwe: "CWE-862" },
1197
+ VC004: { owasp: "A01:2021", cwe: "CWE-284" },
1198
+ VC005: { owasp: "A08:2021", cwe: "CWE-345" },
1199
+ VC006: { owasp: "A03:2021", cwe: "CWE-89" },
1200
+ VC007: { owasp: "A03:2021", cwe: "CWE-79" },
1201
+ VC008: { owasp: "A04:2021", cwe: "CWE-770" },
1202
+ VC009: { owasp: "A05:2021", cwe: "CWE-942" },
1203
+ VC010: { owasp: "A01:2021", cwe: "CWE-602" },
1204
+ VC011: { owasp: "A07:2021", cwe: "CWE-798" },
1205
+ VC012: { owasp: "A05:2021", cwe: "CWE-200" },
1206
+ VC013: { owasp: "A01:2021", cwe: "CWE-269" },
1207
+ VC014: { owasp: "A05:2021", cwe: "CWE-538" },
1208
+ VC015: { owasp: "A03:2021", cwe: "CWE-95" },
1209
+ VC016: { owasp: "A01:2021", cwe: "CWE-601" },
1210
+ VC017: { owasp: "A05:2021", cwe: "CWE-614" },
1211
+ VC018: { owasp: "A07:2021", cwe: "CWE-798" },
1212
+ VC019: { owasp: "A05:2021", cwe: "CWE-693" },
1213
+ VC020: { owasp: "A05:2021", cwe: "CWE-1021" },
1214
+ VC021: { owasp: "A01:2021", cwe: "CWE-22" },
1215
+ VC022: { owasp: "A03:2021", cwe: "CWE-79" },
1216
+ VC023: { owasp: "A08:2021", cwe: "CWE-1321" },
1217
+ VC024: { owasp: "A04:2021", cwe: "CWE-770" },
1218
+ VC025: { owasp: "A03:2021", cwe: "CWE-22" },
1219
+ VC026: { owasp: "A05:2021", cwe: "CWE-693" },
1220
+ VC027: { owasp: "A05:2021", cwe: "CWE-693" },
1221
+ VC028: { owasp: "A07:2021", cwe: "CWE-20" },
1222
+ VC029: { owasp: "A08:2021", cwe: "CWE-20" },
1223
+ VC030: { owasp: "A08:2021", cwe: "CWE-502" },
1224
+ VC031: { owasp: "A02:2021", cwe: "CWE-321" },
1225
+ VC032: { owasp: "A05:2021", cwe: "CWE-319" },
1226
+ VC033: { owasp: "A05:2021", cwe: "CWE-215" },
1227
+ VC034: { owasp: "A02:2021", cwe: "CWE-338" },
1228
+ VC035: { owasp: "A01:2021", cwe: "CWE-601" },
1229
+ VC036: { owasp: "A04:2021", cwe: "CWE-755" },
1230
+ VC037: { owasp: "A09:2021", cwe: "CWE-209" },
1231
+ VC038: { owasp: "A04:2021", cwe: "CWE-434" },
1232
+ VC039: { owasp: "A06:2021", cwe: "CWE-1104" },
1233
+ VC040: { owasp: "A05:2021", cwe: "CWE-538" },
1234
+ VC041: { owasp: "A10:2021", cwe: "CWE-918" },
1235
+ VC042: { owasp: "A01:2021", cwe: "CWE-915" },
1236
+ VC043: { owasp: "A02:2021", cwe: "CWE-208" },
1237
+ VC044: { owasp: "A09:2021", cwe: "CWE-117" },
1238
+ VC045: { owasp: "A07:2021", cwe: "CWE-521" },
1239
+ VC046: { owasp: "A07:2021", cwe: "CWE-384" },
1240
+ VC047: { owasp: "A07:2021", cwe: "CWE-307" },
1241
+ VC048: { owasp: "A03:2021", cwe: "CWE-943" },
1242
+ VC049: { owasp: "A07:2021", cwe: "CWE-798" },
1243
+ VC050: { owasp: "A02:2021", cwe: "CWE-319" },
1244
+ VC051: { owasp: "A05:2021", cwe: "CWE-200" },
1245
+ VC052: { owasp: "A04:2021", cwe: "CWE-770" },
1246
+ VC053: { owasp: "A05:2021", cwe: "CWE-798" },
1247
+ VC054: { owasp: "A07:2021", cwe: "CWE-922" },
1248
+ VC055: { owasp: "A05:2021", cwe: "CWE-540" },
1249
+ VC056: { owasp: "A05:2021", cwe: "CWE-1021" },
1250
+ VC057: { owasp: "A01:2021", cwe: "CWE-269" },
1251
+ VC058: { owasp: "A05:2021", cwe: "CWE-250" },
1252
+ VC059: { owasp: "A05:2021", cwe: "CWE-284" },
1253
+ VC060: { owasp: "A02:2021", cwe: "CWE-328" },
1254
+ VC061: { owasp: "A02:2021", cwe: "CWE-295" },
1255
+ VC062: { owasp: "A02:2021", cwe: "CWE-321" },
1256
+ VC063: { owasp: "A03:2021", cwe: "CWE-79" },
1257
+ VC064: { owasp: "A01:2021", cwe: "CWE-862" },
1258
+ VC065: { owasp: "A01:2021", cwe: "CWE-862" },
1259
+ VC066: { owasp: "A07:2021", cwe: "CWE-798" },
1260
+ VC067: { owasp: "A01:2021", cwe: "CWE-601" },
1261
+ VC068: { owasp: "A07:2021", cwe: "CWE-922" },
1262
+ VC069: { owasp: "A02:2021", cwe: "CWE-295" },
1263
+ VC070: { owasp: "A05:2021", cwe: "CWE-489" },
1264
+ VC071: { owasp: "A05:2021", cwe: "CWE-215" },
1265
+ VC072: { owasp: "A07:2021", cwe: "CWE-798" },
1266
+ VC073: { owasp: "A08:2021", cwe: "CWE-502" },
1267
+ VC074: { owasp: "A01:2021", cwe: "CWE-352" },
1268
+ VC075: { owasp: "A03:2021", cwe: "CWE-78" },
1269
+ VC076: { owasp: "A07:2021", cwe: "CWE-798" },
1270
+ VC077: { owasp: "A05:2021", cwe: "CWE-942" },
1271
+ VC078: { owasp: "A05:2021", cwe: "CWE-250" },
1272
+ VC079: { owasp: "A02:2021", cwe: "CWE-327" },
1273
+ VC080: { owasp: "A04:2021", cwe: "CWE-1333" },
1274
+ VC081: { owasp: "A03:2021", cwe: "CWE-611" },
1275
+ VC082: { owasp: "A03:2021", cwe: "CWE-94" },
1276
+ VC083: { owasp: "A08:2021", cwe: "CWE-502" },
1277
+ VC084: { owasp: "A06:2021", cwe: "CWE-353" },
1278
+ VC085: { owasp: "A01:2021", cwe: "CWE-862" },
1279
+ VC086: { owasp: "A02:2021", cwe: "CWE-319" },
1280
+ VC087: { owasp: "A05:2021", cwe: "CWE-311" },
1281
+ VC088: { owasp: "A07:2021", cwe: "CWE-598" },
1282
+ VC089: { owasp: "A05:2021", cwe: "CWE-430" },
1283
+ VC090: { owasp: "A01:2021", cwe: "CWE-601" },
1284
+ VC091: { owasp: "A04:2021", cwe: "CWE-367" },
1285
+ VC092: { owasp: "A08:2021", cwe: "CWE-1321" },
1286
+ VC093: { owasp: "A01:2021", cwe: "CWE-22" },
1287
+ VC094: { owasp: "A03:2021", cwe: "CWE-78" },
1288
+ VC095: { owasp: "A05:2021", cwe: "CWE-942" },
1289
+ VC096: { owasp: "A02:2021", cwe: "CWE-319" },
1290
+ VC097: { owasp: "A09:2021", cwe: "CWE-532" },
1291
+ VC098: { owasp: "A04:2021", cwe: "CWE-400" },
1292
+ VC099: { owasp: "A04:2021", cwe: "CWE-401" },
1293
+ VC100: { owasp: "A04:2021", cwe: "CWE-400" },
1294
+ VC101: { owasp: "A04:2021", cwe: "CWE-400" },
1295
+ VC102: { owasp: "A04:2021", cwe: "CWE-400" },
1296
+ VC103: { owasp: "A04:2021", cwe: "CWE-710" },
1297
+ VC104: { owasp: "A04:2021", cwe: "CWE-390" },
1298
+ VC105: { owasp: "A04:2021", cwe: "CWE-710" },
1299
+ VC106: { owasp: "A04:2021", cwe: "CWE-710" },
1300
+ VC107: { owasp: "A02:2021", cwe: "CWE-311" },
1301
+ VC108: { owasp: "A01:2021", cwe: "CWE-284" },
1302
+ VC109: { owasp: "A01:2021", cwe: "CWE-284" },
1303
+ VC110: { owasp: "A09:2021", cwe: "CWE-778" },
1304
+ VC111: { owasp: "A05:2021", cwe: "CWE-16" },
1305
+ VC112: { owasp: "A06:2021", cwe: "CWE-1104" },
1306
+ VC113: { owasp: "A05:2021", cwe: "CWE-200" },
1307
+ VC114: { owasp: "A05:2021", cwe: "CWE-16" },
1308
+ VC115: { owasp: "A02:2021", cwe: "CWE-311" },
1309
+ VC116: { owasp: "A05:2021", cwe: "CWE-770" },
1310
+ VC117: { owasp: "A03:2021", cwe: "CWE-22" },
1311
+ VC118: { owasp: "A09:2021", cwe: "CWE-532" },
1312
+ VC119: { owasp: "A07:2021", cwe: "CWE-798" },
1313
+ VC120: { owasp: "A07:2021", cwe: "CWE-352" },
1314
+ VC121: { owasp: "A06:2021", cwe: "CWE-1104" },
1315
+ VC122: { owasp: "A02:2021", cwe: "CWE-326" },
1316
+ VC123: { owasp: "A02:2021", cwe: "CWE-326" },
1317
+ VC124: { owasp: "A02:2021", cwe: "CWE-327" },
1318
+ VC125: { owasp: "A07:2021", cwe: "CWE-640" },
1319
+ VC126: { owasp: "A05:2021", cwe: "CWE-200" },
1320
+ VC127: { owasp: "A01:2021", cwe: "CWE-862" },
1321
+ VC128: { owasp: "A05:2021", cwe: "CWE-444" },
1322
+ VC129: { owasp: "A02:2021", cwe: "CWE-311" },
1323
+ VC130: { owasp: "A07:2021", cwe: "CWE-307" },
1324
+ VC131: { owasp: "A06:2021", cwe: "CWE-1104" }
4098
1325
  };
4099
- var unencryptedPII = {
4100
- id: "VC129",
4101
- title: "Sensitive Data Stored Without Encryption",
4102
- severity: "high",
4103
- category: "Information Leakage",
4104
- description: "Database schemas storing PII (SSN, credit cards, health records) in plaintext violate compliance requirements and expose data in breaches.",
1326
+ var consoleLogProduction = {
1327
+ id: "VC097",
1328
+ title: "Console.log Left in Production Code",
1329
+ severity: "medium",
1330
+ category: "Performance",
1331
+ description: "console.log statements left in production code can leak sensitive data, slow down rendering, and clutter browser consoles.",
4105
1332
  check(content, filePath) {
4106
- if (isTestFile(filePath)) return [];
4107
- if (!filePath.match(/\.(sql|prisma|py|ts|js|rb)$/)) return [];
4108
- const findings = [];
4109
- const schemaPatterns = [
4110
- /(?:column|field|Column|Field)\s*\(\s*["'](?:ssn|social_security|tax_id|credit_card|card_number|cvv|passport|driver_license|bank_account|routing_number)["']\s*,\s*(?:String|TEXT|VARCHAR|Text|text)/gi,
4111
- /(?:ssn|social_security|tax_id|credit_card|card_number|cvv|passport_number|driver_license|bank_account|routing_number)\s+(?:TEXT|VARCHAR|String|text|character varying)/gi
4112
- ];
4113
- for (const pat of schemaPatterns) {
4114
- let m;
4115
- while ((m = pat.exec(content)) !== null) {
4116
- if (isCommentLine(content, m.index)) continue;
4117
- const lineNum = content.substring(0, m.index).split("\n").length;
4118
- findings.push({
4119
- rule: "VC129",
4120
- title: unencryptedPII.title,
4121
- severity: "high",
4122
- category: "Information Leakage",
4123
- file: filePath,
4124
- line: lineNum,
4125
- snippet: getSnippet(content, lineNum),
4126
- fix: "Encrypt sensitive fields at the application level before storing. Use field-level encryption with a KMS for SSN, credit cards, and health data."
4127
- });
1333
+ if (filePath.match(/test|spec|mock|__tests__|fixture|\.test\.|\.spec\./i)) return [];
1334
+ if (!/console\.log\s*\(/g.test(content)) return [];
1335
+ const lines = content.split("\n");
1336
+ const matches = [];
1337
+ for (let i = 0; i < lines.length; i++) {
1338
+ const line = lines[i].trim();
1339
+ if (/console\.log\s*\(/.test(line) && !line.startsWith("//") && !line.startsWith("*") && !/if\s*\(\s*(?:debug|process\.env)/i.test(lines[Math.max(0, i - 1)] + line)) {
1340
+ matches.push({ rule: "VC097", title: consoleLogProduction.title, severity: "medium", category: "Performance", file: filePath, line: i + 1, snippet: getSnippet(content, i + 1), fix: "Remove console.log or use a logger that can be disabled in production." });
4128
1341
  }
4129
1342
  }
4130
- return findings;
1343
+ return matches.slice(0, 3);
4131
1344
  }
4132
1345
  };
4133
- var missingAuthRateLimit = {
4134
- id: "VC130",
4135
- title: "Authentication Endpoint Without Rate Limiting",
4136
- severity: "high",
4137
- category: "Authentication",
4138
- description: "Login, registration, and password reset endpoints without rate limiting are vulnerable to credential stuffing and brute-force attacks.",
1346
+ var todoLeftInCode = {
1347
+ id: "VC103",
1348
+ title: "TODO/FIXME Left in Code",
1349
+ severity: "low",
1350
+ category: "Code Quality",
1351
+ description: "TODO, FIXME, HACK, and XXX comments indicate unfinished work that should be resolved before shipping to production.",
4139
1352
  check(content, filePath) {
4140
- if (isTestFile(filePath)) return [];
4141
- if (!filePath.match(/\.(js|ts|jsx|tsx|py|rb|go|java|php)$/)) return [];
4142
- const findings = [];
4143
- const authRoutePattern = /(?:app|router|server)\.(?:post|put)\s*\(\s*["'](?:\/(?:api\/)?(?:auth\/)?(?:login|signin|sign-in|register|signup|sign-up|forgot-password|reset-password))["']/gi;
4144
- let m;
4145
- while ((m = authRoutePattern.exec(content)) !== null) {
4146
- if (isCommentLine(content, m.index)) continue;
4147
- const surrounding = content.substring(Math.max(0, m.index - 300), Math.min(content.length, m.index + 300));
4148
- if (/rateLimit|rateLimiter|throttle|slowDown|express-rate-limit|rate_limit|RateLimiter|limiter/i.test(surrounding)) continue;
4149
- const lineNum = content.substring(0, m.index).split("\n").length;
4150
- findings.push({
4151
- rule: "VC130",
4152
- title: missingAuthRateLimit.title,
4153
- severity: "high",
4154
- category: "Authentication",
4155
- file: filePath,
4156
- line: lineNum,
4157
- snippet: getSnippet(content, lineNum),
4158
- fix: "Add rate limiting to authentication endpoints. Limit to 5-10 attempts per minute per IP. Use express-rate-limit or similar."
4159
- });
4160
- }
4161
- return findings;
1353
+ if (filePath.match(/test|spec|mock|__tests__|fixture|node_modules/i)) return [];
1354
+ return findMatches(
1355
+ content,
1356
+ /\/\/\s*(?:TODO|FIXME|HACK|XXX)\b/gi,
1357
+ todoLeftInCode,
1358
+ filePath,
1359
+ () => "Resolve TODO/FIXME comments before shipping. If it's intentional tech debt, track it in your issue tracker instead."
1360
+ ).slice(0, 5);
4162
1361
  }
4163
1362
  };
4164
- var vulnerableDependencies = {
4165
- id: "VC131",
4166
- title: "Potentially Vulnerable Dependency",
4167
- severity: "high",
4168
- category: "Supply Chain",
4169
- description: "Dependencies with known security issues that are commonly found in AI-generated codebases.",
1363
+ var emptyCatchBlock = {
1364
+ id: "VC104",
1365
+ title: "Empty Catch Block",
1366
+ severity: "medium",
1367
+ category: "Code Quality",
1368
+ description: "Empty catch blocks silently swallow errors, making bugs impossible to diagnose. At minimum, log the error.",
4170
1369
  check(content, filePath) {
4171
- if (!filePath.endsWith("package.json")) return [];
4172
- if (/node_modules|\.next|dist|build/.test(filePath)) return [];
4173
- const findings = [];
4174
- const vulnerablePackages = [
4175
- [/"jsonwebtoken"\s*:\s*"[\^~]?[0-7]\./g, "jsonwebtoken < 8.x has signature bypass vulnerabilities"],
4176
- [/"lodash"\s*:\s*"[\^~]?[0-3]\./g, "lodash < 4.x has prototype pollution vulnerabilities"],
4177
- [/"minimist"\s*:\s*"[\^~]?[01]\.[01]\./g, "minimist < 1.2.6 has prototype pollution"],
4178
- [/"node-fetch"\s*:\s*"[\^~]?[12]\./g, "node-fetch < 3.x has data exposure issues"],
4179
- [/"express"\s*:\s*"[\^~]?[0-3]\./g, "express < 4.x has multiple known vulnerabilities"],
4180
- [/"axios"\s*:\s*"[\^~]?0\.[0-9]\./g, "axios < 0.21 has SSRF vulnerabilities"],
4181
- [/"tar"\s*:\s*"[\^~]?[0-5]\./g, "tar < 6.x has path traversal vulnerabilities"],
4182
- [/"glob-parent"\s*:\s*"[\^~]?[0-4]\./g, "glob-parent < 5.1.2 has ReDoS vulnerability"]
4183
- ];
4184
- for (const [pat, message] of vulnerablePackages) {
4185
- let m;
4186
- while ((m = pat.exec(content)) !== null) {
4187
- const lineNum = content.substring(0, m.index).split("\n").length;
4188
- findings.push({
4189
- rule: "VC131",
4190
- title: vulnerableDependencies.title,
4191
- severity: "high",
4192
- category: "Supply Chain",
4193
- file: filePath,
4194
- line: lineNum,
4195
- snippet: getSnippet(content, lineNum),
4196
- fix: `${message}. Update to the latest version and run 'npm audit' regularly.`
4197
- });
4198
- }
4199
- }
4200
- return findings;
1370
+ if (filePath.match(/test|spec|mock/i)) return [];
1371
+ return findMatches(
1372
+ content,
1373
+ /catch\s*(?:\([^)]*\))?\s*\{\s*\}/g,
1374
+ emptyCatchBlock,
1375
+ filePath,
1376
+ () => "Handle errors in catch blocks: catch(err) { console.error('Operation failed:', err); } or re-throw if appropriate."
1377
+ );
4201
1378
  }
4202
1379
  };
4203
1380
  var freeRules = [
@@ -4262,160 +1439,12 @@ var freeRules = [
4262
1439
  disabledTLSVerification
4263
1440
  // VC061
4264
1441
  ];
4265
- var proRules = [
4266
- hardcodedSecrets,
4267
- exposedEnvFile,
4268
- missingAuthMiddleware,
4269
- supabaseNoRLS,
4270
- stripeWebhookUnprotected,
4271
- sqlInjection,
4272
- xssVulnerability,
4273
- noRateLimiting,
4274
- corsWildcard,
4275
- clientSideAuth,
4276
- nextPublicSecret,
4277
- firebaseClientConfig,
4278
- supabaseAnonAdmin,
4279
- envNotGitignored,
4280
- evalUsage,
4281
- unvalidatedRedirect,
4282
- insecureCookies,
4283
- exposedAuthSecret,
4284
- insecureElectronWindow,
4285
- missingCSP,
4286
- ipcPathTraversal,
4287
- unsanitizedHTMLExport,
4288
- prototypePollution,
4289
- missingFileSizeLimits,
4290
- unsanitizedFilenames,
4291
- electronNavigationUnrestricted,
4292
- missingSecurityMeta,
4293
- unvalidatedAPIParams,
4294
- unvalidatedEventData,
4295
- insecureDeserialization,
4296
- hardcodedJWTSecret,
4297
- missingHTTPS,
4298
- exposedDebugMode,
4299
- insecureRandomness,
4300
- openRedirectParams,
4301
- missingErrorBoundary,
4302
- exposedStackTraces,
4303
- insecureFileUpload,
4304
- missingLockFile,
4305
- exposedGitDir,
4306
- ssrfVulnerability,
4307
- massAssignment,
4308
- timingAttack,
4309
- logInjection,
4310
- weakPasswordRequirements,
4311
- sessionFixation,
4312
- missingBruteForce,
4313
- nosqlInjection,
4314
- exposedDBCredentials,
4315
- missingDBEncryption,
4316
- graphqlIntrospection,
4317
- missingRequestSizeLimit,
4318
- hardcodedIPAllowlist,
4319
- sensitiveLocalStorage,
4320
- exposedSourceMaps,
4321
- clickjacking,
4322
- overlyPermissiveIAM,
4323
- dockerRunAsRoot,
4324
- exposedDockerPorts,
4325
- weakHashing,
4326
- disabledTLSVerification,
4327
- hardcodedEncryptionKey,
4328
- dangerousInnerHTML,
4329
- exposedServerActions,
4330
- unprotectedAPIRoutes,
4331
- clientComponentSecret,
4332
- insecureDeepLink,
4333
- sensitiveAsyncStorage,
4334
- missingCertPinning,
4335
- androidDebuggable,
4336
- djangoDebug,
4337
- flaskSecretKey,
4338
- pickleDeserialization,
4339
- missingCSRF,
4340
- githubActionsInjection,
4341
- secretsInCI,
4342
- corsServerless,
4343
- k8sPrivileged,
4344
- jwtAlgConfusion,
4345
- regexDos,
4346
- xxeVulnerability,
4347
- ssti,
4348
- javaDeserialization,
4349
- missingSRI,
4350
- exposedAdminRoutes,
4351
- insecureWebSocket,
4352
- missingHSTS,
4353
- sensitiveURLParams,
4354
- missingContentDisposition,
4355
- hostHeaderRedirect,
4356
- raceCondition,
4357
- unsafeObjectAssign,
4358
- unprotectedDownload,
4359
- commandInjection,
4360
- corsLocalhost,
4361
- insecureGRPC,
4362
- consoleLogProduction,
4363
- syncFileOps,
4364
- eventListenerLeak,
4365
- nPlusOneQuery,
4366
- largeBundleImport,
4367
- blockingMainThread,
4368
- todoLeftInCode,
4369
- emptyCatchBlock,
4370
- callbackHell,
4371
- magicNumbers,
4372
- s3BucketNoEncryption,
4373
- securityGroupAllInbound,
4374
- rdsPubliclyAccessible,
4375
- missingCloudTrail,
4376
- lambdaWithoutVPC,
4377
- dockerLatestTag,
4378
- dockerCopySensitive,
4379
- dockerTooManyPorts,
4380
- k8sSecretNotEncrypted,
4381
- k8sNoResourceLimits,
4382
- pathTraversal,
4383
- // VC117
4384
- piiInLogs,
4385
- // VC118
4386
- hardcodedOAuthSecret,
4387
- // VC119
4388
- missingOAuthState,
4389
- // VC120
4390
- unpinnedGitHubAction,
4391
- // VC121
4392
- deprecatedTLS,
4393
- // VC122
4394
- weakRSAKeySize,
4395
- // VC123
4396
- ecbModeEncryption,
4397
- // VC124
4398
- insecurePasswordReset,
4399
- // VC125
4400
- terraformStateExposed,
4401
- // VC126
4402
- insecureHTTPMethods,
4403
- // VC127
4404
- httpRequestSmuggling,
4405
- // VC128
4406
- unencryptedPII,
4407
- // VC129
4408
- missingAuthRateLimit,
4409
- // VC130
4410
- vulnerableDependencies
4411
- // VC131
4412
- ];
4413
- function runCustomRules(content, filePath, disabledRules = [], tier = "free") {
1442
+ function runCustomRules(content, filePath, disabledRules = [], tier = "free", extraRules = []) {
4414
1443
  const findings = [];
4415
1444
  if (/function runScan\(files\)|export function runCustomRules/.test(content) && /const (?:rules|allRules)\s*[:=]/.test(content) && /findMatches/.test(content)) {
4416
1445
  return findings;
4417
1446
  }
4418
- const ruleset = tier === "pro" ? proRules : freeRules;
1447
+ const ruleset = tier === "pro" && extraRules.length > 0 ? [...freeRules, ...extraRules] : freeRules;
4419
1448
  for (const rule of ruleset) {
4420
1449
  if (disabledRules.includes(rule.id)) continue;
4421
1450
  const matches = rule.check(content, filePath);
@@ -6032,6 +3061,22 @@ async function scanCommand(directory, options) {
6032
3061
  `));
6033
3062
  }
6034
3063
  }
3064
+ let proRulesExtra = [];
3065
+ if (tier === "pro") {
3066
+ const cached = loadCachedProRules();
3067
+ if (cached) {
3068
+ proRulesExtra = cached;
3069
+ } else {
3070
+ if (!isSilent) console.log(chalk2.gray(" Downloading Pro rules..."));
3071
+ const downloaded = await downloadProRulesBundle();
3072
+ if (downloaded) {
3073
+ proRulesExtra = loadCachedProRules() || [];
3074
+ }
3075
+ if (proRulesExtra.length === 0 && !isSilent) {
3076
+ console.log(chalk2.yellow(" Could not load Pro rules \u2014 scanning with free rules only"));
3077
+ }
3078
+ }
3079
+ }
6035
3080
  const spinner = ora({
6036
3081
  text: "Scanning files...",
6037
3082
  color: "cyan",
@@ -6071,7 +3116,7 @@ async function scanCommand(directory, options) {
6071
3116
  fileContentsForAnalysis.push({ path: filePath, content });
6072
3117
  const astCtx = buildASTContext(content, filePath);
6073
3118
  if (astCtx.isScannerFile) continue;
6074
- const findings = runCustomRules(content, filePath, config.disableRules, tier);
3119
+ const findings = runCustomRules(content, filePath, config.disableRules, tier, proRulesExtra);
6075
3120
  for (const f of findings) {
6076
3121
  if (astCtx.isTestFile) {
6077
3122
  f.confidence = "low";
@@ -6311,6 +3356,7 @@ async function logoutCommand() {
6311
3356
  return;
6312
3357
  }
6313
3358
  clearToken();
3359
+ clearProRulesCache();
6314
3360
  console.log(chalk3.green("Logged out successfully."));
6315
3361
  }
6316
3362
  async function whoamiCommand() {
@@ -6448,7 +3494,7 @@ auth.command("login").description("Log in to your XploitScan account").action(lo
6448
3494
  auth.command("logout").description("Log out of your XploitScan account").action(logoutCommand);
6449
3495
  auth.command("whoami").description("Show current logged-in user").action(whoamiCommand);
6450
3496
  program.command("upgrade").description("Upgrade to XploitScan Pro for unlimited scans").action(async () => {
6451
- const { getStoredToken: getStoredToken2, getCheckoutUrl } = await import("./api-Z7VNGPT2.js");
3497
+ const { getStoredToken: getStoredToken2, getCheckoutUrl } = await import("./api-ZNWEMMEL.js");
6452
3498
  const chalk4 = (await import("chalk")).default;
6453
3499
  const token = getStoredToken2();
6454
3500
  if (!token) {