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.js CHANGED
@@ -1,13 +1,48 @@
1
1
  // src/snippet.ts
2
- function getSnippet(content, line, contextLines = 2) {
2
+ var cacheContent = null;
3
+ var cacheLines = [];
4
+ var cacheOffsets = [];
5
+ function lineIndex(content) {
6
+ if (content === cacheContent) return { lines: cacheLines, offsets: cacheOffsets };
3
7
  const lines = content.split("\n");
8
+ const offsets = new Array(lines.length);
9
+ let off = 0;
10
+ for (let i = 0; i < lines.length; i++) {
11
+ offsets[i] = off;
12
+ off += lines[i].length + 1;
13
+ }
14
+ cacheContent = content;
15
+ cacheLines = lines;
16
+ cacheOffsets = offsets;
17
+ return { lines, offsets };
18
+ }
19
+ function lineNumberAt(content, index) {
20
+ const { offsets } = lineIndex(content);
21
+ let lo = 0;
22
+ let hi = offsets.length - 1;
23
+ let ans = 0;
24
+ while (lo <= hi) {
25
+ const mid = lo + hi >> 1;
26
+ if (offsets[mid] <= index) {
27
+ ans = mid;
28
+ lo = mid + 1;
29
+ } else {
30
+ hi = mid - 1;
31
+ }
32
+ }
33
+ return ans + 1;
34
+ }
35
+ function getSnippet(content, line, contextLines = 2) {
36
+ const { lines } = lineIndex(content);
4
37
  const start = Math.max(0, line - 1 - contextLines);
5
38
  const end = Math.min(lines.length, line + contextLines);
6
- return lines.slice(start, end).map((l, i) => {
7
- const lineNum = start + i + 1;
39
+ const out = [];
40
+ for (let i = start; i < end; i++) {
41
+ const lineNum = i + 1;
8
42
  const marker = lineNum === line ? ">" : " ";
9
- return `${marker} ${lineNum.toString().padStart(4)} | ${l}`;
10
- }).join("\n");
43
+ out.push(`${marker} ${lineNum.toString().padStart(4)} | ${lines[i]}`);
44
+ }
45
+ return out.join("\n");
11
46
  }
12
47
 
13
48
  // src/rule-impacts.ts
@@ -221,7 +256,9 @@ var RULE_IMPACTS = {
221
256
  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.",
222
257
  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.",
223
258
  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.",
224
- 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."
259
+ 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.",
260
+ 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.",
261
+ 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."
225
262
  };
226
263
 
227
264
  // src/exposure.ts
@@ -815,7 +852,7 @@ var SERVER_SIDE_PATH_RE = new RegExp(
815
852
  ].join("|"),
816
853
  "i"
817
854
  );
818
- 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/;
855
+ 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/;
819
856
  function isServerSideFile(filePath, content) {
820
857
  if (isTestFile(filePath)) return false;
821
858
  if (SERVER_SIDE_PATH_RE.test(filePath)) return true;
@@ -862,13 +899,23 @@ function filterSilenced(matches, content, ruleId) {
862
899
  }
863
900
  function findMatches(content, pattern, rule, filePath, fixTemplate) {
864
901
  const matches = [];
865
- const lines = content.split("\n");
866
902
  let m;
867
903
  const re = new RegExp(pattern.source, pattern.flags.includes("g") ? pattern.flags : `${pattern.flags}g`);
904
+ let scanned = 0;
905
+ let lineNum = 1;
906
+ let lastMatchIndex = -1;
907
+ const MAX_MATCHES = 500;
868
908
  while ((m = re.exec(content)) !== null) {
909
+ if (m.index === lastMatchIndex && m[0].length === 0) {
910
+ re.lastIndex++;
911
+ continue;
912
+ }
913
+ lastMatchIndex = m.index;
869
914
  if (isCommentLine(content, m.index)) continue;
870
915
  if (isInsideFixMessage(content, m.index)) continue;
871
- const lineNum = content.substring(0, m.index).split("\n").length;
916
+ for (; scanned < m.index; scanned++) {
917
+ if (content.charCodeAt(scanned) === 10) lineNum++;
918
+ }
872
919
  matches.push({
873
920
  rule: rule.id,
874
921
  title: rule.title,
@@ -879,6 +926,7 @@ function findMatches(content, pattern, rule, filePath, fixTemplate) {
879
926
  snippet: getSnippet(content, lineNum),
880
927
  fix: fixTemplate?.(m)
881
928
  });
929
+ if (matches.length >= MAX_MATCHES) break;
882
930
  }
883
931
  return matches;
884
932
  }
@@ -894,10 +942,26 @@ function astMatch(content, filePath, line, rule, fix) {
894
942
  fix
895
943
  };
896
944
  }
945
+ var parseCacheContent = null;
946
+ var parseCachePath = null;
947
+ var parseCacheResult = null;
948
+ var parseCacheValid = false;
897
949
  function tryParse(content, filePath) {
898
- const parsed = parseFile(content, filePath);
899
- if (!parsed) return null;
900
- return { parsed, taint: buildTaintMap(parsed) };
950
+ if (parseCacheValid && content === parseCacheContent && filePath === parseCachePath) {
951
+ return parseCacheResult;
952
+ }
953
+ let result = null;
954
+ try {
955
+ const parsed = parseFile(content, filePath);
956
+ result = parsed ? { parsed, taint: buildTaintMap(parsed) } : null;
957
+ } catch {
958
+ result = null;
959
+ }
960
+ parseCacheResult = result;
961
+ parseCacheContent = content;
962
+ parseCachePath = filePath;
963
+ parseCacheValid = true;
964
+ return result;
901
965
  }
902
966
  var hardcodedSecrets = {
903
967
  id: "VC001",
@@ -1265,7 +1329,7 @@ var xssVulnerability = {
1265
1329
  const hasSanitizerBypass = /bypassSecurityTrust(?:Html|Script|Style|Url|ResourceUrl)\s*\(/.test(content);
1266
1330
  if (/(?:sanitize|purify|escape|xss)/i.test(filePath)) return [];
1267
1331
  if (!hasSanitizerBypass && /DOMPurify\.sanitize|sanitizeHtml|xss\(|escapeHtml/i.test(content)) return [];
1268
- if (!hasSanitizerBypass && /(?:import|require)\s*\(?.*(?:DOMPurify|dompurify|sanitize|sanitizer)/i.test(content)) return [];
1332
+ if (!hasSanitizerBypass && /(?:import|require)\s*\(?[^\n]{0,200}(?:DOMPurify|dompurify|sanitize|sanitizer)/i.test(content)) return [];
1269
1333
  const patterns = [
1270
1334
  // React dangerouslySetInnerHTML
1271
1335
  /dangerouslySetInnerHTML\s*=\s*\{\s*\{\s*__html\s*:/g,
@@ -1294,9 +1358,9 @@ var xssVulnerability = {
1294
1358
  () => "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."
1295
1359
  );
1296
1360
  for (const m of raw) {
1297
- const lineText = allLines[m.line - 1] || "";
1361
+ const lineText = (allLines[m.line - 1] || "").slice(0, 4e3);
1298
1362
  if (/\.innerHTML\s*=\s*['"]/.test(lineText) && !/\$\{/.test(lineText)) continue;
1299
- if (/\.innerHTML\s*=\s*['"][^'"]*['"]\s*$/.test(lineText)) continue;
1363
+ if (/\.innerHTML\s*=\s*['"][^'"]{0,2000}['"]\s*$/.test(lineText)) continue;
1300
1364
  if (/dangerouslySetInnerHTML/.test(lineText)) {
1301
1365
  const windowText = allLines.slice(m.line - 1, m.line + 2).join("\n");
1302
1366
  if (/JSON\.stringify/i.test(windowText)) continue;
@@ -1547,7 +1611,7 @@ var evalUsage = {
1547
1611
  /new\s+Function\s*\(\s*(?!["'`])/g
1548
1612
  ];
1549
1613
  const hasEvalInString = /["'`]eval(?:-source-map|["'`])/i.test(content);
1550
- if (hasEvalInString && !/\beval\s*\([^)]*(?:req\.|body\.|input|params|user|data)/i.test(content)) return [];
1614
+ if (hasEvalInString && !/\beval\s*\([^)]{0,500}(?:req\.|body\.|input|params|user|data)/i.test(content)) return [];
1551
1615
  const matches = [];
1552
1616
  for (const p of patterns) {
1553
1617
  const rawMatches = findMatches(
@@ -1588,7 +1652,7 @@ var unvalidatedRedirect = {
1588
1652
  const isPureLiteral = /^["'`][^"'`]*["'`]$/.test(value);
1589
1653
  if (!hasInterpolation && isPureLiteral) continue;
1590
1654
  if (isInlineSilenced(content, m.index, "VC016")) continue;
1591
- const line = content.substring(0, m.index).split("\n").length;
1655
+ const line = lineNumberAt(content, m.index);
1592
1656
  matches.push({
1593
1657
  rule: "VC016",
1594
1658
  title: unvalidatedRedirect.title,
@@ -1811,11 +1875,11 @@ var prototypePollution = {
1811
1875
  ];
1812
1876
  const hasValidation = /schema|validate|sanitize|whitelist|allowedKeys|pick\(|Object\.freeze|zod|yup|joi|ajv/i.test(content);
1813
1877
  if (!hasValidation) {
1814
- const hasUnsafeMerge = /Object\.assign\s*\([^)]*JSON\.parse|\.\.\.JSON\.parse|\{.*\.\.\.(?:stored|saved|cached|parsed|data)/i.test(content);
1878
+ const hasUnsafeMerge = /Object\.assign\s*\([^)]{0,500}JSON\.parse|\.\.\.JSON\.parse|\{[^\n]{0,300}\.\.\.(?:stored|saved|cached|parsed|data)/i.test(content);
1815
1879
  if (hasUnsafeMerge) {
1816
1880
  matches.push(...findMatches(
1817
1881
  content,
1818
- /Object\.assign\s*\([^)]*JSON\.parse|\.\.\.JSON\.parse/g,
1882
+ /Object\.assign\s*\([^)]{0,500}JSON\.parse|\.\.\.JSON\.parse/g,
1819
1883
  prototypePollution,
1820
1884
  filePath,
1821
1885
  () => "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."
@@ -3361,7 +3425,7 @@ var dangerousInnerHTML = {
3361
3425
  if (/^[A-Z][A-Z0-9_]+$/.test(value)) continue;
3362
3426
  if (/^["'`][^$]*["'`]$/.test(value)) continue;
3363
3427
  if (/JSON\.stringify/i.test(value)) continue;
3364
- const lineNum = content.substring(0, m.index).split("\n").length;
3428
+ const lineNum = lineNumberAt(content, m.index);
3365
3429
  findings.push({
3366
3430
  rule: "VC063",
3367
3431
  title: dangerousInnerHTML.title,
@@ -4103,7 +4167,7 @@ var missingSRI = {
4103
4167
  const re = new RegExp(scriptPattern.source, scriptPattern.flags);
4104
4168
  while ((m = re.exec(content)) !== null) {
4105
4169
  if (!m[0].includes("integrity")) {
4106
- const lineNum = content.substring(0, m.index).split("\n").length;
4170
+ const lineNum = lineNumberAt(content, m.index);
4107
4171
  matches.push({
4108
4172
  rule: "VC084",
4109
4173
  title: missingSRI.title,
@@ -4735,8 +4799,13 @@ var complianceMap = {
4735
4799
  // VC209–VC210: advisory heuristics
4736
4800
  VC209: { owasp: "A04:2021", cwe: "CWE-799" },
4737
4801
  // webhook missing idempotency
4738
- VC210: { owasp: "A01:2021", cwe: "CWE-862" }
4802
+ VC210: { owasp: "A01:2021", cwe: "CWE-862" },
4739
4803
  // middleware matcher excludes /api
4804
+ // VC211–VC212: hardcoded sensitive personal data (PII) in source
4805
+ VC211: { owasp: "A02:2021", cwe: "CWE-540" },
4806
+ // hardcoded credit card number
4807
+ VC212: { owasp: "A02:2021", cwe: "CWE-540" }
4808
+ // hardcoded US SSN
4740
4809
  };
4741
4810
  var consoleLogProduction = {
4742
4811
  id: "VC097",
@@ -4974,7 +5043,7 @@ var s3BucketNoEncryption = {
4974
5043
  const blockEnd = Math.min(m.index + 2e3, content.length);
4975
5044
  const blockContent = content.substring(m.index, blockEnd);
4976
5045
  if (!/server_side_encryption/.test(blockContent)) {
4977
- const lineNum = content.substring(0, m.index).split("\n").length;
5046
+ const lineNum = lineNumberAt(content, m.index);
4978
5047
  matches.push({
4979
5048
  rule: "VC107",
4980
5049
  title: s3BucketNoEncryption.title,
@@ -5005,7 +5074,7 @@ var securityGroupAllInbound = {
5005
5074
  while ((m = ingressPattern.exec(content)) !== null) {
5006
5075
  if (isCommentLine(content, m.index)) continue;
5007
5076
  if (/from_port\s*=\s*0/.test(m[0]) || !/from_port/.test(m[0])) {
5008
- const lineNum = content.substring(0, m.index).split("\n").length;
5077
+ const lineNum = lineNumberAt(content, m.index);
5009
5078
  matches.push({
5010
5079
  rule: "VC108",
5011
5080
  title: securityGroupAllInbound.title,
@@ -5092,7 +5161,7 @@ var lambdaWithoutVPC = {
5092
5161
  const blockEnd = Math.min(m.index + 2e3, content.length);
5093
5162
  const blockContent = content.substring(m.index, blockEnd);
5094
5163
  if (!/vpc_config\s*\{/.test(blockContent)) {
5095
- const lineNum = content.substring(0, m.index).split("\n").length;
5164
+ const lineNum = lineNumberAt(content, m.index);
5096
5165
  matches.push({
5097
5166
  rule: "VC111",
5098
5167
  title: lambdaWithoutVPC.title,
@@ -5122,7 +5191,7 @@ var dockerLatestTag = {
5122
5191
  while ((m = fromPattern.exec(content)) !== null) {
5123
5192
  const image = m[1];
5124
5193
  if (image.endsWith(":latest") || !image.includes(":") && !image.startsWith("$")) {
5125
- const lineNum = content.substring(0, m.index).split("\n").length;
5194
+ const lineNum = lineNumberAt(content, m.index);
5126
5195
  matches.push({
5127
5196
  rule: "VC112",
5128
5197
  title: dockerLatestTag.title,
@@ -5250,17 +5319,26 @@ var pathTraversal = {
5250
5319
  const findings = [];
5251
5320
  const patterns = [
5252
5321
  /(?:readFile|readFileSync|createReadStream|writeFile|writeFileSync|appendFile|unlink|unlinkSync|access|stat|statSync|existsSync)\s*\(\s*(?:req\.|request\.|ctx\.|params\.|query\.)/gi,
5253
- /(?:path\.join|path\.resolve)\s*\([^)]*(?:req\.|request\.|ctx\.|params\.|query\.)[^)]*\)/gi,
5254
- /(?:res\.sendFile|res\.download)\s*\([^)]*(?:req\.|request\.)/gi,
5322
+ /(?:path\.join|path\.resolve)\s*\([^)]{0,500}(?:req\.|request\.|ctx\.|params\.|query\.)[^)]{0,500}\)/gi,
5323
+ /(?:res\.sendFile|res\.download)\s*\([^)]{0,500}(?:req\.|request\.)/gi,
5255
5324
  /open\s*\(\s*(?:request\.|params\[|args\.)/gi
5256
5325
  ];
5326
+ const MAX_MATCHES = 500;
5257
5327
  for (const pat of patterns) {
5328
+ if (findings.length >= MAX_MATCHES) break;
5258
5329
  let m;
5330
+ let lastIdx = -1;
5259
5331
  while ((m = pat.exec(content)) !== null) {
5332
+ if (findings.length >= MAX_MATCHES) break;
5333
+ if (m.index === lastIdx && m[0].length === 0) {
5334
+ pat.lastIndex++;
5335
+ continue;
5336
+ }
5337
+ lastIdx = m.index;
5260
5338
  if (isCommentLine(content, m.index)) continue;
5261
5339
  const surrounding = content.substring(Math.max(0, m.index - 200), m.index + 200);
5262
5340
  if (/path\.normalize|sanitize|\.replace\(\s*['"]\.\.|\.startsWith\s*\(|realpath/i.test(surrounding)) continue;
5263
- const lineNum = content.substring(0, m.index).split("\n").length;
5341
+ const lineNum = lineNumberAt(content, m.index);
5264
5342
  findings.push({
5265
5343
  rule: "VC117",
5266
5344
  title: pathTraversal.title,
@@ -5291,7 +5369,7 @@ var piiInLogs = {
5291
5369
  while ((m = logPattern.exec(content)) !== null) {
5292
5370
  if (isCommentLine(content, m.index)) continue;
5293
5371
  if (isInsideFixMessage(content, m.index)) continue;
5294
- const lineNum = content.substring(0, m.index).split("\n").length;
5372
+ const lineNum = lineNumberAt(content, m.index);
5295
5373
  findings.push({
5296
5374
  rule: "VC118",
5297
5375
  title: piiInLogs.title,
@@ -5329,7 +5407,7 @@ var hardcodedOAuthSecret = {
5329
5407
  while ((m = pat.exec(content)) !== null) {
5330
5408
  if (isCommentLine(content, m.index)) continue;
5331
5409
  if (/process\.env|os\.environ|ENV\[|getenv|\$\{|<your|CHANGE_ME|xxx|placeholder/i.test(m[0])) continue;
5332
- const lineNum = content.substring(0, m.index).split("\n").length;
5410
+ const lineNum = lineNumberAt(content, m.index);
5333
5411
  findings.push({
5334
5412
  rule: "VC119",
5335
5413
  title: hardcodedOAuthSecret.title,
@@ -5361,7 +5439,7 @@ var missingOAuthState = {
5361
5439
  if (isCommentLine(content, m.index)) continue;
5362
5440
  const surrounding = content.substring(m.index, Math.min(content.length, m.index + 500));
5363
5441
  if (/state\s*[=:]/i.test(surrounding)) continue;
5364
- const lineNum = content.substring(0, m.index).split("\n").length;
5442
+ const lineNum = lineNumberAt(content, m.index);
5365
5443
  findings.push({
5366
5444
  rule: "VC120",
5367
5445
  title: missingOAuthState.title,
@@ -5389,7 +5467,7 @@ var unpinnedGitHubAction = {
5389
5467
  let m;
5390
5468
  while ((m = usesPattern.exec(content)) !== null) {
5391
5469
  if (isCommentLine(content, m.index)) continue;
5392
- const lineNum = content.substring(0, m.index).split("\n").length;
5470
+ const lineNum = lineNumberAt(content, m.index);
5393
5471
  findings.push({
5394
5472
  rule: "VC121",
5395
5473
  title: unpinnedGitHubAction.title,
@@ -5423,7 +5501,7 @@ var deprecatedTLS = {
5423
5501
  let m;
5424
5502
  while ((m = pat.exec(content)) !== null) {
5425
5503
  if (isCommentLine(content, m.index)) continue;
5426
- const lineNum = content.substring(0, m.index).split("\n").length;
5504
+ const lineNum = lineNumberAt(content, m.index);
5427
5505
  findings.push({
5428
5506
  rule: "VC122",
5429
5507
  title: deprecatedTLS.title,
@@ -5459,7 +5537,7 @@ var weakRSAKeySize = {
5459
5537
  let m;
5460
5538
  while ((m = pat.exec(content)) !== null) {
5461
5539
  if (isCommentLine(content, m.index)) continue;
5462
- const lineNum = content.substring(0, m.index).split("\n").length;
5540
+ const lineNum = lineNumberAt(content, m.index);
5463
5541
  findings.push({
5464
5542
  rule: "VC123",
5465
5543
  title: weakRSAKeySize.title,
@@ -5495,7 +5573,7 @@ var ecbModeEncryption = {
5495
5573
  let m;
5496
5574
  while ((m = pat.exec(content)) !== null) {
5497
5575
  if (isCommentLine(content, m.index)) continue;
5498
- const lineNum = content.substring(0, m.index).split("\n").length;
5576
+ const lineNum = lineNumberAt(content, m.index);
5499
5577
  findings.push({
5500
5578
  rule: "VC124",
5501
5579
  title: ecbModeEncryption.title,
@@ -5526,7 +5604,7 @@ var insecurePasswordReset = {
5526
5604
  let m;
5527
5605
  while ((m = weakTokens.exec(content)) !== null) {
5528
5606
  if (isCommentLine(content, m.index)) continue;
5529
- const lineNum = content.substring(0, m.index).split("\n").length;
5607
+ const lineNum = lineNumberAt(content, m.index);
5530
5608
  findings.push({
5531
5609
  rule: "VC125",
5532
5610
  title: insecurePasswordReset.title,
@@ -5543,7 +5621,7 @@ var insecurePasswordReset = {
5543
5621
  if (isCommentLine(content, m.index)) continue;
5544
5622
  const surrounding = content.substring(Math.max(0, m.index - 500), m.index);
5545
5623
  if (!/(?:reset|forgot).*password/i.test(surrounding)) continue;
5546
- const lineNum = content.substring(0, m.index).split("\n").length;
5624
+ const lineNum = lineNumberAt(content, m.index);
5547
5625
  findings.push({
5548
5626
  rule: "VC125",
5549
5627
  title: insecurePasswordReset.title,
@@ -5604,7 +5682,7 @@ var insecureHTTPMethods = {
5604
5682
  if (isCommentLine(content, m.index)) continue;
5605
5683
  const handlerBlock = content.substring(m.index, Math.min(content.length, m.index + 500));
5606
5684
  if (/auth|authenticate|authorize|requireAuth|isAuthenticated|protect|guard|middleware|session|jwt|verify|clerk|getAuth/i.test(handlerBlock)) continue;
5607
- const lineNum = content.substring(0, m.index).split("\n").length;
5685
+ const lineNum = lineNumberAt(content, m.index);
5608
5686
  findings.push({
5609
5687
  rule: "VC127",
5610
5688
  title: insecureHTTPMethods.title,
@@ -5638,7 +5716,7 @@ var httpRequestSmuggling = {
5638
5716
  let m;
5639
5717
  while ((m = pat.exec(content)) !== null) {
5640
5718
  if (isCommentLine(content, m.index)) continue;
5641
- const lineNum = content.substring(0, m.index).split("\n").length;
5719
+ const lineNum = lineNumberAt(content, m.index);
5642
5720
  findings.push({
5643
5721
  rule: "VC128",
5644
5722
  title: httpRequestSmuggling.title,
@@ -5672,7 +5750,7 @@ var unencryptedPII = {
5672
5750
  let m;
5673
5751
  while ((m = pat.exec(content)) !== null) {
5674
5752
  if (isCommentLine(content, m.index)) continue;
5675
- const lineNum = content.substring(0, m.index).split("\n").length;
5753
+ const lineNum = lineNumberAt(content, m.index);
5676
5754
  findings.push({
5677
5755
  rule: "VC129",
5678
5756
  title: unencryptedPII.title,
@@ -5704,7 +5782,7 @@ var missingAuthRateLimit = {
5704
5782
  if (isCommentLine(content, m.index)) continue;
5705
5783
  const surrounding = content.substring(Math.max(0, m.index - 300), Math.min(content.length, m.index + 300));
5706
5784
  if (/rateLimit|rateLimiter|throttle|slowDown|express-rate-limit|rate_limit|RateLimiter|limiter/i.test(surrounding)) continue;
5707
- const lineNum = content.substring(0, m.index).split("\n").length;
5785
+ const lineNum = lineNumberAt(content, m.index);
5708
5786
  findings.push({
5709
5787
  rule: "VC130",
5710
5788
  title: missingAuthRateLimit.title,
@@ -5742,7 +5820,7 @@ var vulnerableDependencies = {
5742
5820
  for (const [pat, message] of vulnerablePackages) {
5743
5821
  let m;
5744
5822
  while ((m = pat.exec(content)) !== null) {
5745
- const lineNum = content.substring(0, m.index).split("\n").length;
5823
+ const lineNum = lineNumberAt(content, m.index);
5746
5824
  findings.push({
5747
5825
  rule: "VC131",
5748
5826
  title: vulnerableDependencies.title,
@@ -5774,7 +5852,7 @@ function secretRuleCheck(content, filePath, pattern, ruleId, title, severity, fi
5774
5852
  const lineStart = content.lastIndexOf("\n", m.index - 1) + 1;
5775
5853
  const lineText = content.substring(lineStart, content.indexOf("\n", m.index));
5776
5854
  if (PLACEHOLDER_RE.test(lineText)) continue;
5777
- const lineNum = content.substring(0, m.index).split("\n").length;
5855
+ const lineNum = lineNumberAt(content, m.index);
5778
5856
  findings.push({
5779
5857
  rule: ruleId,
5780
5858
  title,
@@ -5875,7 +5953,7 @@ var hardcodedGCPServiceAccount = {
5875
5953
  const findings = [];
5876
5954
  const m = content.match(/"type"\s*:\s*"service_account"/);
5877
5955
  if (m && m.index !== void 0) {
5878
- const lineNum = content.substring(0, m.index).split("\n").length;
5956
+ const lineNum = lineNumberAt(content, m.index);
5879
5957
  findings.push({
5880
5958
  rule: "VC136",
5881
5959
  title: this.title,
@@ -5981,7 +6059,7 @@ var hardcodedDatadogKey = {
5981
6059
  const lineStart = content.lastIndexOf("\n", m.index - 1) + 1;
5982
6060
  const lineText = content.substring(lineStart, content.indexOf("\n", m.index));
5983
6061
  if (PLACEHOLDER_RE.test(lineText)) continue;
5984
- const lineNum = content.substring(0, m.index).split("\n").length;
6062
+ const lineNum = lineNumberAt(content, m.index);
5985
6063
  findings.push({
5986
6064
  rule: "VC141",
5987
6065
  title: this.title,
@@ -6015,7 +6093,7 @@ var hardcodedVercelToken = {
6015
6093
  const lineStart = content.lastIndexOf("\n", m.index - 1) + 1;
6016
6094
  const lineText = content.substring(lineStart, content.indexOf("\n", m.index));
6017
6095
  if (PLACEHOLDER_RE.test(lineText)) continue;
6018
- const lineNum = content.substring(0, m.index).split("\n").length;
6096
+ const lineNum = lineNumberAt(content, m.index);
6019
6097
  findings.push({
6020
6098
  rule: "VC142",
6021
6099
  title: this.title,
@@ -6049,7 +6127,7 @@ var hardcodedSupabaseServiceRole = {
6049
6127
  const lineStart = content.lastIndexOf("\n", m.index - 1) + 1;
6050
6128
  const lineText = content.substring(lineStart, content.indexOf("\n", m.index));
6051
6129
  if (PLACEHOLDER_RE.test(lineText)) continue;
6052
- const lineNum = content.substring(0, m.index).split("\n").length;
6130
+ const lineNum = lineNumberAt(content, m.index);
6053
6131
  findings.push({
6054
6132
  rule: "VC143",
6055
6133
  title: this.title,
@@ -6113,7 +6191,7 @@ function contextSecretRuleCheck(content, filePath, pattern, ruleId, title, sever
6113
6191
  const lineStart = content.lastIndexOf("\n", m.index - 1) + 1;
6114
6192
  const lineText = content.substring(lineStart, content.indexOf("\n", m.index));
6115
6193
  if (PLACEHOLDER_RE.test(lineText)) continue;
6116
- const lineNum = content.substring(0, m.index).split("\n").length;
6194
+ const lineNum = lineNumberAt(content, m.index);
6117
6195
  findings.push({
6118
6196
  rule: ruleId,
6119
6197
  title,
@@ -6593,7 +6671,7 @@ var ghaPullRequestTargetCheckout = {
6593
6671
  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;
6594
6672
  let m;
6595
6673
  while ((m = checkoutPattern.exec(content)) !== null) {
6596
- const lineNum = content.substring(0, m.index).split("\n").length;
6674
+ const lineNum = lineNumberAt(content, m.index);
6597
6675
  findings.push({
6598
6676
  rule: "VC184",
6599
6677
  title: ghaPullRequestTargetCheckout.title,
@@ -6677,7 +6755,7 @@ var ghaThirdPartyActionWithSecrets = {
6677
6755
  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;
6678
6756
  let m;
6679
6757
  while ((m = pattern.exec(content)) !== null) {
6680
- const lineNum = content.substring(0, m.index).split("\n").length;
6758
+ const lineNum = lineNumberAt(content, m.index);
6681
6759
  findings.push({
6682
6760
  rule: "VC187",
6683
6761
  title: ghaThirdPartyActionWithSecrets.title,
@@ -6842,7 +6920,7 @@ var pyRequestsVerifyFalse = {
6842
6920
  let m;
6843
6921
  while ((m = pattern.exec(content)) !== null) {
6844
6922
  if (isCommentLine(content, m.index)) continue;
6845
- const lineNum = content.substring(0, m.index).split("\n").length;
6923
+ const lineNum = lineNumberAt(content, m.index);
6846
6924
  findings.push({
6847
6925
  rule: "VC191",
6848
6926
  title: pyRequestsVerifyFalse.title,
@@ -6873,7 +6951,7 @@ var pyJinja2AutoescapeOff = {
6873
6951
  let m;
6874
6952
  while ((m = pattern.exec(content)) !== null) {
6875
6953
  if (isCommentLine(content, m.index)) continue;
6876
- const lineNum = content.substring(0, m.index).split("\n").length;
6954
+ const lineNum = lineNumberAt(content, m.index);
6877
6955
  findings.push({
6878
6956
  rule: "VC192",
6879
6957
  title: pyJinja2AutoescapeOff.title,
@@ -6902,7 +6980,7 @@ var pyTempfileMktemp = {
6902
6980
  let m;
6903
6981
  while ((m = pattern.exec(content)) !== null) {
6904
6982
  if (isCommentLine(content, m.index)) continue;
6905
- const lineNum = content.substring(0, m.index).split("\n").length;
6983
+ const lineNum = lineNumberAt(content, m.index);
6906
6984
  findings.push({
6907
6985
  rule: "VC193",
6908
6986
  title: pyTempfileMktemp.title,
@@ -6944,7 +7022,7 @@ var pyDjangoMarkSafe = {
6944
7022
  let m;
6945
7023
  while ((m = pattern.exec(content)) !== null) {
6946
7024
  if (isCommentLine(content, m.index)) continue;
6947
- const lineNum = content.substring(0, m.index).split("\n").length;
7025
+ const lineNum = lineNumberAt(content, m.index);
6948
7026
  if (seenLines.has(lineNum)) continue;
6949
7027
  seenLines.add(lineNum);
6950
7028
  findings.push({
@@ -6981,7 +7059,7 @@ var pyParamikoAutoAdd = {
6981
7059
  let m;
6982
7060
  while ((m = pattern.exec(content)) !== null) {
6983
7061
  if (isCommentLine(content, m.index)) continue;
6984
- const lineNum = content.substring(0, m.index).split("\n").length;
7062
+ const lineNum = lineNumberAt(content, m.index);
6985
7063
  if (seenLines.has(lineNum)) continue;
6986
7064
  seenLines.add(lineNum);
6987
7065
  findings.push({
@@ -7014,7 +7092,7 @@ var pyDjangoAllowedHostsWildcard = {
7014
7092
  let m;
7015
7093
  while ((m = pattern.exec(content)) !== null) {
7016
7094
  if (isCommentLine(content, m.index)) continue;
7017
- const lineNum = content.substring(0, m.index).split("\n").length;
7095
+ const lineNum = lineNumberAt(content, m.index);
7018
7096
  findings.push({
7019
7097
  rule: "VC196",
7020
7098
  title: pyDjangoAllowedHostsWildcard.title,
@@ -7052,7 +7130,7 @@ var pyJWTDecodeWeakConfig = {
7052
7130
  if (isCommentLine(content, m.index)) continue;
7053
7131
  const args = m[1];
7054
7132
  if (/\balgorithms\s*=/.test(args)) continue;
7055
- const lineNum = content.substring(0, m.index).split("\n").length;
7133
+ const lineNum = lineNumberAt(content, m.index);
7056
7134
  if (seenLines.has(lineNum)) continue;
7057
7135
  seenLines.add(lineNum);
7058
7136
  findings.push({
@@ -7103,7 +7181,7 @@ var llmPromptInjection = {
7103
7181
  let m;
7104
7182
  while ((m = pattern.exec(content)) !== null) {
7105
7183
  if (isCommentLine(content, m.index)) continue;
7106
- const lineNum = content.substring(0, m.index).split("\n").length;
7184
+ const lineNum = lineNumberAt(content, m.index);
7107
7185
  if (seenLines.has(lineNum)) continue;
7108
7186
  seenLines.add(lineNum);
7109
7187
  findings.push({
@@ -7142,7 +7220,7 @@ var llmSystemPromptInjection = {
7142
7220
  let m;
7143
7221
  while ((m = pattern.exec(content)) !== null) {
7144
7222
  if (isCommentLine(content, m.index)) continue;
7145
- const lineNum = content.substring(0, m.index).split("\n").length;
7223
+ const lineNum = lineNumberAt(content, m.index);
7146
7224
  findings.push({
7147
7225
  rule: "VC199",
7148
7226
  title: llmSystemPromptInjection.title,
@@ -7184,7 +7262,7 @@ var llmOutputAsHTML = {
7184
7262
  let m;
7185
7263
  while ((m = pattern.exec(content)) !== null) {
7186
7264
  if (isCommentLine(content, m.index)) continue;
7187
- const lineNum = content.substring(0, m.index).split("\n").length;
7265
+ const lineNum = lineNumberAt(content, m.index);
7188
7266
  if (seenLines.has(lineNum)) continue;
7189
7267
  seenLines.add(lineNum);
7190
7268
  findings.push({
@@ -7221,7 +7299,7 @@ var vectorStoreQueryNoUserFilter = {
7221
7299
  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;
7222
7300
  if (/\bnamespace\s*[:=]\s*[`"']?[^,)`"']*(?:user|tenant|org|owner|account|customer|workspace)/i.test(args)) continue;
7223
7301
  if (/\bfilter\s*[:=][\s\S]*?(?:\buser|\btenant|\borg|\bowner|\baccount|\bcustomer|\bworkspace)/i.test(args)) continue;
7224
- const lineNum = content.substring(0, m.index).split("\n").length;
7302
+ const lineNum = lineNumberAt(content, m.index);
7225
7303
  findings.push({
7226
7304
  rule: "VC201",
7227
7305
  title: vectorStoreQueryNoUserFilter.title,
@@ -7258,7 +7336,7 @@ var vectorStoreUpsertNoMetadata = {
7258
7336
  if (/\bnamespace\s*[:=]\s*[`"']?[^,)`"']*(?:user|tenant|org)/i.test(args)) {
7259
7337
  continue;
7260
7338
  }
7261
- const lineNum = content.substring(0, m.index).split("\n").length;
7339
+ const lineNum = lineNumberAt(content, m.index);
7262
7340
  findings.push({
7263
7341
  rule: "VC202",
7264
7342
  title: vectorStoreUpsertNoMetadata.title,
@@ -7312,7 +7390,7 @@ var llmCallNoMaxTokens = {
7312
7390
  if (depth !== 0) continue;
7313
7391
  const args = content.substring(openIdx + 1, i).replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/[^\n]*/g, "").replace(/#[^\n]*/g, "");
7314
7392
  if (TOKEN_KW_RE.test(args)) continue;
7315
- const lineNum = content.substring(0, m.index).split("\n").length;
7393
+ const lineNum = lineNumberAt(content, m.index);
7316
7394
  findings.push({
7317
7395
  rule: "VC203",
7318
7396
  title: llmCallNoMaxTokens.title,
@@ -7352,7 +7430,7 @@ var graphqlNoDepthLimit = {
7352
7430
  let m;
7353
7431
  while ((m = anchorRe.exec(content)) !== null) {
7354
7432
  if (isCommentLine(content, m.index)) continue;
7355
- const lineNum = content.substring(0, m.index).split("\n").length;
7433
+ const lineNum = lineNumberAt(content, m.index);
7356
7434
  findings.push({
7357
7435
  rule: "VC204",
7358
7436
  title: graphqlNoDepthLimit.title,
@@ -7386,7 +7464,7 @@ var graphqlNoComplexityLimit = {
7386
7464
  let m;
7387
7465
  while ((m = anchorRe.exec(content)) !== null) {
7388
7466
  if (isCommentLine(content, m.index)) continue;
7389
- const lineNum = content.substring(0, m.index).split("\n").length;
7467
+ const lineNum = lineNumberAt(content, m.index);
7390
7468
  findings.push({
7391
7469
  rule: "VC205",
7392
7470
  title: graphqlNoComplexityLimit.title,
@@ -7416,7 +7494,7 @@ var graphqlCSRFDisabled = {
7416
7494
  let m;
7417
7495
  while ((m = pattern.exec(content)) !== null) {
7418
7496
  if (isCommentLine(content, m.index)) continue;
7419
- const lineNum = content.substring(0, m.index).split("\n").length;
7497
+ const lineNum = lineNumberAt(content, m.index);
7420
7498
  findings.push({
7421
7499
  rule: "VC206",
7422
7500
  title: graphqlCSRFDisabled.title,
@@ -7512,7 +7590,7 @@ var webhookMissingIdempotency = {
7512
7590
  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 [];
7513
7591
  const m = content.match(/constructEvent|new\s+Webhook\s*\(|verifyHeader/i);
7514
7592
  if (!m || m.index === void 0) return [];
7515
- const line = content.substring(0, m.index).split("\n").length;
7593
+ const line = lineNumberAt(content, m.index);
7516
7594
  return filterSilenced([{
7517
7595
  rule: "VC209",
7518
7596
  title: webhookMissingIdempotency.title,
@@ -7539,7 +7617,7 @@ var middlewareMatcherExcludesApi = {
7539
7617
  if (!/\(\?!\s*[^)]*\bapi\b/.test(content)) return [];
7540
7618
  const m = content.match(/matcher\s*:/);
7541
7619
  if (!m || m.index === void 0) return [];
7542
- const line = content.substring(0, m.index).split("\n").length;
7620
+ const line = lineNumberAt(content, m.index);
7543
7621
  return filterSilenced([{
7544
7622
  rule: "VC210",
7545
7623
  title: middlewareMatcherExcludesApi.title,
@@ -7552,6 +7630,113 @@ var middlewareMatcherExcludesApi = {
7552
7630
  }], content, "VC210");
7553
7631
  }
7554
7632
  };
7633
+ function passesLuhn(digits) {
7634
+ let sum = 0;
7635
+ let alt = false;
7636
+ for (let i = digits.length - 1; i >= 0; i--) {
7637
+ let d = digits.charCodeAt(i) - 48;
7638
+ if (d < 0 || d > 9) return false;
7639
+ if (alt) {
7640
+ d *= 2;
7641
+ if (d > 9) d -= 9;
7642
+ }
7643
+ sum += d;
7644
+ alt = !alt;
7645
+ }
7646
+ return sum % 10 === 0;
7647
+ }
7648
+ function looksLikeIssuerCard(d) {
7649
+ if (/^4/.test(d) && (d.length === 13 || d.length === 16 || d.length === 19)) return true;
7650
+ if (/^5[1-5]/.test(d) && d.length === 16) return true;
7651
+ if (/^2(2[2-9]|[3-6]\d|7[01]|720)/.test(d) && d.length === 16) return true;
7652
+ if (/^3[47]/.test(d) && d.length === 15) return true;
7653
+ if (/^(6011|65|64[4-9])/.test(d) && d.length === 16) return true;
7654
+ return false;
7655
+ }
7656
+ var TEST_CARD_NUMBERS = /* @__PURE__ */ new Set([
7657
+ "4242424242424242",
7658
+ "4111111111111111",
7659
+ "4012888888881881",
7660
+ "4000056655665556",
7661
+ "4000000000000002",
7662
+ "4000000000009995",
7663
+ "5555555555554444",
7664
+ "5200828282828210",
7665
+ "5105105105105100",
7666
+ "2223003122003222",
7667
+ "378282246310005",
7668
+ "371449635398431",
7669
+ "6011111111111117",
7670
+ "6011000990139424",
7671
+ "3056930009020004",
7672
+ "38520000023237"
7673
+ ]);
7674
+ var hardcodedCreditCard = {
7675
+ id: "VC211",
7676
+ title: "Hardcoded credit card number",
7677
+ severity: "high",
7678
+ category: "Information Leakage",
7679
+ 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.",
7680
+ check(content, filePath) {
7681
+ if (isTestFile(filePath)) return [];
7682
+ const findings = [];
7683
+ const pattern = /(?<![\d.])\d[\d -]{11,21}\d(?![\d.])/g;
7684
+ let m;
7685
+ while ((m = pattern.exec(content)) !== null) {
7686
+ const digits = m[0].replace(/[ -]/g, "");
7687
+ if (digits.length < 13 || digits.length > 19) continue;
7688
+ if (TEST_CARD_NUMBERS.has(digits)) continue;
7689
+ if (!looksLikeIssuerCard(digits)) continue;
7690
+ if (!passesLuhn(digits)) continue;
7691
+ if (isCommentLine(content, m.index)) continue;
7692
+ const lineNum = lineNumberAt(content, m.index);
7693
+ findings.push({
7694
+ rule: "VC211",
7695
+ title: hardcodedCreditCard.title,
7696
+ severity: "high",
7697
+ category: "Information Leakage",
7698
+ file: filePath,
7699
+ line: lineNum,
7700
+ snippet: getSnippet(content, lineNum),
7701
+ 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)."
7702
+ });
7703
+ }
7704
+ return findings;
7705
+ }
7706
+ };
7707
+ var hardcodedSSN = {
7708
+ id: "VC212",
7709
+ title: "Hardcoded US Social Security Number",
7710
+ severity: "high",
7711
+ category: "Information Leakage",
7712
+ 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.",
7713
+ check(content, filePath) {
7714
+ if (isTestFile(filePath)) return [];
7715
+ const findings = [];
7716
+ const pattern = /(?:ssn|social[_-]?sec(?:urity)?(?:[_-]?(?:no|num|number))?|taxpayer[_-]?id)\b["']?\s*[:=]\s*["']?(\d{3}-\d{2}-\d{4})/gi;
7717
+ let m;
7718
+ while ((m = pattern.exec(content)) !== null) {
7719
+ const ssn = m[1];
7720
+ const [area, group, serial] = ssn.split("-");
7721
+ if (area === "000" || area === "666" || Number(area) >= 900) continue;
7722
+ if (group === "00" || serial === "0000") continue;
7723
+ if (ssn === "123-45-6789") continue;
7724
+ if (isCommentLine(content, m.index)) continue;
7725
+ const lineNum = lineNumberAt(content, m.index);
7726
+ findings.push({
7727
+ rule: "VC212",
7728
+ title: hardcodedSSN.title,
7729
+ severity: "high",
7730
+ category: "Information Leakage",
7731
+ file: filePath,
7732
+ line: lineNum,
7733
+ snippet: getSnippet(content, lineNum),
7734
+ 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."
7735
+ });
7736
+ }
7737
+ return findings;
7738
+ }
7739
+ };
7555
7740
  var secretInURLParam = {
7556
7741
  id: "VC146",
7557
7742
  title: "Secret Passed in URL Query Parameter",
@@ -7577,7 +7762,7 @@ var secretInURLParam = {
7577
7762
  if (isCommentLine(content, m.index)) continue;
7578
7763
  if (isInsideFixMessage(content, m.index)) continue;
7579
7764
  if (isInlineSilenced(content, m.index, "VC146")) continue;
7580
- const lineNum = content.substring(0, m.index).split("\n").length;
7765
+ const lineNum = lineNumberAt(content, m.index);
7581
7766
  findings.push({
7582
7767
  rule: "VC146",
7583
7768
  title: this.title,
@@ -7608,7 +7793,7 @@ var secretLoggedToConsole = {
7608
7793
  while ((m = pattern.exec(content)) !== null) {
7609
7794
  if (isCommentLine(content, m.index)) continue;
7610
7795
  if (isInsideFixMessage(content, m.index)) continue;
7611
- const lineNum = content.substring(0, m.index).split("\n").length;
7796
+ const lineNum = lineNumberAt(content, m.index);
7612
7797
  findings.push({
7613
7798
  rule: "VC147",
7614
7799
  title: this.title,
@@ -7638,7 +7823,7 @@ var secretInErrorResponse = {
7638
7823
  while ((m = pattern.exec(content)) !== null) {
7639
7824
  if (isCommentLine(content, m.index)) continue;
7640
7825
  if (isInsideFixMessage(content, m.index)) continue;
7641
- const lineNum = content.substring(0, m.index).split("\n").length;
7826
+ const lineNum = lineNumberAt(content, m.index);
7642
7827
  findings.push({
7643
7828
  rule: "VC148",
7644
7829
  title: this.title,
@@ -7677,7 +7862,7 @@ var secretInBundleConfig = {
7677
7862
  while ((m = re.exec(content)) !== null) {
7678
7863
  if (isCommentLine(content, m.index)) continue;
7679
7864
  if (isInsideFixMessage(content, m.index)) continue;
7680
- const lineNum = content.substring(0, m.index).split("\n").length;
7865
+ const lineNum = lineNumberAt(content, m.index);
7681
7866
  findings.push({
7682
7867
  rule: "VC149",
7683
7868
  title: this.title,
@@ -7715,7 +7900,7 @@ var secretInHTMLAttribute = {
7715
7900
  while ((m = re.exec(content)) !== null) {
7716
7901
  if (isCommentLine(content, m.index)) continue;
7717
7902
  if (isInsideFixMessage(content, m.index)) continue;
7718
- const lineNum = content.substring(0, m.index).split("\n").length;
7903
+ const lineNum = lineNumberAt(content, m.index);
7719
7904
  findings.push({
7720
7905
  rule: "VC150",
7721
7906
  title: this.title,
@@ -7742,9 +7927,9 @@ var secretInCLIArgument = {
7742
7927
  if (!filePath.match(/\.(js|ts|jsx|tsx|py|rb)$/)) return [];
7743
7928
  const findings = [];
7744
7929
  const patterns = [
7745
- /(?:exec|execSync|spawn|spawnSync|child_process)\s*\([^)]*\$\{[^}]*(?:api[_-]?key|secret|token|password|credentials)/gi,
7746
- /(?:exec|execSync|spawn|spawnSync|child_process)\s*\([^)]*["']\s*\+\s*(?:api[_-]?key|secret|token|password|credentials)/gi,
7747
- /(?:subprocess|os\.system|os\.popen)\s*\([^)]*\{[^}]*(?:api[_-]?key|secret|token|password|credentials)/gi
7930
+ /(?:exec|execSync|spawn|spawnSync|child_process)\s*\([^)]{0,500}\$\{[^}]{0,200}(?:api[_-]?key|secret|token|password|credentials)/gi,
7931
+ /(?:exec|execSync|spawn|spawnSync|child_process)\s*\([^)]{0,500}["']\s*\+\s*(?:api[_-]?key|secret|token|password|credentials)/gi,
7932
+ /(?:subprocess|os\.system|os\.popen)\s*\([^)]{0,500}\{[^}]{0,200}(?:api[_-]?key|secret|token|password|credentials)/gi
7748
7933
  ];
7749
7934
  for (const p of patterns) {
7750
7935
  let m;
@@ -7752,7 +7937,7 @@ var secretInCLIArgument = {
7752
7937
  while ((m = re.exec(content)) !== null) {
7753
7938
  if (isCommentLine(content, m.index)) continue;
7754
7939
  if (isInsideFixMessage(content, m.index)) continue;
7755
- const lineNum = content.substring(0, m.index).split("\n").length;
7940
+ const lineNum = lineNumberAt(content, m.index);
7756
7941
  findings.push({
7757
7942
  rule: "VC151",
7758
7943
  title: this.title,
@@ -7798,7 +7983,7 @@ var webhookSignatureVerification = {
7798
7983
  if (svc.verify.test(content)) continue;
7799
7984
  const m = content.match(/export\s+(?:async\s+)?function\s+POST/);
7800
7985
  if (!m || m.index === void 0) continue;
7801
- const lineNum = content.substring(0, m.index).split("\n").length;
7986
+ const lineNum = lineNumberAt(content, m.index);
7802
7987
  findings.push({
7803
7988
  rule: "VC152",
7804
7989
  title: `${svc.name} Webhook Missing Signature Verification`,
@@ -7865,7 +8050,7 @@ var missingRequestValidation = {
7865
8050
  if (/if\s*\(\s*!(?:body|parsed|data|payload)\.|throw.*(?:missing|invalid|required)/i.test(content)) return [];
7866
8051
  const m = content.match(/export\s+(?:async\s+)?function\s+(?:POST|PUT|PATCH)/);
7867
8052
  if (!m || m.index === void 0) return [];
7868
- const lineNum = content.substring(0, m.index).split("\n").length;
8053
+ const lineNum = lineNumberAt(content, m.index);
7869
8054
  return [{
7870
8055
  rule: "VC154",
7871
8056
  title: this.title,
@@ -7892,7 +8077,7 @@ var missingAIRateLimit = {
7892
8077
  if (/rateLimit|rateLimiter|throttle|checkRateLimit|limiter|slowDown|express-rate-limit/i.test(content)) return [];
7893
8078
  const m = content.match(/export\s+(?:async\s+)?function\s+(?:POST|GET)/i) || content.match(/\.(post|get)\s*\(/i);
7894
8079
  if (!m || m.index === void 0) return [];
7895
- const lineNum = content.substring(0, m.index).split("\n").length;
8080
+ const lineNum = lineNumberAt(content, m.index);
7896
8081
  return [{
7897
8082
  rule: "VC155",
7898
8083
  title: this.title,
@@ -7921,7 +8106,7 @@ var missingPagination = {
7921
8106
  if (/findUnique|findFirst|findById|\.findOne|WHERE.*id\s*=/i.test(content)) return [];
7922
8107
  const m = content.match(/export\s+(?:async\s+)?function\s+GET/);
7923
8108
  if (!m || m.index === void 0) return [];
7924
- const lineNum = content.substring(0, m.index).split("\n").length;
8109
+ const lineNum = lineNumberAt(content, m.index);
7925
8110
  return [{
7926
8111
  rule: "VC156",
7927
8112
  title: this.title,
@@ -7982,7 +8167,7 @@ var insecureDirectObjectReference = {
7982
8167
  if (/requireUser|requireUserForApi|getServerSession|auth\(\)/i.test(content)) return [];
7983
8168
  const m = content.match(/params\.id|params\.(?:slug|uuid)|req\.params\./);
7984
8169
  if (!m || m.index === void 0) return [];
7985
- const lineNum = content.substring(0, m.index).split("\n").length;
8170
+ const lineNum = lineNumberAt(content, m.index);
7986
8171
  return [{
7987
8172
  rule: "VC158",
7988
8173
  title: this.title,
@@ -8280,7 +8465,10 @@ var allCustomRules = [
8280
8465
  // VC204–VC206: GraphQL server hardening
8281
8466
  graphqlNoDepthLimit,
8282
8467
  graphqlNoComplexityLimit,
8283
- graphqlCSRFDisabled
8468
+ graphqlCSRFDisabled,
8469
+ // VC211–VC212: hardcoded sensitive personal data (PII)
8470
+ hardcodedCreditCard,
8471
+ hardcodedSSN
8284
8472
  ];
8285
8473
  function runCustomRules(content, filePath, disabledRules = [], tier = "free", extraRules = []) {
8286
8474
  const findings = [];
@@ -8288,6 +8476,17 @@ function runCustomRules(content, filePath, disabledRules = [], tier = "free", ex
8288
8476
  return findings;
8289
8477
  }
8290
8478
  if (/pro-rules-bundle|\.bundle\./i.test(filePath)) return findings;
8479
+ let maxLineLen = 0;
8480
+ {
8481
+ let lineStart = 0;
8482
+ for (let i = 0; i <= content.length; i++) {
8483
+ if (i === content.length || content.charCodeAt(i) === 10) {
8484
+ if (i - lineStart > maxLineLen) maxLineLen = i - lineStart;
8485
+ lineStart = i + 1;
8486
+ }
8487
+ }
8488
+ }
8489
+ if (maxLineLen > 5e4) return findings;
8291
8490
  if (/onboarding|demo-data|example-vulnerable|code-sample/i.test(filePath)) return findings;
8292
8491
  const ruleset = tier === "pro" && extraRules.length > 0 ? [...freeRules, ...extraRules] : freeRules;
8293
8492
  for (const rule of ruleset) {
@@ -8697,6 +8896,7 @@ export {
8697
8896
  hardcodedAnthropicKey,
8698
8897
  hardcodedCloudflareToken,
8699
8898
  hardcodedCohereKey,
8899
+ hardcodedCreditCard,
8700
8900
  hardcodedDatadogKey,
8701
8901
  hardcodedDiscordToken,
8702
8902
  hardcodedEncryptionKey,
@@ -8726,6 +8926,7 @@ export {
8726
8926
  hardcodedRailwayToken,
8727
8927
  hardcodedReplicateKey,
8728
8928
  hardcodedResendKey,
8929
+ hardcodedSSN,
8729
8930
  hardcodedSecrets,
8730
8931
  hardcodedSendGridKey,
8731
8932
  hardcodedSentryAuthToken,