xploitscan-shared-rules 1.12.0 → 1.13.1

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
@@ -815,7 +850,7 @@ var SERVER_SIDE_PATH_RE = new RegExp(
815
850
  ].join("|"),
816
851
  "i"
817
852
  );
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/;
853
+ 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
854
  function isServerSideFile(filePath, content) {
820
855
  if (isTestFile(filePath)) return false;
821
856
  if (SERVER_SIDE_PATH_RE.test(filePath)) return true;
@@ -862,13 +897,23 @@ function filterSilenced(matches, content, ruleId) {
862
897
  }
863
898
  function findMatches(content, pattern, rule, filePath, fixTemplate) {
864
899
  const matches = [];
865
- const lines = content.split("\n");
866
900
  let m;
867
901
  const re = new RegExp(pattern.source, pattern.flags.includes("g") ? pattern.flags : `${pattern.flags}g`);
902
+ let scanned = 0;
903
+ let lineNum = 1;
904
+ let lastMatchIndex = -1;
905
+ const MAX_MATCHES = 500;
868
906
  while ((m = re.exec(content)) !== null) {
907
+ if (m.index === lastMatchIndex && m[0].length === 0) {
908
+ re.lastIndex++;
909
+ continue;
910
+ }
911
+ lastMatchIndex = m.index;
869
912
  if (isCommentLine(content, m.index)) continue;
870
913
  if (isInsideFixMessage(content, m.index)) continue;
871
- const lineNum = content.substring(0, m.index).split("\n").length;
914
+ for (; scanned < m.index; scanned++) {
915
+ if (content.charCodeAt(scanned) === 10) lineNum++;
916
+ }
872
917
  matches.push({
873
918
  rule: rule.id,
874
919
  title: rule.title,
@@ -879,6 +924,7 @@ function findMatches(content, pattern, rule, filePath, fixTemplate) {
879
924
  snippet: getSnippet(content, lineNum),
880
925
  fix: fixTemplate?.(m)
881
926
  });
927
+ if (matches.length >= MAX_MATCHES) break;
882
928
  }
883
929
  return matches;
884
930
  }
@@ -894,10 +940,26 @@ function astMatch(content, filePath, line, rule, fix) {
894
940
  fix
895
941
  };
896
942
  }
943
+ var parseCacheContent = null;
944
+ var parseCachePath = null;
945
+ var parseCacheResult = null;
946
+ var parseCacheValid = false;
897
947
  function tryParse(content, filePath) {
898
- const parsed = parseFile(content, filePath);
899
- if (!parsed) return null;
900
- return { parsed, taint: buildTaintMap(parsed) };
948
+ if (parseCacheValid && content === parseCacheContent && filePath === parseCachePath) {
949
+ return parseCacheResult;
950
+ }
951
+ let result = null;
952
+ try {
953
+ const parsed = parseFile(content, filePath);
954
+ result = parsed ? { parsed, taint: buildTaintMap(parsed) } : null;
955
+ } catch {
956
+ result = null;
957
+ }
958
+ parseCacheResult = result;
959
+ parseCacheContent = content;
960
+ parseCachePath = filePath;
961
+ parseCacheValid = true;
962
+ return result;
901
963
  }
902
964
  var hardcodedSecrets = {
903
965
  id: "VC001",
@@ -1262,9 +1324,10 @@ var xssVulnerability = {
1262
1324
  category: "Injection",
1263
1325
  description: "Rendering user input without sanitization allows attackers to inject malicious scripts.",
1264
1326
  check(content, filePath) {
1327
+ const hasSanitizerBypass = /bypassSecurityTrust(?:Html|Script|Style|Url|ResourceUrl)\s*\(/.test(content);
1265
1328
  if (/(?:sanitize|purify|escape|xss)/i.test(filePath)) return [];
1266
- if (/DOMPurify\.sanitize|sanitizeHtml|xss\(|escapeHtml/i.test(content)) return [];
1267
- if (/(?:import|require)\s*\(?.*(?:DOMPurify|dompurify|sanitize|sanitizer)/i.test(content)) return [];
1329
+ if (!hasSanitizerBypass && /DOMPurify\.sanitize|sanitizeHtml|xss\(|escapeHtml/i.test(content)) return [];
1330
+ if (!hasSanitizerBypass && /(?:import|require)\s*\(?[^\n]{0,200}(?:DOMPurify|dompurify|sanitize|sanitizer)/i.test(content)) return [];
1268
1331
  const patterns = [
1269
1332
  // React dangerouslySetInnerHTML
1270
1333
  /dangerouslySetInnerHTML\s*=\s*\{\s*\{\s*__html\s*:/g,
@@ -1275,7 +1338,12 @@ var xssVulnerability = {
1275
1338
  // v-html in Vue
1276
1339
  /v-html\s*=/g,
1277
1340
  // {@html} in Svelte
1278
- /\{@html\s/g
1341
+ /\{@html\s/g,
1342
+ // Angular DomSanitizer escape hatches — bypassSecurityTrustHtml /
1343
+ // bypassSecurityTrustScript / ...Url / ...ResourceUrl / ...Style. These
1344
+ // mark a value as trusted and skip Angular's built-in sanitization;
1345
+ // passing user input through one is the canonical Angular DOM-XSS sink.
1346
+ /bypassSecurityTrust(?:Html|Script|Style|Url|ResourceUrl)\s*\(/g
1279
1347
  ];
1280
1348
  const allLines = content.split("\n");
1281
1349
  const matches = [];
@@ -1288,9 +1356,9 @@ var xssVulnerability = {
1288
1356
  () => "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."
1289
1357
  );
1290
1358
  for (const m of raw) {
1291
- const lineText = allLines[m.line - 1] || "";
1359
+ const lineText = (allLines[m.line - 1] || "").slice(0, 4e3);
1292
1360
  if (/\.innerHTML\s*=\s*['"]/.test(lineText) && !/\$\{/.test(lineText)) continue;
1293
- if (/\.innerHTML\s*=\s*['"][^'"]*['"]\s*$/.test(lineText)) continue;
1361
+ if (/\.innerHTML\s*=\s*['"][^'"]{0,2000}['"]\s*$/.test(lineText)) continue;
1294
1362
  if (/dangerouslySetInnerHTML/.test(lineText)) {
1295
1363
  const windowText = allLines.slice(m.line - 1, m.line + 2).join("\n");
1296
1364
  if (/JSON\.stringify/i.test(windowText)) continue;
@@ -1541,7 +1609,7 @@ var evalUsage = {
1541
1609
  /new\s+Function\s*\(\s*(?!["'`])/g
1542
1610
  ];
1543
1611
  const hasEvalInString = /["'`]eval(?:-source-map|["'`])/i.test(content);
1544
- if (hasEvalInString && !/\beval\s*\([^)]*(?:req\.|body\.|input|params|user|data)/i.test(content)) return [];
1612
+ if (hasEvalInString && !/\beval\s*\([^)]{0,500}(?:req\.|body\.|input|params|user|data)/i.test(content)) return [];
1545
1613
  const matches = [];
1546
1614
  for (const p of patterns) {
1547
1615
  const rawMatches = findMatches(
@@ -1582,7 +1650,7 @@ var unvalidatedRedirect = {
1582
1650
  const isPureLiteral = /^["'`][^"'`]*["'`]$/.test(value);
1583
1651
  if (!hasInterpolation && isPureLiteral) continue;
1584
1652
  if (isInlineSilenced(content, m.index, "VC016")) continue;
1585
- const line = content.substring(0, m.index).split("\n").length;
1653
+ const line = lineNumberAt(content, m.index);
1586
1654
  matches.push({
1587
1655
  rule: "VC016",
1588
1656
  title: unvalidatedRedirect.title,
@@ -1805,11 +1873,11 @@ var prototypePollution = {
1805
1873
  ];
1806
1874
  const hasValidation = /schema|validate|sanitize|whitelist|allowedKeys|pick\(|Object\.freeze|zod|yup|joi|ajv/i.test(content);
1807
1875
  if (!hasValidation) {
1808
- const hasUnsafeMerge = /Object\.assign\s*\([^)]*JSON\.parse|\.\.\.JSON\.parse|\{.*\.\.\.(?:stored|saved|cached|parsed|data)/i.test(content);
1876
+ const hasUnsafeMerge = /Object\.assign\s*\([^)]{0,500}JSON\.parse|\.\.\.JSON\.parse|\{[^\n]{0,300}\.\.\.(?:stored|saved|cached|parsed|data)/i.test(content);
1809
1877
  if (hasUnsafeMerge) {
1810
1878
  matches.push(...findMatches(
1811
1879
  content,
1812
- /Object\.assign\s*\([^)]*JSON\.parse|\.\.\.JSON\.parse/g,
1880
+ /Object\.assign\s*\([^)]{0,500}JSON\.parse|\.\.\.JSON\.parse/g,
1813
1881
  prototypePollution,
1814
1882
  filePath,
1815
1883
  () => "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."
@@ -3355,7 +3423,7 @@ var dangerousInnerHTML = {
3355
3423
  if (/^[A-Z][A-Z0-9_]+$/.test(value)) continue;
3356
3424
  if (/^["'`][^$]*["'`]$/.test(value)) continue;
3357
3425
  if (/JSON\.stringify/i.test(value)) continue;
3358
- const lineNum = content.substring(0, m.index).split("\n").length;
3426
+ const lineNum = lineNumberAt(content, m.index);
3359
3427
  findings.push({
3360
3428
  rule: "VC063",
3361
3429
  title: dangerousInnerHTML.title,
@@ -4018,17 +4086,37 @@ var ssti = {
4018
4086
  (call, line) => {
4019
4087
  const first = call.arguments[0];
4020
4088
  if (!first || first.type === "SpreadElement") return;
4021
- if (!taint.isTainted(first)) return;
4022
- if (matches.some((m) => m.line === line)) return;
4023
- matches.push(
4024
- astMatch(
4025
- content,
4026
- filePath,
4027
- line,
4028
- ssti,
4029
- "Compile templates from a trusted, static source (a file path or a constant string). Pass user data only as context values."
4030
- )
4031
- );
4089
+ if (taint.isTainted(first)) {
4090
+ if (matches.some((m) => m.line === line)) return;
4091
+ matches.push(
4092
+ astMatch(
4093
+ content,
4094
+ filePath,
4095
+ line,
4096
+ ssti,
4097
+ "Compile templates from a trusted, static source (a file path or a constant string). Pass user data only as context values."
4098
+ )
4099
+ );
4100
+ return;
4101
+ }
4102
+ const opts = call.arguments[1];
4103
+ if (opts && opts.type === "ObjectExpression") {
4104
+ const hasTaintedSpread = opts.properties.some(
4105
+ (p) => p.type === "SpreadElement" && taint.isTainted(p.argument)
4106
+ );
4107
+ if (hasTaintedSpread) {
4108
+ if (matches.some((m) => m.line === line)) return;
4109
+ matches.push(
4110
+ astMatch(
4111
+ content,
4112
+ filePath,
4113
+ line,
4114
+ ssti,
4115
+ "Don't spread req.body/req.query into template locals \u2014 an attacker can inject reserved view options (e.g. `layout`) to load arbitrary templates. Pass only the specific values the view needs: render('view', { name: req.body.name })."
4116
+ )
4117
+ );
4118
+ }
4119
+ }
4032
4120
  }
4033
4121
  );
4034
4122
  return filterSilenced(matches, content, "VC082");
@@ -4077,7 +4165,7 @@ var missingSRI = {
4077
4165
  const re = new RegExp(scriptPattern.source, scriptPattern.flags);
4078
4166
  while ((m = re.exec(content)) !== null) {
4079
4167
  if (!m[0].includes("integrity")) {
4080
- const lineNum = content.substring(0, m.index).split("\n").length;
4168
+ const lineNum = lineNumberAt(content, m.index);
4081
4169
  matches.push({
4082
4170
  rule: "VC084",
4083
4171
  title: missingSRI.title,
@@ -4291,16 +4379,61 @@ var unprotectedDownload = {
4291
4379
  if (!/(?:download|sendFile|send_file)/i.test(content)) return [];
4292
4380
  if (!isServerSideFile(filePath, content)) return [];
4293
4381
  const matches = [];
4294
- if (/(?:sendFile|download|send_file)\s*\([^)]*(?:req\.|params\.|query\.|body\.)/i.test(content)) {
4295
- const hasValidation = /path\.resolve|path\.normalize|path\.join.*__dirname|realpath|includes\s*\(\s*["']\.\./i.test(content);
4296
- if (!hasValidation) {
4297
- matches.push(...findMatches(
4298
- content,
4299
- /(?:sendFile|download|send_file)\s*\(/gi,
4300
- unprotectedDownload,
4301
- filePath,
4302
- () => "Validate file paths: const safePath = path.resolve(DOWNLOAD_DIR, filename); if (!safePath.startsWith(DOWNLOAD_DIR)) throw new Error('Invalid path');"
4303
- ));
4382
+ const hasContainmentCheck = /\.startsWith\s*\(/.test(content) || /realpath/i.test(content) || /includes\s*\(\s*["']\.\./.test(content);
4383
+ if (/(?:sendFile|download|send_file)\s*\([^)]*(?:req\.|params\.|query\.|body\.)/i.test(content) && !hasContainmentCheck) {
4384
+ matches.push(...findMatches(
4385
+ content,
4386
+ /(?:sendFile|download|send_file)\s*\(/gi,
4387
+ unprotectedDownload,
4388
+ filePath,
4389
+ () => "Validate file paths: const safePath = path.resolve(DOWNLOAD_DIR, filename); if (!safePath.startsWith(DOWNLOAD_DIR + path.sep)) throw new Error('Invalid path');"
4390
+ ));
4391
+ }
4392
+ if (!hasContainmentCheck && /\b(?:sendFile|download)\s*\(/.test(content)) {
4393
+ const ctx = tryParse(content, filePath);
4394
+ if (ctx) {
4395
+ const { parsed, taint } = ctx;
4396
+ const readsUserInput = /\b(?:req|request)\s*\.\s*(?:params|query|body|headers|cookies)\b/.test(content) || /\bparams\s*\.\s*\w/.test(content) || /\bquery\s*\.\s*\w/.test(content) || /\breq\.body\b/.test(content);
4397
+ const isUnsafePathBuild = (node) => {
4398
+ if (node.type !== "CallExpression") return false;
4399
+ const callee = node.callee;
4400
+ const isPathBuild = callee.type === "MemberExpression" && callee.object.type === "Identifier" && callee.object.name === "path" && callee.property.type === "Identifier" && (callee.property.name === "resolve" || callee.property.name === "join");
4401
+ if (!isPathBuild) return false;
4402
+ const rootedAtDirname = node.arguments.some(
4403
+ (a) => a.type === "Identifier" && (a.name === "__dirname" || a.name === "__filename")
4404
+ );
4405
+ if (rootedAtDirname) return false;
4406
+ return node.arguments.some(
4407
+ (a) => a.type !== "SpreadElement" && a.type !== "StringLiteral" && a.type !== "TemplateLiteral"
4408
+ );
4409
+ };
4410
+ const argIsTainted = (node) => {
4411
+ if (taint.isTainted(node)) return true;
4412
+ if (readsUserInput && isUnsafePathBuild(node)) return true;
4413
+ if (node.type === "CallExpression") {
4414
+ return node.arguments.some((a) => a.type !== "SpreadElement" && argIsTainted(a));
4415
+ }
4416
+ return false;
4417
+ };
4418
+ visitCalls(
4419
+ parsed,
4420
+ (callee) => callee.type === "MemberExpression" && callee.property.type === "Identifier" && (callee.property.name === "sendFile" || callee.property.name === "download"),
4421
+ (call, line) => {
4422
+ const first = call.arguments[0];
4423
+ if (!first || first.type === "SpreadElement") return;
4424
+ if (!argIsTainted(first)) return;
4425
+ if (matches.some((m) => m.line === line)) return;
4426
+ matches.push(
4427
+ astMatch(
4428
+ content,
4429
+ filePath,
4430
+ line,
4431
+ unprotectedDownload,
4432
+ "Validate file paths: const safePath = path.resolve(DOWNLOAD_DIR, filename); if (!safePath.startsWith(DOWNLOAD_DIR + path.sep)) throw new Error('Invalid path');"
4433
+ )
4434
+ );
4435
+ }
4436
+ );
4304
4437
  }
4305
4438
  }
4306
4439
  return matches;
@@ -4903,7 +5036,7 @@ var s3BucketNoEncryption = {
4903
5036
  const blockEnd = Math.min(m.index + 2e3, content.length);
4904
5037
  const blockContent = content.substring(m.index, blockEnd);
4905
5038
  if (!/server_side_encryption/.test(blockContent)) {
4906
- const lineNum = content.substring(0, m.index).split("\n").length;
5039
+ const lineNum = lineNumberAt(content, m.index);
4907
5040
  matches.push({
4908
5041
  rule: "VC107",
4909
5042
  title: s3BucketNoEncryption.title,
@@ -4934,7 +5067,7 @@ var securityGroupAllInbound = {
4934
5067
  while ((m = ingressPattern.exec(content)) !== null) {
4935
5068
  if (isCommentLine(content, m.index)) continue;
4936
5069
  if (/from_port\s*=\s*0/.test(m[0]) || !/from_port/.test(m[0])) {
4937
- const lineNum = content.substring(0, m.index).split("\n").length;
5070
+ const lineNum = lineNumberAt(content, m.index);
4938
5071
  matches.push({
4939
5072
  rule: "VC108",
4940
5073
  title: securityGroupAllInbound.title,
@@ -5021,7 +5154,7 @@ var lambdaWithoutVPC = {
5021
5154
  const blockEnd = Math.min(m.index + 2e3, content.length);
5022
5155
  const blockContent = content.substring(m.index, blockEnd);
5023
5156
  if (!/vpc_config\s*\{/.test(blockContent)) {
5024
- const lineNum = content.substring(0, m.index).split("\n").length;
5157
+ const lineNum = lineNumberAt(content, m.index);
5025
5158
  matches.push({
5026
5159
  rule: "VC111",
5027
5160
  title: lambdaWithoutVPC.title,
@@ -5051,7 +5184,7 @@ var dockerLatestTag = {
5051
5184
  while ((m = fromPattern.exec(content)) !== null) {
5052
5185
  const image = m[1];
5053
5186
  if (image.endsWith(":latest") || !image.includes(":") && !image.startsWith("$")) {
5054
- const lineNum = content.substring(0, m.index).split("\n").length;
5187
+ const lineNum = lineNumberAt(content, m.index);
5055
5188
  matches.push({
5056
5189
  rule: "VC112",
5057
5190
  title: dockerLatestTag.title,
@@ -5179,17 +5312,26 @@ var pathTraversal = {
5179
5312
  const findings = [];
5180
5313
  const patterns = [
5181
5314
  /(?:readFile|readFileSync|createReadStream|writeFile|writeFileSync|appendFile|unlink|unlinkSync|access|stat|statSync|existsSync)\s*\(\s*(?:req\.|request\.|ctx\.|params\.|query\.)/gi,
5182
- /(?:path\.join|path\.resolve)\s*\([^)]*(?:req\.|request\.|ctx\.|params\.|query\.)[^)]*\)/gi,
5183
- /(?:res\.sendFile|res\.download)\s*\([^)]*(?:req\.|request\.)/gi,
5315
+ /(?:path\.join|path\.resolve)\s*\([^)]{0,500}(?:req\.|request\.|ctx\.|params\.|query\.)[^)]{0,500}\)/gi,
5316
+ /(?:res\.sendFile|res\.download)\s*\([^)]{0,500}(?:req\.|request\.)/gi,
5184
5317
  /open\s*\(\s*(?:request\.|params\[|args\.)/gi
5185
5318
  ];
5319
+ const MAX_MATCHES = 500;
5186
5320
  for (const pat of patterns) {
5321
+ if (findings.length >= MAX_MATCHES) break;
5187
5322
  let m;
5323
+ let lastIdx = -1;
5188
5324
  while ((m = pat.exec(content)) !== null) {
5325
+ if (findings.length >= MAX_MATCHES) break;
5326
+ if (m.index === lastIdx && m[0].length === 0) {
5327
+ pat.lastIndex++;
5328
+ continue;
5329
+ }
5330
+ lastIdx = m.index;
5189
5331
  if (isCommentLine(content, m.index)) continue;
5190
5332
  const surrounding = content.substring(Math.max(0, m.index - 200), m.index + 200);
5191
- if (/path\.normalize|sanitize|\.replace\(\s*['"]\.\./i.test(surrounding)) continue;
5192
- const lineNum = content.substring(0, m.index).split("\n").length;
5333
+ if (/path\.normalize|sanitize|\.replace\(\s*['"]\.\.|\.startsWith\s*\(|realpath/i.test(surrounding)) continue;
5334
+ const lineNum = lineNumberAt(content, m.index);
5193
5335
  findings.push({
5194
5336
  rule: "VC117",
5195
5337
  title: pathTraversal.title,
@@ -5220,7 +5362,7 @@ var piiInLogs = {
5220
5362
  while ((m = logPattern.exec(content)) !== null) {
5221
5363
  if (isCommentLine(content, m.index)) continue;
5222
5364
  if (isInsideFixMessage(content, m.index)) continue;
5223
- const lineNum = content.substring(0, m.index).split("\n").length;
5365
+ const lineNum = lineNumberAt(content, m.index);
5224
5366
  findings.push({
5225
5367
  rule: "VC118",
5226
5368
  title: piiInLogs.title,
@@ -5258,7 +5400,7 @@ var hardcodedOAuthSecret = {
5258
5400
  while ((m = pat.exec(content)) !== null) {
5259
5401
  if (isCommentLine(content, m.index)) continue;
5260
5402
  if (/process\.env|os\.environ|ENV\[|getenv|\$\{|<your|CHANGE_ME|xxx|placeholder/i.test(m[0])) continue;
5261
- const lineNum = content.substring(0, m.index).split("\n").length;
5403
+ const lineNum = lineNumberAt(content, m.index);
5262
5404
  findings.push({
5263
5405
  rule: "VC119",
5264
5406
  title: hardcodedOAuthSecret.title,
@@ -5290,7 +5432,7 @@ var missingOAuthState = {
5290
5432
  if (isCommentLine(content, m.index)) continue;
5291
5433
  const surrounding = content.substring(m.index, Math.min(content.length, m.index + 500));
5292
5434
  if (/state\s*[=:]/i.test(surrounding)) continue;
5293
- const lineNum = content.substring(0, m.index).split("\n").length;
5435
+ const lineNum = lineNumberAt(content, m.index);
5294
5436
  findings.push({
5295
5437
  rule: "VC120",
5296
5438
  title: missingOAuthState.title,
@@ -5318,7 +5460,7 @@ var unpinnedGitHubAction = {
5318
5460
  let m;
5319
5461
  while ((m = usesPattern.exec(content)) !== null) {
5320
5462
  if (isCommentLine(content, m.index)) continue;
5321
- const lineNum = content.substring(0, m.index).split("\n").length;
5463
+ const lineNum = lineNumberAt(content, m.index);
5322
5464
  findings.push({
5323
5465
  rule: "VC121",
5324
5466
  title: unpinnedGitHubAction.title,
@@ -5352,7 +5494,7 @@ var deprecatedTLS = {
5352
5494
  let m;
5353
5495
  while ((m = pat.exec(content)) !== null) {
5354
5496
  if (isCommentLine(content, m.index)) continue;
5355
- const lineNum = content.substring(0, m.index).split("\n").length;
5497
+ const lineNum = lineNumberAt(content, m.index);
5356
5498
  findings.push({
5357
5499
  rule: "VC122",
5358
5500
  title: deprecatedTLS.title,
@@ -5388,7 +5530,7 @@ var weakRSAKeySize = {
5388
5530
  let m;
5389
5531
  while ((m = pat.exec(content)) !== null) {
5390
5532
  if (isCommentLine(content, m.index)) continue;
5391
- const lineNum = content.substring(0, m.index).split("\n").length;
5533
+ const lineNum = lineNumberAt(content, m.index);
5392
5534
  findings.push({
5393
5535
  rule: "VC123",
5394
5536
  title: weakRSAKeySize.title,
@@ -5424,7 +5566,7 @@ var ecbModeEncryption = {
5424
5566
  let m;
5425
5567
  while ((m = pat.exec(content)) !== null) {
5426
5568
  if (isCommentLine(content, m.index)) continue;
5427
- const lineNum = content.substring(0, m.index).split("\n").length;
5569
+ const lineNum = lineNumberAt(content, m.index);
5428
5570
  findings.push({
5429
5571
  rule: "VC124",
5430
5572
  title: ecbModeEncryption.title,
@@ -5455,7 +5597,7 @@ var insecurePasswordReset = {
5455
5597
  let m;
5456
5598
  while ((m = weakTokens.exec(content)) !== null) {
5457
5599
  if (isCommentLine(content, m.index)) continue;
5458
- const lineNum = content.substring(0, m.index).split("\n").length;
5600
+ const lineNum = lineNumberAt(content, m.index);
5459
5601
  findings.push({
5460
5602
  rule: "VC125",
5461
5603
  title: insecurePasswordReset.title,
@@ -5472,7 +5614,7 @@ var insecurePasswordReset = {
5472
5614
  if (isCommentLine(content, m.index)) continue;
5473
5615
  const surrounding = content.substring(Math.max(0, m.index - 500), m.index);
5474
5616
  if (!/(?:reset|forgot).*password/i.test(surrounding)) continue;
5475
- const lineNum = content.substring(0, m.index).split("\n").length;
5617
+ const lineNum = lineNumberAt(content, m.index);
5476
5618
  findings.push({
5477
5619
  rule: "VC125",
5478
5620
  title: insecurePasswordReset.title,
@@ -5533,7 +5675,7 @@ var insecureHTTPMethods = {
5533
5675
  if (isCommentLine(content, m.index)) continue;
5534
5676
  const handlerBlock = content.substring(m.index, Math.min(content.length, m.index + 500));
5535
5677
  if (/auth|authenticate|authorize|requireAuth|isAuthenticated|protect|guard|middleware|session|jwt|verify|clerk|getAuth/i.test(handlerBlock)) continue;
5536
- const lineNum = content.substring(0, m.index).split("\n").length;
5678
+ const lineNum = lineNumberAt(content, m.index);
5537
5679
  findings.push({
5538
5680
  rule: "VC127",
5539
5681
  title: insecureHTTPMethods.title,
@@ -5567,7 +5709,7 @@ var httpRequestSmuggling = {
5567
5709
  let m;
5568
5710
  while ((m = pat.exec(content)) !== null) {
5569
5711
  if (isCommentLine(content, m.index)) continue;
5570
- const lineNum = content.substring(0, m.index).split("\n").length;
5712
+ const lineNum = lineNumberAt(content, m.index);
5571
5713
  findings.push({
5572
5714
  rule: "VC128",
5573
5715
  title: httpRequestSmuggling.title,
@@ -5601,7 +5743,7 @@ var unencryptedPII = {
5601
5743
  let m;
5602
5744
  while ((m = pat.exec(content)) !== null) {
5603
5745
  if (isCommentLine(content, m.index)) continue;
5604
- const lineNum = content.substring(0, m.index).split("\n").length;
5746
+ const lineNum = lineNumberAt(content, m.index);
5605
5747
  findings.push({
5606
5748
  rule: "VC129",
5607
5749
  title: unencryptedPII.title,
@@ -5633,7 +5775,7 @@ var missingAuthRateLimit = {
5633
5775
  if (isCommentLine(content, m.index)) continue;
5634
5776
  const surrounding = content.substring(Math.max(0, m.index - 300), Math.min(content.length, m.index + 300));
5635
5777
  if (/rateLimit|rateLimiter|throttle|slowDown|express-rate-limit|rate_limit|RateLimiter|limiter/i.test(surrounding)) continue;
5636
- const lineNum = content.substring(0, m.index).split("\n").length;
5778
+ const lineNum = lineNumberAt(content, m.index);
5637
5779
  findings.push({
5638
5780
  rule: "VC130",
5639
5781
  title: missingAuthRateLimit.title,
@@ -5671,7 +5813,7 @@ var vulnerableDependencies = {
5671
5813
  for (const [pat, message] of vulnerablePackages) {
5672
5814
  let m;
5673
5815
  while ((m = pat.exec(content)) !== null) {
5674
- const lineNum = content.substring(0, m.index).split("\n").length;
5816
+ const lineNum = lineNumberAt(content, m.index);
5675
5817
  findings.push({
5676
5818
  rule: "VC131",
5677
5819
  title: vulnerableDependencies.title,
@@ -5703,7 +5845,7 @@ function secretRuleCheck(content, filePath, pattern, ruleId, title, severity, fi
5703
5845
  const lineStart = content.lastIndexOf("\n", m.index - 1) + 1;
5704
5846
  const lineText = content.substring(lineStart, content.indexOf("\n", m.index));
5705
5847
  if (PLACEHOLDER_RE.test(lineText)) continue;
5706
- const lineNum = content.substring(0, m.index).split("\n").length;
5848
+ const lineNum = lineNumberAt(content, m.index);
5707
5849
  findings.push({
5708
5850
  rule: ruleId,
5709
5851
  title,
@@ -5804,7 +5946,7 @@ var hardcodedGCPServiceAccount = {
5804
5946
  const findings = [];
5805
5947
  const m = content.match(/"type"\s*:\s*"service_account"/);
5806
5948
  if (m && m.index !== void 0) {
5807
- const lineNum = content.substring(0, m.index).split("\n").length;
5949
+ const lineNum = lineNumberAt(content, m.index);
5808
5950
  findings.push({
5809
5951
  rule: "VC136",
5810
5952
  title: this.title,
@@ -5910,7 +6052,7 @@ var hardcodedDatadogKey = {
5910
6052
  const lineStart = content.lastIndexOf("\n", m.index - 1) + 1;
5911
6053
  const lineText = content.substring(lineStart, content.indexOf("\n", m.index));
5912
6054
  if (PLACEHOLDER_RE.test(lineText)) continue;
5913
- const lineNum = content.substring(0, m.index).split("\n").length;
6055
+ const lineNum = lineNumberAt(content, m.index);
5914
6056
  findings.push({
5915
6057
  rule: "VC141",
5916
6058
  title: this.title,
@@ -5944,7 +6086,7 @@ var hardcodedVercelToken = {
5944
6086
  const lineStart = content.lastIndexOf("\n", m.index - 1) + 1;
5945
6087
  const lineText = content.substring(lineStart, content.indexOf("\n", m.index));
5946
6088
  if (PLACEHOLDER_RE.test(lineText)) continue;
5947
- const lineNum = content.substring(0, m.index).split("\n").length;
6089
+ const lineNum = lineNumberAt(content, m.index);
5948
6090
  findings.push({
5949
6091
  rule: "VC142",
5950
6092
  title: this.title,
@@ -5978,7 +6120,7 @@ var hardcodedSupabaseServiceRole = {
5978
6120
  const lineStart = content.lastIndexOf("\n", m.index - 1) + 1;
5979
6121
  const lineText = content.substring(lineStart, content.indexOf("\n", m.index));
5980
6122
  if (PLACEHOLDER_RE.test(lineText)) continue;
5981
- const lineNum = content.substring(0, m.index).split("\n").length;
6123
+ const lineNum = lineNumberAt(content, m.index);
5982
6124
  findings.push({
5983
6125
  rule: "VC143",
5984
6126
  title: this.title,
@@ -6042,7 +6184,7 @@ function contextSecretRuleCheck(content, filePath, pattern, ruleId, title, sever
6042
6184
  const lineStart = content.lastIndexOf("\n", m.index - 1) + 1;
6043
6185
  const lineText = content.substring(lineStart, content.indexOf("\n", m.index));
6044
6186
  if (PLACEHOLDER_RE.test(lineText)) continue;
6045
- const lineNum = content.substring(0, m.index).split("\n").length;
6187
+ const lineNum = lineNumberAt(content, m.index);
6046
6188
  findings.push({
6047
6189
  rule: ruleId,
6048
6190
  title,
@@ -6522,7 +6664,7 @@ var ghaPullRequestTargetCheckout = {
6522
6664
  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;
6523
6665
  let m;
6524
6666
  while ((m = checkoutPattern.exec(content)) !== null) {
6525
- const lineNum = content.substring(0, m.index).split("\n").length;
6667
+ const lineNum = lineNumberAt(content, m.index);
6526
6668
  findings.push({
6527
6669
  rule: "VC184",
6528
6670
  title: ghaPullRequestTargetCheckout.title,
@@ -6606,7 +6748,7 @@ var ghaThirdPartyActionWithSecrets = {
6606
6748
  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;
6607
6749
  let m;
6608
6750
  while ((m = pattern.exec(content)) !== null) {
6609
- const lineNum = content.substring(0, m.index).split("\n").length;
6751
+ const lineNum = lineNumberAt(content, m.index);
6610
6752
  findings.push({
6611
6753
  rule: "VC187",
6612
6754
  title: ghaThirdPartyActionWithSecrets.title,
@@ -6771,7 +6913,7 @@ var pyRequestsVerifyFalse = {
6771
6913
  let m;
6772
6914
  while ((m = pattern.exec(content)) !== null) {
6773
6915
  if (isCommentLine(content, m.index)) continue;
6774
- const lineNum = content.substring(0, m.index).split("\n").length;
6916
+ const lineNum = lineNumberAt(content, m.index);
6775
6917
  findings.push({
6776
6918
  rule: "VC191",
6777
6919
  title: pyRequestsVerifyFalse.title,
@@ -6802,7 +6944,7 @@ var pyJinja2AutoescapeOff = {
6802
6944
  let m;
6803
6945
  while ((m = pattern.exec(content)) !== null) {
6804
6946
  if (isCommentLine(content, m.index)) continue;
6805
- const lineNum = content.substring(0, m.index).split("\n").length;
6947
+ const lineNum = lineNumberAt(content, m.index);
6806
6948
  findings.push({
6807
6949
  rule: "VC192",
6808
6950
  title: pyJinja2AutoescapeOff.title,
@@ -6831,7 +6973,7 @@ var pyTempfileMktemp = {
6831
6973
  let m;
6832
6974
  while ((m = pattern.exec(content)) !== null) {
6833
6975
  if (isCommentLine(content, m.index)) continue;
6834
- const lineNum = content.substring(0, m.index).split("\n").length;
6976
+ const lineNum = lineNumberAt(content, m.index);
6835
6977
  findings.push({
6836
6978
  rule: "VC193",
6837
6979
  title: pyTempfileMktemp.title,
@@ -6873,7 +7015,7 @@ var pyDjangoMarkSafe = {
6873
7015
  let m;
6874
7016
  while ((m = pattern.exec(content)) !== null) {
6875
7017
  if (isCommentLine(content, m.index)) continue;
6876
- const lineNum = content.substring(0, m.index).split("\n").length;
7018
+ const lineNum = lineNumberAt(content, m.index);
6877
7019
  if (seenLines.has(lineNum)) continue;
6878
7020
  seenLines.add(lineNum);
6879
7021
  findings.push({
@@ -6910,7 +7052,7 @@ var pyParamikoAutoAdd = {
6910
7052
  let m;
6911
7053
  while ((m = pattern.exec(content)) !== null) {
6912
7054
  if (isCommentLine(content, m.index)) continue;
6913
- const lineNum = content.substring(0, m.index).split("\n").length;
7055
+ const lineNum = lineNumberAt(content, m.index);
6914
7056
  if (seenLines.has(lineNum)) continue;
6915
7057
  seenLines.add(lineNum);
6916
7058
  findings.push({
@@ -6943,7 +7085,7 @@ var pyDjangoAllowedHostsWildcard = {
6943
7085
  let m;
6944
7086
  while ((m = pattern.exec(content)) !== null) {
6945
7087
  if (isCommentLine(content, m.index)) continue;
6946
- const lineNum = content.substring(0, m.index).split("\n").length;
7088
+ const lineNum = lineNumberAt(content, m.index);
6947
7089
  findings.push({
6948
7090
  rule: "VC196",
6949
7091
  title: pyDjangoAllowedHostsWildcard.title,
@@ -6981,7 +7123,7 @@ var pyJWTDecodeWeakConfig = {
6981
7123
  if (isCommentLine(content, m.index)) continue;
6982
7124
  const args = m[1];
6983
7125
  if (/\balgorithms\s*=/.test(args)) continue;
6984
- const lineNum = content.substring(0, m.index).split("\n").length;
7126
+ const lineNum = lineNumberAt(content, m.index);
6985
7127
  if (seenLines.has(lineNum)) continue;
6986
7128
  seenLines.add(lineNum);
6987
7129
  findings.push({
@@ -7032,7 +7174,7 @@ var llmPromptInjection = {
7032
7174
  let m;
7033
7175
  while ((m = pattern.exec(content)) !== null) {
7034
7176
  if (isCommentLine(content, m.index)) continue;
7035
- const lineNum = content.substring(0, m.index).split("\n").length;
7177
+ const lineNum = lineNumberAt(content, m.index);
7036
7178
  if (seenLines.has(lineNum)) continue;
7037
7179
  seenLines.add(lineNum);
7038
7180
  findings.push({
@@ -7071,7 +7213,7 @@ var llmSystemPromptInjection = {
7071
7213
  let m;
7072
7214
  while ((m = pattern.exec(content)) !== null) {
7073
7215
  if (isCommentLine(content, m.index)) continue;
7074
- const lineNum = content.substring(0, m.index).split("\n").length;
7216
+ const lineNum = lineNumberAt(content, m.index);
7075
7217
  findings.push({
7076
7218
  rule: "VC199",
7077
7219
  title: llmSystemPromptInjection.title,
@@ -7113,7 +7255,7 @@ var llmOutputAsHTML = {
7113
7255
  let m;
7114
7256
  while ((m = pattern.exec(content)) !== null) {
7115
7257
  if (isCommentLine(content, m.index)) continue;
7116
- const lineNum = content.substring(0, m.index).split("\n").length;
7258
+ const lineNum = lineNumberAt(content, m.index);
7117
7259
  if (seenLines.has(lineNum)) continue;
7118
7260
  seenLines.add(lineNum);
7119
7261
  findings.push({
@@ -7150,7 +7292,7 @@ var vectorStoreQueryNoUserFilter = {
7150
7292
  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;
7151
7293
  if (/\bnamespace\s*[:=]\s*[`"']?[^,)`"']*(?:user|tenant|org|owner|account|customer|workspace)/i.test(args)) continue;
7152
7294
  if (/\bfilter\s*[:=][\s\S]*?(?:\buser|\btenant|\borg|\bowner|\baccount|\bcustomer|\bworkspace)/i.test(args)) continue;
7153
- const lineNum = content.substring(0, m.index).split("\n").length;
7295
+ const lineNum = lineNumberAt(content, m.index);
7154
7296
  findings.push({
7155
7297
  rule: "VC201",
7156
7298
  title: vectorStoreQueryNoUserFilter.title,
@@ -7187,7 +7329,7 @@ var vectorStoreUpsertNoMetadata = {
7187
7329
  if (/\bnamespace\s*[:=]\s*[`"']?[^,)`"']*(?:user|tenant|org)/i.test(args)) {
7188
7330
  continue;
7189
7331
  }
7190
- const lineNum = content.substring(0, m.index).split("\n").length;
7332
+ const lineNum = lineNumberAt(content, m.index);
7191
7333
  findings.push({
7192
7334
  rule: "VC202",
7193
7335
  title: vectorStoreUpsertNoMetadata.title,
@@ -7241,7 +7383,7 @@ var llmCallNoMaxTokens = {
7241
7383
  if (depth !== 0) continue;
7242
7384
  const args = content.substring(openIdx + 1, i).replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/[^\n]*/g, "").replace(/#[^\n]*/g, "");
7243
7385
  if (TOKEN_KW_RE.test(args)) continue;
7244
- const lineNum = content.substring(0, m.index).split("\n").length;
7386
+ const lineNum = lineNumberAt(content, m.index);
7245
7387
  findings.push({
7246
7388
  rule: "VC203",
7247
7389
  title: llmCallNoMaxTokens.title,
@@ -7281,7 +7423,7 @@ var graphqlNoDepthLimit = {
7281
7423
  let m;
7282
7424
  while ((m = anchorRe.exec(content)) !== null) {
7283
7425
  if (isCommentLine(content, m.index)) continue;
7284
- const lineNum = content.substring(0, m.index).split("\n").length;
7426
+ const lineNum = lineNumberAt(content, m.index);
7285
7427
  findings.push({
7286
7428
  rule: "VC204",
7287
7429
  title: graphqlNoDepthLimit.title,
@@ -7315,7 +7457,7 @@ var graphqlNoComplexityLimit = {
7315
7457
  let m;
7316
7458
  while ((m = anchorRe.exec(content)) !== null) {
7317
7459
  if (isCommentLine(content, m.index)) continue;
7318
- const lineNum = content.substring(0, m.index).split("\n").length;
7460
+ const lineNum = lineNumberAt(content, m.index);
7319
7461
  findings.push({
7320
7462
  rule: "VC205",
7321
7463
  title: graphqlNoComplexityLimit.title,
@@ -7345,7 +7487,7 @@ var graphqlCSRFDisabled = {
7345
7487
  let m;
7346
7488
  while ((m = pattern.exec(content)) !== null) {
7347
7489
  if (isCommentLine(content, m.index)) continue;
7348
- const lineNum = content.substring(0, m.index).split("\n").length;
7490
+ const lineNum = lineNumberAt(content, m.index);
7349
7491
  findings.push({
7350
7492
  rule: "VC206",
7351
7493
  title: graphqlCSRFDisabled.title,
@@ -7441,7 +7583,7 @@ var webhookMissingIdempotency = {
7441
7583
  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 [];
7442
7584
  const m = content.match(/constructEvent|new\s+Webhook\s*\(|verifyHeader/i);
7443
7585
  if (!m || m.index === void 0) return [];
7444
- const line = content.substring(0, m.index).split("\n").length;
7586
+ const line = lineNumberAt(content, m.index);
7445
7587
  return filterSilenced([{
7446
7588
  rule: "VC209",
7447
7589
  title: webhookMissingIdempotency.title,
@@ -7468,7 +7610,7 @@ var middlewareMatcherExcludesApi = {
7468
7610
  if (!/\(\?!\s*[^)]*\bapi\b/.test(content)) return [];
7469
7611
  const m = content.match(/matcher\s*:/);
7470
7612
  if (!m || m.index === void 0) return [];
7471
- const line = content.substring(0, m.index).split("\n").length;
7613
+ const line = lineNumberAt(content, m.index);
7472
7614
  return filterSilenced([{
7473
7615
  rule: "VC210",
7474
7616
  title: middlewareMatcherExcludesApi.title,
@@ -7506,7 +7648,7 @@ var secretInURLParam = {
7506
7648
  if (isCommentLine(content, m.index)) continue;
7507
7649
  if (isInsideFixMessage(content, m.index)) continue;
7508
7650
  if (isInlineSilenced(content, m.index, "VC146")) continue;
7509
- const lineNum = content.substring(0, m.index).split("\n").length;
7651
+ const lineNum = lineNumberAt(content, m.index);
7510
7652
  findings.push({
7511
7653
  rule: "VC146",
7512
7654
  title: this.title,
@@ -7537,7 +7679,7 @@ var secretLoggedToConsole = {
7537
7679
  while ((m = pattern.exec(content)) !== null) {
7538
7680
  if (isCommentLine(content, m.index)) continue;
7539
7681
  if (isInsideFixMessage(content, m.index)) continue;
7540
- const lineNum = content.substring(0, m.index).split("\n").length;
7682
+ const lineNum = lineNumberAt(content, m.index);
7541
7683
  findings.push({
7542
7684
  rule: "VC147",
7543
7685
  title: this.title,
@@ -7567,7 +7709,7 @@ var secretInErrorResponse = {
7567
7709
  while ((m = pattern.exec(content)) !== null) {
7568
7710
  if (isCommentLine(content, m.index)) continue;
7569
7711
  if (isInsideFixMessage(content, m.index)) continue;
7570
- const lineNum = content.substring(0, m.index).split("\n").length;
7712
+ const lineNum = lineNumberAt(content, m.index);
7571
7713
  findings.push({
7572
7714
  rule: "VC148",
7573
7715
  title: this.title,
@@ -7606,7 +7748,7 @@ var secretInBundleConfig = {
7606
7748
  while ((m = re.exec(content)) !== null) {
7607
7749
  if (isCommentLine(content, m.index)) continue;
7608
7750
  if (isInsideFixMessage(content, m.index)) continue;
7609
- const lineNum = content.substring(0, m.index).split("\n").length;
7751
+ const lineNum = lineNumberAt(content, m.index);
7610
7752
  findings.push({
7611
7753
  rule: "VC149",
7612
7754
  title: this.title,
@@ -7644,7 +7786,7 @@ var secretInHTMLAttribute = {
7644
7786
  while ((m = re.exec(content)) !== null) {
7645
7787
  if (isCommentLine(content, m.index)) continue;
7646
7788
  if (isInsideFixMessage(content, m.index)) continue;
7647
- const lineNum = content.substring(0, m.index).split("\n").length;
7789
+ const lineNum = lineNumberAt(content, m.index);
7648
7790
  findings.push({
7649
7791
  rule: "VC150",
7650
7792
  title: this.title,
@@ -7671,9 +7813,9 @@ var secretInCLIArgument = {
7671
7813
  if (!filePath.match(/\.(js|ts|jsx|tsx|py|rb)$/)) return [];
7672
7814
  const findings = [];
7673
7815
  const patterns = [
7674
- /(?:exec|execSync|spawn|spawnSync|child_process)\s*\([^)]*\$\{[^}]*(?:api[_-]?key|secret|token|password|credentials)/gi,
7675
- /(?:exec|execSync|spawn|spawnSync|child_process)\s*\([^)]*["']\s*\+\s*(?:api[_-]?key|secret|token|password|credentials)/gi,
7676
- /(?:subprocess|os\.system|os\.popen)\s*\([^)]*\{[^}]*(?:api[_-]?key|secret|token|password|credentials)/gi
7816
+ /(?:exec|execSync|spawn|spawnSync|child_process)\s*\([^)]{0,500}\$\{[^}]{0,200}(?:api[_-]?key|secret|token|password|credentials)/gi,
7817
+ /(?:exec|execSync|spawn|spawnSync|child_process)\s*\([^)]{0,500}["']\s*\+\s*(?:api[_-]?key|secret|token|password|credentials)/gi,
7818
+ /(?:subprocess|os\.system|os\.popen)\s*\([^)]{0,500}\{[^}]{0,200}(?:api[_-]?key|secret|token|password|credentials)/gi
7677
7819
  ];
7678
7820
  for (const p of patterns) {
7679
7821
  let m;
@@ -7681,7 +7823,7 @@ var secretInCLIArgument = {
7681
7823
  while ((m = re.exec(content)) !== null) {
7682
7824
  if (isCommentLine(content, m.index)) continue;
7683
7825
  if (isInsideFixMessage(content, m.index)) continue;
7684
- const lineNum = content.substring(0, m.index).split("\n").length;
7826
+ const lineNum = lineNumberAt(content, m.index);
7685
7827
  findings.push({
7686
7828
  rule: "VC151",
7687
7829
  title: this.title,
@@ -7727,7 +7869,7 @@ var webhookSignatureVerification = {
7727
7869
  if (svc.verify.test(content)) continue;
7728
7870
  const m = content.match(/export\s+(?:async\s+)?function\s+POST/);
7729
7871
  if (!m || m.index === void 0) continue;
7730
- const lineNum = content.substring(0, m.index).split("\n").length;
7872
+ const lineNum = lineNumberAt(content, m.index);
7731
7873
  findings.push({
7732
7874
  rule: "VC152",
7733
7875
  title: `${svc.name} Webhook Missing Signature Verification`,
@@ -7794,7 +7936,7 @@ var missingRequestValidation = {
7794
7936
  if (/if\s*\(\s*!(?:body|parsed|data|payload)\.|throw.*(?:missing|invalid|required)/i.test(content)) return [];
7795
7937
  const m = content.match(/export\s+(?:async\s+)?function\s+(?:POST|PUT|PATCH)/);
7796
7938
  if (!m || m.index === void 0) return [];
7797
- const lineNum = content.substring(0, m.index).split("\n").length;
7939
+ const lineNum = lineNumberAt(content, m.index);
7798
7940
  return [{
7799
7941
  rule: "VC154",
7800
7942
  title: this.title,
@@ -7821,7 +7963,7 @@ var missingAIRateLimit = {
7821
7963
  if (/rateLimit|rateLimiter|throttle|checkRateLimit|limiter|slowDown|express-rate-limit/i.test(content)) return [];
7822
7964
  const m = content.match(/export\s+(?:async\s+)?function\s+(?:POST|GET)/i) || content.match(/\.(post|get)\s*\(/i);
7823
7965
  if (!m || m.index === void 0) return [];
7824
- const lineNum = content.substring(0, m.index).split("\n").length;
7966
+ const lineNum = lineNumberAt(content, m.index);
7825
7967
  return [{
7826
7968
  rule: "VC155",
7827
7969
  title: this.title,
@@ -7850,7 +7992,7 @@ var missingPagination = {
7850
7992
  if (/findUnique|findFirst|findById|\.findOne|WHERE.*id\s*=/i.test(content)) return [];
7851
7993
  const m = content.match(/export\s+(?:async\s+)?function\s+GET/);
7852
7994
  if (!m || m.index === void 0) return [];
7853
- const lineNum = content.substring(0, m.index).split("\n").length;
7995
+ const lineNum = lineNumberAt(content, m.index);
7854
7996
  return [{
7855
7997
  rule: "VC156",
7856
7998
  title: this.title,
@@ -7911,7 +8053,7 @@ var insecureDirectObjectReference = {
7911
8053
  if (/requireUser|requireUserForApi|getServerSession|auth\(\)/i.test(content)) return [];
7912
8054
  const m = content.match(/params\.id|params\.(?:slug|uuid)|req\.params\./);
7913
8055
  if (!m || m.index === void 0) return [];
7914
- const lineNum = content.substring(0, m.index).split("\n").length;
8056
+ const lineNum = lineNumberAt(content, m.index);
7915
8057
  return [{
7916
8058
  rule: "VC158",
7917
8059
  title: this.title,
@@ -8217,6 +8359,17 @@ function runCustomRules(content, filePath, disabledRules = [], tier = "free", ex
8217
8359
  return findings;
8218
8360
  }
8219
8361
  if (/pro-rules-bundle|\.bundle\./i.test(filePath)) return findings;
8362
+ let maxLineLen = 0;
8363
+ {
8364
+ let lineStart = 0;
8365
+ for (let i = 0; i <= content.length; i++) {
8366
+ if (i === content.length || content.charCodeAt(i) === 10) {
8367
+ if (i - lineStart > maxLineLen) maxLineLen = i - lineStart;
8368
+ lineStart = i + 1;
8369
+ }
8370
+ }
8371
+ }
8372
+ if (maxLineLen > 5e4) return findings;
8220
8373
  if (/onboarding|demo-data|example-vulnerable|code-sample/i.test(filePath)) return findings;
8221
8374
  const ruleset = tier === "pro" && extraRules.length > 0 ? [...freeRules, ...extraRules] : freeRules;
8222
8375
  for (const rule of ruleset) {