xploitscan-shared-rules 1.13.0 → 1.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -97,6 +97,7 @@ __export(index_exports, {
97
97
  hardcodedAnthropicKey: () => hardcodedAnthropicKey,
98
98
  hardcodedCloudflareToken: () => hardcodedCloudflareToken,
99
99
  hardcodedCohereKey: () => hardcodedCohereKey,
100
+ hardcodedCreditCard: () => hardcodedCreditCard,
100
101
  hardcodedDatadogKey: () => hardcodedDatadogKey,
101
102
  hardcodedDiscordToken: () => hardcodedDiscordToken,
102
103
  hardcodedEncryptionKey: () => hardcodedEncryptionKey,
@@ -126,6 +127,7 @@ __export(index_exports, {
126
127
  hardcodedRailwayToken: () => hardcodedRailwayToken,
127
128
  hardcodedReplicateKey: () => hardcodedReplicateKey,
128
129
  hardcodedResendKey: () => hardcodedResendKey,
130
+ hardcodedSSN: () => hardcodedSSN,
129
131
  hardcodedSecrets: () => hardcodedSecrets,
130
132
  hardcodedSendGridKey: () => hardcodedSendGridKey,
131
133
  hardcodedSentryAuthToken: () => hardcodedSentryAuthToken,
@@ -264,15 +266,50 @@ __export(index_exports, {
264
266
  module.exports = __toCommonJS(index_exports);
265
267
 
266
268
  // src/snippet.ts
267
- function getSnippet(content, line, contextLines = 2) {
269
+ var cacheContent = null;
270
+ var cacheLines = [];
271
+ var cacheOffsets = [];
272
+ function lineIndex(content) {
273
+ if (content === cacheContent) return { lines: cacheLines, offsets: cacheOffsets };
268
274
  const lines = content.split("\n");
275
+ const offsets = new Array(lines.length);
276
+ let off = 0;
277
+ for (let i = 0; i < lines.length; i++) {
278
+ offsets[i] = off;
279
+ off += lines[i].length + 1;
280
+ }
281
+ cacheContent = content;
282
+ cacheLines = lines;
283
+ cacheOffsets = offsets;
284
+ return { lines, offsets };
285
+ }
286
+ function lineNumberAt(content, index) {
287
+ const { offsets } = lineIndex(content);
288
+ let lo = 0;
289
+ let hi = offsets.length - 1;
290
+ let ans = 0;
291
+ while (lo <= hi) {
292
+ const mid = lo + hi >> 1;
293
+ if (offsets[mid] <= index) {
294
+ ans = mid;
295
+ lo = mid + 1;
296
+ } else {
297
+ hi = mid - 1;
298
+ }
299
+ }
300
+ return ans + 1;
301
+ }
302
+ function getSnippet(content, line, contextLines = 2) {
303
+ const { lines } = lineIndex(content);
269
304
  const start = Math.max(0, line - 1 - contextLines);
270
305
  const end = Math.min(lines.length, line + contextLines);
271
- return lines.slice(start, end).map((l, i) => {
272
- const lineNum = start + i + 1;
306
+ const out = [];
307
+ for (let i = start; i < end; i++) {
308
+ const lineNum = i + 1;
273
309
  const marker = lineNum === line ? ">" : " ";
274
- return `${marker} ${lineNum.toString().padStart(4)} | ${l}`;
275
- }).join("\n");
310
+ out.push(`${marker} ${lineNum.toString().padStart(4)} | ${lines[i]}`);
311
+ }
312
+ return out.join("\n");
276
313
  }
277
314
 
278
315
  // src/rule-impacts.ts
@@ -486,7 +523,9 @@ var RULE_IMPACTS = {
486
523
  VC207: "Model output is attacker-influenceable via prompt injection. Feeding it into eval, new Function, a shell command, a raw SQL string, or a filesystem path turns a crafted or hallucinated response into remote code execution, command injection, SQL injection, or path traversal \u2014 your most dangerous sinks, driven by untrusted text.",
487
524
  VC208: "Interpolating a secret into a prompt ships your API key, token, or password to a third-party model provider, where it persists in their request logs and training-eligible data. A credential that leaves your infrastructure in prompt text should be considered compromised and rotated.",
488
525
  VC209: "Webhooks are delivered at-least-once. Without de-duplicating on the event id, a retried or replayed delivery re-runs the side effect \u2014 a customer is charged twice, a record is duplicated, or an entitlement is granted again. Stripe and Svix both retry on any non-2xx, so this fires in normal operation, not just under attack.",
489
- VC210: "If your auth middleware skips /api, those routes run with no gate unless each one re-checks auth itself. It is the most common way a Next.js app ends up with publicly callable API routes that everyone assumed the middleware was protecting."
526
+ VC210: "If your auth middleware skips /api, those routes run with no gate unless each one re-checks auth itself. It is the most common way a Next.js app ends up with publicly callable API routes that everyone assumed the middleware was protecting.",
527
+ VC211: "A real credit card number in source is cardholder data sitting in git history, CI logs, and every backup \u2014 a direct PCI-DSS violation. Anyone with repo access can read it, and it cannot be un-leaked once committed; the card must be treated as compromised.",
528
+ VC212: "A hardcoded Social Security Number is regulated PII permanently embedded in your git history and backups. It exposes a real person to identity theft, and its presence in source can trigger breach-notification and privacy-law obligations the moment the repo is accessed."
490
529
  };
491
530
 
492
531
  // src/exposure.ts
@@ -1080,7 +1119,7 @@ var SERVER_SIDE_PATH_RE = new RegExp(
1080
1119
  ].join("|"),
1081
1120
  "i"
1082
1121
  );
1083
- var SERVER_SIDE_CONTENT_RE = /\b(?:req|request)\.(?:body|query|params|headers|cookies)\b|\b(?:app|router)\.(?:get|post|put|patch|delete|use|all)\s*\(|\bctx\.request\b|\bevent\.(?:body|queryStringParameters|pathParameters)\b|\(\s*req\s*,\s*res\b|(?:import|require)\b[^;\n]*['"]express['"]|\bNextFunction\b/;
1122
+ var SERVER_SIDE_CONTENT_RE = /\b(?:req|request)\.(?:body|query|params|headers|cookies)\b|\b(?:app|router)\.(?:get|post|put|patch|delete|use|all)\s*\(|\bctx\.request\b|\bevent\.(?:body|queryStringParameters|pathParameters)\b|\(\s*req\s*,\s*res\b|(?:import|require)\b[^;\n]{0,200}['"]express['"]|\bNextFunction\b/;
1084
1123
  function isServerSideFile(filePath, content) {
1085
1124
  if (isTestFile(filePath)) return false;
1086
1125
  if (SERVER_SIDE_PATH_RE.test(filePath)) return true;
@@ -1127,13 +1166,23 @@ function filterSilenced(matches, content, ruleId) {
1127
1166
  }
1128
1167
  function findMatches(content, pattern, rule, filePath, fixTemplate) {
1129
1168
  const matches = [];
1130
- const lines = content.split("\n");
1131
1169
  let m;
1132
1170
  const re = new RegExp(pattern.source, pattern.flags.includes("g") ? pattern.flags : `${pattern.flags}g`);
1171
+ let scanned = 0;
1172
+ let lineNum = 1;
1173
+ let lastMatchIndex = -1;
1174
+ const MAX_MATCHES = 500;
1133
1175
  while ((m = re.exec(content)) !== null) {
1176
+ if (m.index === lastMatchIndex && m[0].length === 0) {
1177
+ re.lastIndex++;
1178
+ continue;
1179
+ }
1180
+ lastMatchIndex = m.index;
1134
1181
  if (isCommentLine(content, m.index)) continue;
1135
1182
  if (isInsideFixMessage(content, m.index)) continue;
1136
- const lineNum = content.substring(0, m.index).split("\n").length;
1183
+ for (; scanned < m.index; scanned++) {
1184
+ if (content.charCodeAt(scanned) === 10) lineNum++;
1185
+ }
1137
1186
  matches.push({
1138
1187
  rule: rule.id,
1139
1188
  title: rule.title,
@@ -1144,6 +1193,7 @@ function findMatches(content, pattern, rule, filePath, fixTemplate) {
1144
1193
  snippet: getSnippet(content, lineNum),
1145
1194
  fix: fixTemplate?.(m)
1146
1195
  });
1196
+ if (matches.length >= MAX_MATCHES) break;
1147
1197
  }
1148
1198
  return matches;
1149
1199
  }
@@ -1159,10 +1209,26 @@ function astMatch(content, filePath, line, rule, fix) {
1159
1209
  fix
1160
1210
  };
1161
1211
  }
1212
+ var parseCacheContent = null;
1213
+ var parseCachePath = null;
1214
+ var parseCacheResult = null;
1215
+ var parseCacheValid = false;
1162
1216
  function tryParse(content, filePath) {
1163
- const parsed = parseFile(content, filePath);
1164
- if (!parsed) return null;
1165
- return { parsed, taint: buildTaintMap(parsed) };
1217
+ if (parseCacheValid && content === parseCacheContent && filePath === parseCachePath) {
1218
+ return parseCacheResult;
1219
+ }
1220
+ let result = null;
1221
+ try {
1222
+ const parsed = parseFile(content, filePath);
1223
+ result = parsed ? { parsed, taint: buildTaintMap(parsed) } : null;
1224
+ } catch {
1225
+ result = null;
1226
+ }
1227
+ parseCacheResult = result;
1228
+ parseCacheContent = content;
1229
+ parseCachePath = filePath;
1230
+ parseCacheValid = true;
1231
+ return result;
1166
1232
  }
1167
1233
  var hardcodedSecrets = {
1168
1234
  id: "VC001",
@@ -1530,7 +1596,7 @@ var xssVulnerability = {
1530
1596
  const hasSanitizerBypass = /bypassSecurityTrust(?:Html|Script|Style|Url|ResourceUrl)\s*\(/.test(content);
1531
1597
  if (/(?:sanitize|purify|escape|xss)/i.test(filePath)) return [];
1532
1598
  if (!hasSanitizerBypass && /DOMPurify\.sanitize|sanitizeHtml|xss\(|escapeHtml/i.test(content)) return [];
1533
- if (!hasSanitizerBypass && /(?:import|require)\s*\(?.*(?:DOMPurify|dompurify|sanitize|sanitizer)/i.test(content)) return [];
1599
+ if (!hasSanitizerBypass && /(?:import|require)\s*\(?[^\n]{0,200}(?:DOMPurify|dompurify|sanitize|sanitizer)/i.test(content)) return [];
1534
1600
  const patterns = [
1535
1601
  // React dangerouslySetInnerHTML
1536
1602
  /dangerouslySetInnerHTML\s*=\s*\{\s*\{\s*__html\s*:/g,
@@ -1559,9 +1625,9 @@ var xssVulnerability = {
1559
1625
  () => "Sanitize user input before rendering as HTML. Use a library like DOMPurify: DOMPurify.sanitize(userInput). If this site is intentional (JSON-LD structured data, a const boot script, a sandboxed preview), add an inline `// VC007-OK: <reason>` comment above the line to silence."
1560
1626
  );
1561
1627
  for (const m of raw) {
1562
- const lineText = allLines[m.line - 1] || "";
1628
+ const lineText = (allLines[m.line - 1] || "").slice(0, 4e3);
1563
1629
  if (/\.innerHTML\s*=\s*['"]/.test(lineText) && !/\$\{/.test(lineText)) continue;
1564
- if (/\.innerHTML\s*=\s*['"][^'"]*['"]\s*$/.test(lineText)) continue;
1630
+ if (/\.innerHTML\s*=\s*['"][^'"]{0,2000}['"]\s*$/.test(lineText)) continue;
1565
1631
  if (/dangerouslySetInnerHTML/.test(lineText)) {
1566
1632
  const windowText = allLines.slice(m.line - 1, m.line + 2).join("\n");
1567
1633
  if (/JSON\.stringify/i.test(windowText)) continue;
@@ -1812,7 +1878,7 @@ var evalUsage = {
1812
1878
  /new\s+Function\s*\(\s*(?!["'`])/g
1813
1879
  ];
1814
1880
  const hasEvalInString = /["'`]eval(?:-source-map|["'`])/i.test(content);
1815
- if (hasEvalInString && !/\beval\s*\([^)]*(?:req\.|body\.|input|params|user|data)/i.test(content)) return [];
1881
+ if (hasEvalInString && !/\beval\s*\([^)]{0,500}(?:req\.|body\.|input|params|user|data)/i.test(content)) return [];
1816
1882
  const matches = [];
1817
1883
  for (const p of patterns) {
1818
1884
  const rawMatches = findMatches(
@@ -1853,7 +1919,7 @@ var unvalidatedRedirect = {
1853
1919
  const isPureLiteral = /^["'`][^"'`]*["'`]$/.test(value);
1854
1920
  if (!hasInterpolation && isPureLiteral) continue;
1855
1921
  if (isInlineSilenced(content, m.index, "VC016")) continue;
1856
- const line = content.substring(0, m.index).split("\n").length;
1922
+ const line = lineNumberAt(content, m.index);
1857
1923
  matches.push({
1858
1924
  rule: "VC016",
1859
1925
  title: unvalidatedRedirect.title,
@@ -2076,11 +2142,11 @@ var prototypePollution = {
2076
2142
  ];
2077
2143
  const hasValidation = /schema|validate|sanitize|whitelist|allowedKeys|pick\(|Object\.freeze|zod|yup|joi|ajv/i.test(content);
2078
2144
  if (!hasValidation) {
2079
- const hasUnsafeMerge = /Object\.assign\s*\([^)]*JSON\.parse|\.\.\.JSON\.parse|\{.*\.\.\.(?:stored|saved|cached|parsed|data)/i.test(content);
2145
+ const hasUnsafeMerge = /Object\.assign\s*\([^)]{0,500}JSON\.parse|\.\.\.JSON\.parse|\{[^\n]{0,300}\.\.\.(?:stored|saved|cached|parsed|data)/i.test(content);
2080
2146
  if (hasUnsafeMerge) {
2081
2147
  matches.push(...findMatches(
2082
2148
  content,
2083
- /Object\.assign\s*\([^)]*JSON\.parse|\.\.\.JSON\.parse/g,
2149
+ /Object\.assign\s*\([^)]{0,500}JSON\.parse|\.\.\.JSON\.parse/g,
2084
2150
  prototypePollution,
2085
2151
  filePath,
2086
2152
  () => "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."
@@ -3626,7 +3692,7 @@ var dangerousInnerHTML = {
3626
3692
  if (/^[A-Z][A-Z0-9_]+$/.test(value)) continue;
3627
3693
  if (/^["'`][^$]*["'`]$/.test(value)) continue;
3628
3694
  if (/JSON\.stringify/i.test(value)) continue;
3629
- const lineNum = content.substring(0, m.index).split("\n").length;
3695
+ const lineNum = lineNumberAt(content, m.index);
3630
3696
  findings.push({
3631
3697
  rule: "VC063",
3632
3698
  title: dangerousInnerHTML.title,
@@ -4368,7 +4434,7 @@ var missingSRI = {
4368
4434
  const re = new RegExp(scriptPattern.source, scriptPattern.flags);
4369
4435
  while ((m = re.exec(content)) !== null) {
4370
4436
  if (!m[0].includes("integrity")) {
4371
- const lineNum = content.substring(0, m.index).split("\n").length;
4437
+ const lineNum = lineNumberAt(content, m.index);
4372
4438
  matches.push({
4373
4439
  rule: "VC084",
4374
4440
  title: missingSRI.title,
@@ -5000,8 +5066,13 @@ var complianceMap = {
5000
5066
  // VC209–VC210: advisory heuristics
5001
5067
  VC209: { owasp: "A04:2021", cwe: "CWE-799" },
5002
5068
  // webhook missing idempotency
5003
- VC210: { owasp: "A01:2021", cwe: "CWE-862" }
5069
+ VC210: { owasp: "A01:2021", cwe: "CWE-862" },
5004
5070
  // middleware matcher excludes /api
5071
+ // VC211–VC212: hardcoded sensitive personal data (PII) in source
5072
+ VC211: { owasp: "A02:2021", cwe: "CWE-540" },
5073
+ // hardcoded credit card number
5074
+ VC212: { owasp: "A02:2021", cwe: "CWE-540" }
5075
+ // hardcoded US SSN
5005
5076
  };
5006
5077
  var consoleLogProduction = {
5007
5078
  id: "VC097",
@@ -5239,7 +5310,7 @@ var s3BucketNoEncryption = {
5239
5310
  const blockEnd = Math.min(m.index + 2e3, content.length);
5240
5311
  const blockContent = content.substring(m.index, blockEnd);
5241
5312
  if (!/server_side_encryption/.test(blockContent)) {
5242
- const lineNum = content.substring(0, m.index).split("\n").length;
5313
+ const lineNum = lineNumberAt(content, m.index);
5243
5314
  matches.push({
5244
5315
  rule: "VC107",
5245
5316
  title: s3BucketNoEncryption.title,
@@ -5270,7 +5341,7 @@ var securityGroupAllInbound = {
5270
5341
  while ((m = ingressPattern.exec(content)) !== null) {
5271
5342
  if (isCommentLine(content, m.index)) continue;
5272
5343
  if (/from_port\s*=\s*0/.test(m[0]) || !/from_port/.test(m[0])) {
5273
- const lineNum = content.substring(0, m.index).split("\n").length;
5344
+ const lineNum = lineNumberAt(content, m.index);
5274
5345
  matches.push({
5275
5346
  rule: "VC108",
5276
5347
  title: securityGroupAllInbound.title,
@@ -5357,7 +5428,7 @@ var lambdaWithoutVPC = {
5357
5428
  const blockEnd = Math.min(m.index + 2e3, content.length);
5358
5429
  const blockContent = content.substring(m.index, blockEnd);
5359
5430
  if (!/vpc_config\s*\{/.test(blockContent)) {
5360
- const lineNum = content.substring(0, m.index).split("\n").length;
5431
+ const lineNum = lineNumberAt(content, m.index);
5361
5432
  matches.push({
5362
5433
  rule: "VC111",
5363
5434
  title: lambdaWithoutVPC.title,
@@ -5387,7 +5458,7 @@ var dockerLatestTag = {
5387
5458
  while ((m = fromPattern.exec(content)) !== null) {
5388
5459
  const image = m[1];
5389
5460
  if (image.endsWith(":latest") || !image.includes(":") && !image.startsWith("$")) {
5390
- const lineNum = content.substring(0, m.index).split("\n").length;
5461
+ const lineNum = lineNumberAt(content, m.index);
5391
5462
  matches.push({
5392
5463
  rule: "VC112",
5393
5464
  title: dockerLatestTag.title,
@@ -5515,17 +5586,26 @@ var pathTraversal = {
5515
5586
  const findings = [];
5516
5587
  const patterns = [
5517
5588
  /(?:readFile|readFileSync|createReadStream|writeFile|writeFileSync|appendFile|unlink|unlinkSync|access|stat|statSync|existsSync)\s*\(\s*(?:req\.|request\.|ctx\.|params\.|query\.)/gi,
5518
- /(?:path\.join|path\.resolve)\s*\([^)]*(?:req\.|request\.|ctx\.|params\.|query\.)[^)]*\)/gi,
5519
- /(?:res\.sendFile|res\.download)\s*\([^)]*(?:req\.|request\.)/gi,
5589
+ /(?:path\.join|path\.resolve)\s*\([^)]{0,500}(?:req\.|request\.|ctx\.|params\.|query\.)[^)]{0,500}\)/gi,
5590
+ /(?:res\.sendFile|res\.download)\s*\([^)]{0,500}(?:req\.|request\.)/gi,
5520
5591
  /open\s*\(\s*(?:request\.|params\[|args\.)/gi
5521
5592
  ];
5593
+ const MAX_MATCHES = 500;
5522
5594
  for (const pat of patterns) {
5595
+ if (findings.length >= MAX_MATCHES) break;
5523
5596
  let m;
5597
+ let lastIdx = -1;
5524
5598
  while ((m = pat.exec(content)) !== null) {
5599
+ if (findings.length >= MAX_MATCHES) break;
5600
+ if (m.index === lastIdx && m[0].length === 0) {
5601
+ pat.lastIndex++;
5602
+ continue;
5603
+ }
5604
+ lastIdx = m.index;
5525
5605
  if (isCommentLine(content, m.index)) continue;
5526
5606
  const surrounding = content.substring(Math.max(0, m.index - 200), m.index + 200);
5527
5607
  if (/path\.normalize|sanitize|\.replace\(\s*['"]\.\.|\.startsWith\s*\(|realpath/i.test(surrounding)) continue;
5528
- const lineNum = content.substring(0, m.index).split("\n").length;
5608
+ const lineNum = lineNumberAt(content, m.index);
5529
5609
  findings.push({
5530
5610
  rule: "VC117",
5531
5611
  title: pathTraversal.title,
@@ -5556,7 +5636,7 @@ var piiInLogs = {
5556
5636
  while ((m = logPattern.exec(content)) !== null) {
5557
5637
  if (isCommentLine(content, m.index)) continue;
5558
5638
  if (isInsideFixMessage(content, m.index)) continue;
5559
- const lineNum = content.substring(0, m.index).split("\n").length;
5639
+ const lineNum = lineNumberAt(content, m.index);
5560
5640
  findings.push({
5561
5641
  rule: "VC118",
5562
5642
  title: piiInLogs.title,
@@ -5594,7 +5674,7 @@ var hardcodedOAuthSecret = {
5594
5674
  while ((m = pat.exec(content)) !== null) {
5595
5675
  if (isCommentLine(content, m.index)) continue;
5596
5676
  if (/process\.env|os\.environ|ENV\[|getenv|\$\{|<your|CHANGE_ME|xxx|placeholder/i.test(m[0])) continue;
5597
- const lineNum = content.substring(0, m.index).split("\n").length;
5677
+ const lineNum = lineNumberAt(content, m.index);
5598
5678
  findings.push({
5599
5679
  rule: "VC119",
5600
5680
  title: hardcodedOAuthSecret.title,
@@ -5626,7 +5706,7 @@ var missingOAuthState = {
5626
5706
  if (isCommentLine(content, m.index)) continue;
5627
5707
  const surrounding = content.substring(m.index, Math.min(content.length, m.index + 500));
5628
5708
  if (/state\s*[=:]/i.test(surrounding)) continue;
5629
- const lineNum = content.substring(0, m.index).split("\n").length;
5709
+ const lineNum = lineNumberAt(content, m.index);
5630
5710
  findings.push({
5631
5711
  rule: "VC120",
5632
5712
  title: missingOAuthState.title,
@@ -5654,7 +5734,7 @@ var unpinnedGitHubAction = {
5654
5734
  let m;
5655
5735
  while ((m = usesPattern.exec(content)) !== null) {
5656
5736
  if (isCommentLine(content, m.index)) continue;
5657
- const lineNum = content.substring(0, m.index).split("\n").length;
5737
+ const lineNum = lineNumberAt(content, m.index);
5658
5738
  findings.push({
5659
5739
  rule: "VC121",
5660
5740
  title: unpinnedGitHubAction.title,
@@ -5688,7 +5768,7 @@ var deprecatedTLS = {
5688
5768
  let m;
5689
5769
  while ((m = pat.exec(content)) !== null) {
5690
5770
  if (isCommentLine(content, m.index)) continue;
5691
- const lineNum = content.substring(0, m.index).split("\n").length;
5771
+ const lineNum = lineNumberAt(content, m.index);
5692
5772
  findings.push({
5693
5773
  rule: "VC122",
5694
5774
  title: deprecatedTLS.title,
@@ -5724,7 +5804,7 @@ var weakRSAKeySize = {
5724
5804
  let m;
5725
5805
  while ((m = pat.exec(content)) !== null) {
5726
5806
  if (isCommentLine(content, m.index)) continue;
5727
- const lineNum = content.substring(0, m.index).split("\n").length;
5807
+ const lineNum = lineNumberAt(content, m.index);
5728
5808
  findings.push({
5729
5809
  rule: "VC123",
5730
5810
  title: weakRSAKeySize.title,
@@ -5760,7 +5840,7 @@ var ecbModeEncryption = {
5760
5840
  let m;
5761
5841
  while ((m = pat.exec(content)) !== null) {
5762
5842
  if (isCommentLine(content, m.index)) continue;
5763
- const lineNum = content.substring(0, m.index).split("\n").length;
5843
+ const lineNum = lineNumberAt(content, m.index);
5764
5844
  findings.push({
5765
5845
  rule: "VC124",
5766
5846
  title: ecbModeEncryption.title,
@@ -5791,7 +5871,7 @@ var insecurePasswordReset = {
5791
5871
  let m;
5792
5872
  while ((m = weakTokens.exec(content)) !== null) {
5793
5873
  if (isCommentLine(content, m.index)) continue;
5794
- const lineNum = content.substring(0, m.index).split("\n").length;
5874
+ const lineNum = lineNumberAt(content, m.index);
5795
5875
  findings.push({
5796
5876
  rule: "VC125",
5797
5877
  title: insecurePasswordReset.title,
@@ -5808,7 +5888,7 @@ var insecurePasswordReset = {
5808
5888
  if (isCommentLine(content, m.index)) continue;
5809
5889
  const surrounding = content.substring(Math.max(0, m.index - 500), m.index);
5810
5890
  if (!/(?:reset|forgot).*password/i.test(surrounding)) continue;
5811
- const lineNum = content.substring(0, m.index).split("\n").length;
5891
+ const lineNum = lineNumberAt(content, m.index);
5812
5892
  findings.push({
5813
5893
  rule: "VC125",
5814
5894
  title: insecurePasswordReset.title,
@@ -5869,7 +5949,7 @@ var insecureHTTPMethods = {
5869
5949
  if (isCommentLine(content, m.index)) continue;
5870
5950
  const handlerBlock = content.substring(m.index, Math.min(content.length, m.index + 500));
5871
5951
  if (/auth|authenticate|authorize|requireAuth|isAuthenticated|protect|guard|middleware|session|jwt|verify|clerk|getAuth/i.test(handlerBlock)) continue;
5872
- const lineNum = content.substring(0, m.index).split("\n").length;
5952
+ const lineNum = lineNumberAt(content, m.index);
5873
5953
  findings.push({
5874
5954
  rule: "VC127",
5875
5955
  title: insecureHTTPMethods.title,
@@ -5903,7 +5983,7 @@ var httpRequestSmuggling = {
5903
5983
  let m;
5904
5984
  while ((m = pat.exec(content)) !== null) {
5905
5985
  if (isCommentLine(content, m.index)) continue;
5906
- const lineNum = content.substring(0, m.index).split("\n").length;
5986
+ const lineNum = lineNumberAt(content, m.index);
5907
5987
  findings.push({
5908
5988
  rule: "VC128",
5909
5989
  title: httpRequestSmuggling.title,
@@ -5937,7 +6017,7 @@ var unencryptedPII = {
5937
6017
  let m;
5938
6018
  while ((m = pat.exec(content)) !== null) {
5939
6019
  if (isCommentLine(content, m.index)) continue;
5940
- const lineNum = content.substring(0, m.index).split("\n").length;
6020
+ const lineNum = lineNumberAt(content, m.index);
5941
6021
  findings.push({
5942
6022
  rule: "VC129",
5943
6023
  title: unencryptedPII.title,
@@ -5969,7 +6049,7 @@ var missingAuthRateLimit = {
5969
6049
  if (isCommentLine(content, m.index)) continue;
5970
6050
  const surrounding = content.substring(Math.max(0, m.index - 300), Math.min(content.length, m.index + 300));
5971
6051
  if (/rateLimit|rateLimiter|throttle|slowDown|express-rate-limit|rate_limit|RateLimiter|limiter/i.test(surrounding)) continue;
5972
- const lineNum = content.substring(0, m.index).split("\n").length;
6052
+ const lineNum = lineNumberAt(content, m.index);
5973
6053
  findings.push({
5974
6054
  rule: "VC130",
5975
6055
  title: missingAuthRateLimit.title,
@@ -6007,7 +6087,7 @@ var vulnerableDependencies = {
6007
6087
  for (const [pat, message] of vulnerablePackages) {
6008
6088
  let m;
6009
6089
  while ((m = pat.exec(content)) !== null) {
6010
- const lineNum = content.substring(0, m.index).split("\n").length;
6090
+ const lineNum = lineNumberAt(content, m.index);
6011
6091
  findings.push({
6012
6092
  rule: "VC131",
6013
6093
  title: vulnerableDependencies.title,
@@ -6039,7 +6119,7 @@ function secretRuleCheck(content, filePath, pattern, ruleId, title, severity, fi
6039
6119
  const lineStart = content.lastIndexOf("\n", m.index - 1) + 1;
6040
6120
  const lineText = content.substring(lineStart, content.indexOf("\n", m.index));
6041
6121
  if (PLACEHOLDER_RE.test(lineText)) continue;
6042
- const lineNum = content.substring(0, m.index).split("\n").length;
6122
+ const lineNum = lineNumberAt(content, m.index);
6043
6123
  findings.push({
6044
6124
  rule: ruleId,
6045
6125
  title,
@@ -6140,7 +6220,7 @@ var hardcodedGCPServiceAccount = {
6140
6220
  const findings = [];
6141
6221
  const m = content.match(/"type"\s*:\s*"service_account"/);
6142
6222
  if (m && m.index !== void 0) {
6143
- const lineNum = content.substring(0, m.index).split("\n").length;
6223
+ const lineNum = lineNumberAt(content, m.index);
6144
6224
  findings.push({
6145
6225
  rule: "VC136",
6146
6226
  title: this.title,
@@ -6246,7 +6326,7 @@ var hardcodedDatadogKey = {
6246
6326
  const lineStart = content.lastIndexOf("\n", m.index - 1) + 1;
6247
6327
  const lineText = content.substring(lineStart, content.indexOf("\n", m.index));
6248
6328
  if (PLACEHOLDER_RE.test(lineText)) continue;
6249
- const lineNum = content.substring(0, m.index).split("\n").length;
6329
+ const lineNum = lineNumberAt(content, m.index);
6250
6330
  findings.push({
6251
6331
  rule: "VC141",
6252
6332
  title: this.title,
@@ -6280,7 +6360,7 @@ var hardcodedVercelToken = {
6280
6360
  const lineStart = content.lastIndexOf("\n", m.index - 1) + 1;
6281
6361
  const lineText = content.substring(lineStart, content.indexOf("\n", m.index));
6282
6362
  if (PLACEHOLDER_RE.test(lineText)) continue;
6283
- const lineNum = content.substring(0, m.index).split("\n").length;
6363
+ const lineNum = lineNumberAt(content, m.index);
6284
6364
  findings.push({
6285
6365
  rule: "VC142",
6286
6366
  title: this.title,
@@ -6314,7 +6394,7 @@ var hardcodedSupabaseServiceRole = {
6314
6394
  const lineStart = content.lastIndexOf("\n", m.index - 1) + 1;
6315
6395
  const lineText = content.substring(lineStart, content.indexOf("\n", m.index));
6316
6396
  if (PLACEHOLDER_RE.test(lineText)) continue;
6317
- const lineNum = content.substring(0, m.index).split("\n").length;
6397
+ const lineNum = lineNumberAt(content, m.index);
6318
6398
  findings.push({
6319
6399
  rule: "VC143",
6320
6400
  title: this.title,
@@ -6378,7 +6458,7 @@ function contextSecretRuleCheck(content, filePath, pattern, ruleId, title, sever
6378
6458
  const lineStart = content.lastIndexOf("\n", m.index - 1) + 1;
6379
6459
  const lineText = content.substring(lineStart, content.indexOf("\n", m.index));
6380
6460
  if (PLACEHOLDER_RE.test(lineText)) continue;
6381
- const lineNum = content.substring(0, m.index).split("\n").length;
6461
+ const lineNum = lineNumberAt(content, m.index);
6382
6462
  findings.push({
6383
6463
  rule: ruleId,
6384
6464
  title,
@@ -6858,7 +6938,7 @@ var ghaPullRequestTargetCheckout = {
6858
6938
  const checkoutPattern = /uses\s*:\s*actions\/checkout@[^\n]*[\s\S]{0,400}?ref\s*:\s*\$\{\{\s*github\.event\.pull_request\.head\.(?:ref|sha)\s*\}\}/g;
6859
6939
  let m;
6860
6940
  while ((m = checkoutPattern.exec(content)) !== null) {
6861
- const lineNum = content.substring(0, m.index).split("\n").length;
6941
+ const lineNum = lineNumberAt(content, m.index);
6862
6942
  findings.push({
6863
6943
  rule: "VC184",
6864
6944
  title: ghaPullRequestTargetCheckout.title,
@@ -6942,7 +7022,7 @@ var ghaThirdPartyActionWithSecrets = {
6942
7022
  const pattern = /uses\s*:\s*(?!actions\/|github\/|aws-actions\/|azure\/|google-github-actions\/|hashicorp\/)([\w\-]+\/[\w\-./]+)@[^\n]*\n[\s\S]{0,800}?with\s*:[\s\S]{0,800}?\$\{\{\s*secrets\./g;
6943
7023
  let m;
6944
7024
  while ((m = pattern.exec(content)) !== null) {
6945
- const lineNum = content.substring(0, m.index).split("\n").length;
7025
+ const lineNum = lineNumberAt(content, m.index);
6946
7026
  findings.push({
6947
7027
  rule: "VC187",
6948
7028
  title: ghaThirdPartyActionWithSecrets.title,
@@ -7107,7 +7187,7 @@ var pyRequestsVerifyFalse = {
7107
7187
  let m;
7108
7188
  while ((m = pattern.exec(content)) !== null) {
7109
7189
  if (isCommentLine(content, m.index)) continue;
7110
- const lineNum = content.substring(0, m.index).split("\n").length;
7190
+ const lineNum = lineNumberAt(content, m.index);
7111
7191
  findings.push({
7112
7192
  rule: "VC191",
7113
7193
  title: pyRequestsVerifyFalse.title,
@@ -7138,7 +7218,7 @@ var pyJinja2AutoescapeOff = {
7138
7218
  let m;
7139
7219
  while ((m = pattern.exec(content)) !== null) {
7140
7220
  if (isCommentLine(content, m.index)) continue;
7141
- const lineNum = content.substring(0, m.index).split("\n").length;
7221
+ const lineNum = lineNumberAt(content, m.index);
7142
7222
  findings.push({
7143
7223
  rule: "VC192",
7144
7224
  title: pyJinja2AutoescapeOff.title,
@@ -7167,7 +7247,7 @@ var pyTempfileMktemp = {
7167
7247
  let m;
7168
7248
  while ((m = pattern.exec(content)) !== null) {
7169
7249
  if (isCommentLine(content, m.index)) continue;
7170
- const lineNum = content.substring(0, m.index).split("\n").length;
7250
+ const lineNum = lineNumberAt(content, m.index);
7171
7251
  findings.push({
7172
7252
  rule: "VC193",
7173
7253
  title: pyTempfileMktemp.title,
@@ -7209,7 +7289,7 @@ var pyDjangoMarkSafe = {
7209
7289
  let m;
7210
7290
  while ((m = pattern.exec(content)) !== null) {
7211
7291
  if (isCommentLine(content, m.index)) continue;
7212
- const lineNum = content.substring(0, m.index).split("\n").length;
7292
+ const lineNum = lineNumberAt(content, m.index);
7213
7293
  if (seenLines.has(lineNum)) continue;
7214
7294
  seenLines.add(lineNum);
7215
7295
  findings.push({
@@ -7246,7 +7326,7 @@ var pyParamikoAutoAdd = {
7246
7326
  let m;
7247
7327
  while ((m = pattern.exec(content)) !== null) {
7248
7328
  if (isCommentLine(content, m.index)) continue;
7249
- const lineNum = content.substring(0, m.index).split("\n").length;
7329
+ const lineNum = lineNumberAt(content, m.index);
7250
7330
  if (seenLines.has(lineNum)) continue;
7251
7331
  seenLines.add(lineNum);
7252
7332
  findings.push({
@@ -7279,7 +7359,7 @@ var pyDjangoAllowedHostsWildcard = {
7279
7359
  let m;
7280
7360
  while ((m = pattern.exec(content)) !== null) {
7281
7361
  if (isCommentLine(content, m.index)) continue;
7282
- const lineNum = content.substring(0, m.index).split("\n").length;
7362
+ const lineNum = lineNumberAt(content, m.index);
7283
7363
  findings.push({
7284
7364
  rule: "VC196",
7285
7365
  title: pyDjangoAllowedHostsWildcard.title,
@@ -7317,7 +7397,7 @@ var pyJWTDecodeWeakConfig = {
7317
7397
  if (isCommentLine(content, m.index)) continue;
7318
7398
  const args = m[1];
7319
7399
  if (/\balgorithms\s*=/.test(args)) continue;
7320
- const lineNum = content.substring(0, m.index).split("\n").length;
7400
+ const lineNum = lineNumberAt(content, m.index);
7321
7401
  if (seenLines.has(lineNum)) continue;
7322
7402
  seenLines.add(lineNum);
7323
7403
  findings.push({
@@ -7368,7 +7448,7 @@ var llmPromptInjection = {
7368
7448
  let m;
7369
7449
  while ((m = pattern.exec(content)) !== null) {
7370
7450
  if (isCommentLine(content, m.index)) continue;
7371
- const lineNum = content.substring(0, m.index).split("\n").length;
7451
+ const lineNum = lineNumberAt(content, m.index);
7372
7452
  if (seenLines.has(lineNum)) continue;
7373
7453
  seenLines.add(lineNum);
7374
7454
  findings.push({
@@ -7407,7 +7487,7 @@ var llmSystemPromptInjection = {
7407
7487
  let m;
7408
7488
  while ((m = pattern.exec(content)) !== null) {
7409
7489
  if (isCommentLine(content, m.index)) continue;
7410
- const lineNum = content.substring(0, m.index).split("\n").length;
7490
+ const lineNum = lineNumberAt(content, m.index);
7411
7491
  findings.push({
7412
7492
  rule: "VC199",
7413
7493
  title: llmSystemPromptInjection.title,
@@ -7449,7 +7529,7 @@ var llmOutputAsHTML = {
7449
7529
  let m;
7450
7530
  while ((m = pattern.exec(content)) !== null) {
7451
7531
  if (isCommentLine(content, m.index)) continue;
7452
- const lineNum = content.substring(0, m.index).split("\n").length;
7532
+ const lineNum = lineNumberAt(content, m.index);
7453
7533
  if (seenLines.has(lineNum)) continue;
7454
7534
  seenLines.add(lineNum);
7455
7535
  findings.push({
@@ -7486,7 +7566,7 @@ var vectorStoreQueryNoUserFilter = {
7486
7566
  if (/\b(?:user[_-]?id|userId|tenant[_-]?id|tenantId|org[_-]?id|orgId|owner[_-]?id|ownerId|account[_-]?id|customer[_-]?id|workspace[_-]?id)\b/i.test(args)) continue;
7487
7567
  if (/\bnamespace\s*[:=]\s*[`"']?[^,)`"']*(?:user|tenant|org|owner|account|customer|workspace)/i.test(args)) continue;
7488
7568
  if (/\bfilter\s*[:=][\s\S]*?(?:\buser|\btenant|\borg|\bowner|\baccount|\bcustomer|\bworkspace)/i.test(args)) continue;
7489
- const lineNum = content.substring(0, m.index).split("\n").length;
7569
+ const lineNum = lineNumberAt(content, m.index);
7490
7570
  findings.push({
7491
7571
  rule: "VC201",
7492
7572
  title: vectorStoreQueryNoUserFilter.title,
@@ -7523,7 +7603,7 @@ var vectorStoreUpsertNoMetadata = {
7523
7603
  if (/\bnamespace\s*[:=]\s*[`"']?[^,)`"']*(?:user|tenant|org)/i.test(args)) {
7524
7604
  continue;
7525
7605
  }
7526
- const lineNum = content.substring(0, m.index).split("\n").length;
7606
+ const lineNum = lineNumberAt(content, m.index);
7527
7607
  findings.push({
7528
7608
  rule: "VC202",
7529
7609
  title: vectorStoreUpsertNoMetadata.title,
@@ -7577,7 +7657,7 @@ var llmCallNoMaxTokens = {
7577
7657
  if (depth !== 0) continue;
7578
7658
  const args = content.substring(openIdx + 1, i).replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/[^\n]*/g, "").replace(/#[^\n]*/g, "");
7579
7659
  if (TOKEN_KW_RE.test(args)) continue;
7580
- const lineNum = content.substring(0, m.index).split("\n").length;
7660
+ const lineNum = lineNumberAt(content, m.index);
7581
7661
  findings.push({
7582
7662
  rule: "VC203",
7583
7663
  title: llmCallNoMaxTokens.title,
@@ -7617,7 +7697,7 @@ var graphqlNoDepthLimit = {
7617
7697
  let m;
7618
7698
  while ((m = anchorRe.exec(content)) !== null) {
7619
7699
  if (isCommentLine(content, m.index)) continue;
7620
- const lineNum = content.substring(0, m.index).split("\n").length;
7700
+ const lineNum = lineNumberAt(content, m.index);
7621
7701
  findings.push({
7622
7702
  rule: "VC204",
7623
7703
  title: graphqlNoDepthLimit.title,
@@ -7651,7 +7731,7 @@ var graphqlNoComplexityLimit = {
7651
7731
  let m;
7652
7732
  while ((m = anchorRe.exec(content)) !== null) {
7653
7733
  if (isCommentLine(content, m.index)) continue;
7654
- const lineNum = content.substring(0, m.index).split("\n").length;
7734
+ const lineNum = lineNumberAt(content, m.index);
7655
7735
  findings.push({
7656
7736
  rule: "VC205",
7657
7737
  title: graphqlNoComplexityLimit.title,
@@ -7681,7 +7761,7 @@ var graphqlCSRFDisabled = {
7681
7761
  let m;
7682
7762
  while ((m = pattern.exec(content)) !== null) {
7683
7763
  if (isCommentLine(content, m.index)) continue;
7684
- const lineNum = content.substring(0, m.index).split("\n").length;
7764
+ const lineNum = lineNumberAt(content, m.index);
7685
7765
  findings.push({
7686
7766
  rule: "VC206",
7687
7767
  title: graphqlCSRFDisabled.title,
@@ -7777,7 +7857,7 @@ var webhookMissingIdempotency = {
7777
7857
  if (/idempoten|event\.id|evt\.id|delivery_?id|alreadyProcessed|processedEvents|ON\s+CONFLICT|INSERT\s+OR\s+IGNORE|\bseen\b|\bprocessed\b/i.test(content)) return [];
7778
7858
  const m = content.match(/constructEvent|new\s+Webhook\s*\(|verifyHeader/i);
7779
7859
  if (!m || m.index === void 0) return [];
7780
- const line = content.substring(0, m.index).split("\n").length;
7860
+ const line = lineNumberAt(content, m.index);
7781
7861
  return filterSilenced([{
7782
7862
  rule: "VC209",
7783
7863
  title: webhookMissingIdempotency.title,
@@ -7804,7 +7884,7 @@ var middlewareMatcherExcludesApi = {
7804
7884
  if (!/\(\?!\s*[^)]*\bapi\b/.test(content)) return [];
7805
7885
  const m = content.match(/matcher\s*:/);
7806
7886
  if (!m || m.index === void 0) return [];
7807
- const line = content.substring(0, m.index).split("\n").length;
7887
+ const line = lineNumberAt(content, m.index);
7808
7888
  return filterSilenced([{
7809
7889
  rule: "VC210",
7810
7890
  title: middlewareMatcherExcludesApi.title,
@@ -7817,6 +7897,113 @@ var middlewareMatcherExcludesApi = {
7817
7897
  }], content, "VC210");
7818
7898
  }
7819
7899
  };
7900
+ function passesLuhn(digits) {
7901
+ let sum = 0;
7902
+ let alt = false;
7903
+ for (let i = digits.length - 1; i >= 0; i--) {
7904
+ let d = digits.charCodeAt(i) - 48;
7905
+ if (d < 0 || d > 9) return false;
7906
+ if (alt) {
7907
+ d *= 2;
7908
+ if (d > 9) d -= 9;
7909
+ }
7910
+ sum += d;
7911
+ alt = !alt;
7912
+ }
7913
+ return sum % 10 === 0;
7914
+ }
7915
+ function looksLikeIssuerCard(d) {
7916
+ if (/^4/.test(d) && (d.length === 13 || d.length === 16 || d.length === 19)) return true;
7917
+ if (/^5[1-5]/.test(d) && d.length === 16) return true;
7918
+ if (/^2(2[2-9]|[3-6]\d|7[01]|720)/.test(d) && d.length === 16) return true;
7919
+ if (/^3[47]/.test(d) && d.length === 15) return true;
7920
+ if (/^(6011|65|64[4-9])/.test(d) && d.length === 16) return true;
7921
+ return false;
7922
+ }
7923
+ var TEST_CARD_NUMBERS = /* @__PURE__ */ new Set([
7924
+ "4242424242424242",
7925
+ "4111111111111111",
7926
+ "4012888888881881",
7927
+ "4000056655665556",
7928
+ "4000000000000002",
7929
+ "4000000000009995",
7930
+ "5555555555554444",
7931
+ "5200828282828210",
7932
+ "5105105105105100",
7933
+ "2223003122003222",
7934
+ "378282246310005",
7935
+ "371449635398431",
7936
+ "6011111111111117",
7937
+ "6011000990139424",
7938
+ "3056930009020004",
7939
+ "38520000023237"
7940
+ ]);
7941
+ var hardcodedCreditCard = {
7942
+ id: "VC211",
7943
+ title: "Hardcoded credit card number",
7944
+ severity: "high",
7945
+ category: "Information Leakage",
7946
+ description: "A literal credit card number (Luhn-valid, with a real issuer prefix, and not a known network test card) appears in source. Cardholder data does not belong in code \u2014 it lands in git history, logs, and backups, and is a PCI-DSS violation. Use your processor's published test cards for tests, and tokenize real cards.",
7947
+ check(content, filePath) {
7948
+ if (isTestFile(filePath)) return [];
7949
+ const findings = [];
7950
+ const pattern = /(?<![\d.])\d[\d -]{11,21}\d(?![\d.])/g;
7951
+ let m;
7952
+ while ((m = pattern.exec(content)) !== null) {
7953
+ const digits = m[0].replace(/[ -]/g, "");
7954
+ if (digits.length < 13 || digits.length > 19) continue;
7955
+ if (TEST_CARD_NUMBERS.has(digits)) continue;
7956
+ if (!looksLikeIssuerCard(digits)) continue;
7957
+ if (!passesLuhn(digits)) continue;
7958
+ if (isCommentLine(content, m.index)) continue;
7959
+ const lineNum = lineNumberAt(content, m.index);
7960
+ findings.push({
7961
+ rule: "VC211",
7962
+ title: hardcodedCreditCard.title,
7963
+ severity: "high",
7964
+ category: "Information Leakage",
7965
+ file: filePath,
7966
+ line: lineNum,
7967
+ snippet: getSnippet(content, lineNum),
7968
+ fix: "Never store a real card number in source. Tokenize through your payment processor (Stripe, Braintree) and keep only the returned token. For tests, use the processor's published test cards (e.g. Stripe's 4242 4242 4242 4242)."
7969
+ });
7970
+ }
7971
+ return findings;
7972
+ }
7973
+ };
7974
+ var hardcodedSSN = {
7975
+ id: "VC212",
7976
+ title: "Hardcoded US Social Security Number",
7977
+ severity: "high",
7978
+ category: "Information Leakage",
7979
+ description: "A value in SSN format (NNN-NN-NNNN) is assigned to an SSN-named field in source. Social Security Numbers are regulated PII; a literal one leaks into git history, logs, and backups. Store SSNs encrypted at rest and load test data from generated fakes, never a code literal.",
7980
+ check(content, filePath) {
7981
+ if (isTestFile(filePath)) return [];
7982
+ const findings = [];
7983
+ const pattern = /(?:ssn|social[_-]?sec(?:urity)?(?:[_-]?(?:no|num|number))?|taxpayer[_-]?id)\b["']?\s*[:=]\s*["']?(\d{3}-\d{2}-\d{4})/gi;
7984
+ let m;
7985
+ while ((m = pattern.exec(content)) !== null) {
7986
+ const ssn = m[1];
7987
+ const [area, group, serial] = ssn.split("-");
7988
+ if (area === "000" || area === "666" || Number(area) >= 900) continue;
7989
+ if (group === "00" || serial === "0000") continue;
7990
+ if (ssn === "123-45-6789") continue;
7991
+ if (isCommentLine(content, m.index)) continue;
7992
+ const lineNum = lineNumberAt(content, m.index);
7993
+ findings.push({
7994
+ rule: "VC212",
7995
+ title: hardcodedSSN.title,
7996
+ severity: "high",
7997
+ category: "Information Leakage",
7998
+ file: filePath,
7999
+ line: lineNum,
8000
+ snippet: getSnippet(content, lineNum),
8001
+ fix: "Don't hardcode SSNs. Generate fake values for tests (a faker/factory), and store real SSNs with application-level encryption or in a secrets vault \u2014 never as a code literal."
8002
+ });
8003
+ }
8004
+ return findings;
8005
+ }
8006
+ };
7820
8007
  var secretInURLParam = {
7821
8008
  id: "VC146",
7822
8009
  title: "Secret Passed in URL Query Parameter",
@@ -7842,7 +8029,7 @@ var secretInURLParam = {
7842
8029
  if (isCommentLine(content, m.index)) continue;
7843
8030
  if (isInsideFixMessage(content, m.index)) continue;
7844
8031
  if (isInlineSilenced(content, m.index, "VC146")) continue;
7845
- const lineNum = content.substring(0, m.index).split("\n").length;
8032
+ const lineNum = lineNumberAt(content, m.index);
7846
8033
  findings.push({
7847
8034
  rule: "VC146",
7848
8035
  title: this.title,
@@ -7873,7 +8060,7 @@ var secretLoggedToConsole = {
7873
8060
  while ((m = pattern.exec(content)) !== null) {
7874
8061
  if (isCommentLine(content, m.index)) continue;
7875
8062
  if (isInsideFixMessage(content, m.index)) continue;
7876
- const lineNum = content.substring(0, m.index).split("\n").length;
8063
+ const lineNum = lineNumberAt(content, m.index);
7877
8064
  findings.push({
7878
8065
  rule: "VC147",
7879
8066
  title: this.title,
@@ -7903,7 +8090,7 @@ var secretInErrorResponse = {
7903
8090
  while ((m = pattern.exec(content)) !== null) {
7904
8091
  if (isCommentLine(content, m.index)) continue;
7905
8092
  if (isInsideFixMessage(content, m.index)) continue;
7906
- const lineNum = content.substring(0, m.index).split("\n").length;
8093
+ const lineNum = lineNumberAt(content, m.index);
7907
8094
  findings.push({
7908
8095
  rule: "VC148",
7909
8096
  title: this.title,
@@ -7942,7 +8129,7 @@ var secretInBundleConfig = {
7942
8129
  while ((m = re.exec(content)) !== null) {
7943
8130
  if (isCommentLine(content, m.index)) continue;
7944
8131
  if (isInsideFixMessage(content, m.index)) continue;
7945
- const lineNum = content.substring(0, m.index).split("\n").length;
8132
+ const lineNum = lineNumberAt(content, m.index);
7946
8133
  findings.push({
7947
8134
  rule: "VC149",
7948
8135
  title: this.title,
@@ -7980,7 +8167,7 @@ var secretInHTMLAttribute = {
7980
8167
  while ((m = re.exec(content)) !== null) {
7981
8168
  if (isCommentLine(content, m.index)) continue;
7982
8169
  if (isInsideFixMessage(content, m.index)) continue;
7983
- const lineNum = content.substring(0, m.index).split("\n").length;
8170
+ const lineNum = lineNumberAt(content, m.index);
7984
8171
  findings.push({
7985
8172
  rule: "VC150",
7986
8173
  title: this.title,
@@ -8007,9 +8194,9 @@ var secretInCLIArgument = {
8007
8194
  if (!filePath.match(/\.(js|ts|jsx|tsx|py|rb)$/)) return [];
8008
8195
  const findings = [];
8009
8196
  const patterns = [
8010
- /(?:exec|execSync|spawn|spawnSync|child_process)\s*\([^)]*\$\{[^}]*(?:api[_-]?key|secret|token|password|credentials)/gi,
8011
- /(?:exec|execSync|spawn|spawnSync|child_process)\s*\([^)]*["']\s*\+\s*(?:api[_-]?key|secret|token|password|credentials)/gi,
8012
- /(?:subprocess|os\.system|os\.popen)\s*\([^)]*\{[^}]*(?:api[_-]?key|secret|token|password|credentials)/gi
8197
+ /(?:exec|execSync|spawn|spawnSync|child_process)\s*\([^)]{0,500}\$\{[^}]{0,200}(?:api[_-]?key|secret|token|password|credentials)/gi,
8198
+ /(?:exec|execSync|spawn|spawnSync|child_process)\s*\([^)]{0,500}["']\s*\+\s*(?:api[_-]?key|secret|token|password|credentials)/gi,
8199
+ /(?:subprocess|os\.system|os\.popen)\s*\([^)]{0,500}\{[^}]{0,200}(?:api[_-]?key|secret|token|password|credentials)/gi
8013
8200
  ];
8014
8201
  for (const p of patterns) {
8015
8202
  let m;
@@ -8017,7 +8204,7 @@ var secretInCLIArgument = {
8017
8204
  while ((m = re.exec(content)) !== null) {
8018
8205
  if (isCommentLine(content, m.index)) continue;
8019
8206
  if (isInsideFixMessage(content, m.index)) continue;
8020
- const lineNum = content.substring(0, m.index).split("\n").length;
8207
+ const lineNum = lineNumberAt(content, m.index);
8021
8208
  findings.push({
8022
8209
  rule: "VC151",
8023
8210
  title: this.title,
@@ -8063,7 +8250,7 @@ var webhookSignatureVerification = {
8063
8250
  if (svc.verify.test(content)) continue;
8064
8251
  const m = content.match(/export\s+(?:async\s+)?function\s+POST/);
8065
8252
  if (!m || m.index === void 0) continue;
8066
- const lineNum = content.substring(0, m.index).split("\n").length;
8253
+ const lineNum = lineNumberAt(content, m.index);
8067
8254
  findings.push({
8068
8255
  rule: "VC152",
8069
8256
  title: `${svc.name} Webhook Missing Signature Verification`,
@@ -8130,7 +8317,7 @@ var missingRequestValidation = {
8130
8317
  if (/if\s*\(\s*!(?:body|parsed|data|payload)\.|throw.*(?:missing|invalid|required)/i.test(content)) return [];
8131
8318
  const m = content.match(/export\s+(?:async\s+)?function\s+(?:POST|PUT|PATCH)/);
8132
8319
  if (!m || m.index === void 0) return [];
8133
- const lineNum = content.substring(0, m.index).split("\n").length;
8320
+ const lineNum = lineNumberAt(content, m.index);
8134
8321
  return [{
8135
8322
  rule: "VC154",
8136
8323
  title: this.title,
@@ -8157,7 +8344,7 @@ var missingAIRateLimit = {
8157
8344
  if (/rateLimit|rateLimiter|throttle|checkRateLimit|limiter|slowDown|express-rate-limit/i.test(content)) return [];
8158
8345
  const m = content.match(/export\s+(?:async\s+)?function\s+(?:POST|GET)/i) || content.match(/\.(post|get)\s*\(/i);
8159
8346
  if (!m || m.index === void 0) return [];
8160
- const lineNum = content.substring(0, m.index).split("\n").length;
8347
+ const lineNum = lineNumberAt(content, m.index);
8161
8348
  return [{
8162
8349
  rule: "VC155",
8163
8350
  title: this.title,
@@ -8186,7 +8373,7 @@ var missingPagination = {
8186
8373
  if (/findUnique|findFirst|findById|\.findOne|WHERE.*id\s*=/i.test(content)) return [];
8187
8374
  const m = content.match(/export\s+(?:async\s+)?function\s+GET/);
8188
8375
  if (!m || m.index === void 0) return [];
8189
- const lineNum = content.substring(0, m.index).split("\n").length;
8376
+ const lineNum = lineNumberAt(content, m.index);
8190
8377
  return [{
8191
8378
  rule: "VC156",
8192
8379
  title: this.title,
@@ -8247,7 +8434,7 @@ var insecureDirectObjectReference = {
8247
8434
  if (/requireUser|requireUserForApi|getServerSession|auth\(\)/i.test(content)) return [];
8248
8435
  const m = content.match(/params\.id|params\.(?:slug|uuid)|req\.params\./);
8249
8436
  if (!m || m.index === void 0) return [];
8250
- const lineNum = content.substring(0, m.index).split("\n").length;
8437
+ const lineNum = lineNumberAt(content, m.index);
8251
8438
  return [{
8252
8439
  rule: "VC158",
8253
8440
  title: this.title,
@@ -8545,7 +8732,10 @@ var allCustomRules = [
8545
8732
  // VC204–VC206: GraphQL server hardening
8546
8733
  graphqlNoDepthLimit,
8547
8734
  graphqlNoComplexityLimit,
8548
- graphqlCSRFDisabled
8735
+ graphqlCSRFDisabled,
8736
+ // VC211–VC212: hardcoded sensitive personal data (PII)
8737
+ hardcodedCreditCard,
8738
+ hardcodedSSN
8549
8739
  ];
8550
8740
  function runCustomRules(content, filePath, disabledRules = [], tier = "free", extraRules = []) {
8551
8741
  const findings = [];
@@ -8553,6 +8743,17 @@ function runCustomRules(content, filePath, disabledRules = [], tier = "free", ex
8553
8743
  return findings;
8554
8744
  }
8555
8745
  if (/pro-rules-bundle|\.bundle\./i.test(filePath)) return findings;
8746
+ let maxLineLen = 0;
8747
+ {
8748
+ let lineStart = 0;
8749
+ for (let i = 0; i <= content.length; i++) {
8750
+ if (i === content.length || content.charCodeAt(i) === 10) {
8751
+ if (i - lineStart > maxLineLen) maxLineLen = i - lineStart;
8752
+ lineStart = i + 1;
8753
+ }
8754
+ }
8755
+ }
8756
+ if (maxLineLen > 5e4) return findings;
8556
8757
  if (/onboarding|demo-data|example-vulnerable|code-sample/i.test(filePath)) return findings;
8557
8758
  const ruleset = tier === "pro" && extraRules.length > 0 ? [...freeRules, ...extraRules] : freeRules;
8558
8759
  for (const rule of ruleset) {
@@ -8963,6 +9164,7 @@ function scanEntropy(files) {
8963
9164
  hardcodedAnthropicKey,
8964
9165
  hardcodedCloudflareToken,
8965
9166
  hardcodedCohereKey,
9167
+ hardcodedCreditCard,
8966
9168
  hardcodedDatadogKey,
8967
9169
  hardcodedDiscordToken,
8968
9170
  hardcodedEncryptionKey,
@@ -8992,6 +9194,7 @@ function scanEntropy(files) {
8992
9194
  hardcodedRailwayToken,
8993
9195
  hardcodedReplicateKey,
8994
9196
  hardcodedResendKey,
9197
+ hardcodedSSN,
8995
9198
  hardcodedSecrets,
8996
9199
  hardcodedSendGridKey,
8997
9200
  hardcodedSentryAuthToken,