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.cjs CHANGED
@@ -264,15 +264,50 @@ __export(index_exports, {
264
264
  module.exports = __toCommonJS(index_exports);
265
265
 
266
266
  // src/snippet.ts
267
- function getSnippet(content, line, contextLines = 2) {
267
+ var cacheContent = null;
268
+ var cacheLines = [];
269
+ var cacheOffsets = [];
270
+ function lineIndex(content) {
271
+ if (content === cacheContent) return { lines: cacheLines, offsets: cacheOffsets };
268
272
  const lines = content.split("\n");
273
+ const offsets = new Array(lines.length);
274
+ let off = 0;
275
+ for (let i = 0; i < lines.length; i++) {
276
+ offsets[i] = off;
277
+ off += lines[i].length + 1;
278
+ }
279
+ cacheContent = content;
280
+ cacheLines = lines;
281
+ cacheOffsets = offsets;
282
+ return { lines, offsets };
283
+ }
284
+ function lineNumberAt(content, index) {
285
+ const { offsets } = lineIndex(content);
286
+ let lo = 0;
287
+ let hi = offsets.length - 1;
288
+ let ans = 0;
289
+ while (lo <= hi) {
290
+ const mid = lo + hi >> 1;
291
+ if (offsets[mid] <= index) {
292
+ ans = mid;
293
+ lo = mid + 1;
294
+ } else {
295
+ hi = mid - 1;
296
+ }
297
+ }
298
+ return ans + 1;
299
+ }
300
+ function getSnippet(content, line, contextLines = 2) {
301
+ const { lines } = lineIndex(content);
269
302
  const start = Math.max(0, line - 1 - contextLines);
270
303
  const end = Math.min(lines.length, line + contextLines);
271
- return lines.slice(start, end).map((l, i) => {
272
- const lineNum = start + i + 1;
304
+ const out = [];
305
+ for (let i = start; i < end; i++) {
306
+ const lineNum = i + 1;
273
307
  const marker = lineNum === line ? ">" : " ";
274
- return `${marker} ${lineNum.toString().padStart(4)} | ${l}`;
275
- }).join("\n");
308
+ out.push(`${marker} ${lineNum.toString().padStart(4)} | ${lines[i]}`);
309
+ }
310
+ return out.join("\n");
276
311
  }
277
312
 
278
313
  // src/rule-impacts.ts
@@ -1080,7 +1115,7 @@ var SERVER_SIDE_PATH_RE = new RegExp(
1080
1115
  ].join("|"),
1081
1116
  "i"
1082
1117
  );
1083
- var SERVER_SIDE_CONTENT_RE = /\b(?:req|request)\.(?:body|query|params|headers|cookies)\b|\b(?:app|router)\.(?:get|post|put|patch|delete|use|all)\s*\(|\bctx\.request\b|\bevent\.(?:body|queryStringParameters|pathParameters)\b|\(\s*req\s*,\s*res\b/;
1118
+ var SERVER_SIDE_CONTENT_RE = /\b(?:req|request)\.(?:body|query|params|headers|cookies)\b|\b(?:app|router)\.(?:get|post|put|patch|delete|use|all)\s*\(|\bctx\.request\b|\bevent\.(?:body|queryStringParameters|pathParameters)\b|\(\s*req\s*,\s*res\b|(?:import|require)\b[^;\n]{0,200}['"]express['"]|\bNextFunction\b/;
1084
1119
  function isServerSideFile(filePath, content) {
1085
1120
  if (isTestFile(filePath)) return false;
1086
1121
  if (SERVER_SIDE_PATH_RE.test(filePath)) return true;
@@ -1127,13 +1162,23 @@ function filterSilenced(matches, content, ruleId) {
1127
1162
  }
1128
1163
  function findMatches(content, pattern, rule, filePath, fixTemplate) {
1129
1164
  const matches = [];
1130
- const lines = content.split("\n");
1131
1165
  let m;
1132
1166
  const re = new RegExp(pattern.source, pattern.flags.includes("g") ? pattern.flags : `${pattern.flags}g`);
1167
+ let scanned = 0;
1168
+ let lineNum = 1;
1169
+ let lastMatchIndex = -1;
1170
+ const MAX_MATCHES = 500;
1133
1171
  while ((m = re.exec(content)) !== null) {
1172
+ if (m.index === lastMatchIndex && m[0].length === 0) {
1173
+ re.lastIndex++;
1174
+ continue;
1175
+ }
1176
+ lastMatchIndex = m.index;
1134
1177
  if (isCommentLine(content, m.index)) continue;
1135
1178
  if (isInsideFixMessage(content, m.index)) continue;
1136
- const lineNum = content.substring(0, m.index).split("\n").length;
1179
+ for (; scanned < m.index; scanned++) {
1180
+ if (content.charCodeAt(scanned) === 10) lineNum++;
1181
+ }
1137
1182
  matches.push({
1138
1183
  rule: rule.id,
1139
1184
  title: rule.title,
@@ -1144,6 +1189,7 @@ function findMatches(content, pattern, rule, filePath, fixTemplate) {
1144
1189
  snippet: getSnippet(content, lineNum),
1145
1190
  fix: fixTemplate?.(m)
1146
1191
  });
1192
+ if (matches.length >= MAX_MATCHES) break;
1147
1193
  }
1148
1194
  return matches;
1149
1195
  }
@@ -1159,10 +1205,26 @@ function astMatch(content, filePath, line, rule, fix) {
1159
1205
  fix
1160
1206
  };
1161
1207
  }
1208
+ var parseCacheContent = null;
1209
+ var parseCachePath = null;
1210
+ var parseCacheResult = null;
1211
+ var parseCacheValid = false;
1162
1212
  function tryParse(content, filePath) {
1163
- const parsed = parseFile(content, filePath);
1164
- if (!parsed) return null;
1165
- return { parsed, taint: buildTaintMap(parsed) };
1213
+ if (parseCacheValid && content === parseCacheContent && filePath === parseCachePath) {
1214
+ return parseCacheResult;
1215
+ }
1216
+ let result = null;
1217
+ try {
1218
+ const parsed = parseFile(content, filePath);
1219
+ result = parsed ? { parsed, taint: buildTaintMap(parsed) } : null;
1220
+ } catch {
1221
+ result = null;
1222
+ }
1223
+ parseCacheResult = result;
1224
+ parseCacheContent = content;
1225
+ parseCachePath = filePath;
1226
+ parseCacheValid = true;
1227
+ return result;
1166
1228
  }
1167
1229
  var hardcodedSecrets = {
1168
1230
  id: "VC001",
@@ -1527,9 +1589,10 @@ var xssVulnerability = {
1527
1589
  category: "Injection",
1528
1590
  description: "Rendering user input without sanitization allows attackers to inject malicious scripts.",
1529
1591
  check(content, filePath) {
1592
+ const hasSanitizerBypass = /bypassSecurityTrust(?:Html|Script|Style|Url|ResourceUrl)\s*\(/.test(content);
1530
1593
  if (/(?:sanitize|purify|escape|xss)/i.test(filePath)) return [];
1531
- if (/DOMPurify\.sanitize|sanitizeHtml|xss\(|escapeHtml/i.test(content)) return [];
1532
- if (/(?:import|require)\s*\(?.*(?:DOMPurify|dompurify|sanitize|sanitizer)/i.test(content)) return [];
1594
+ if (!hasSanitizerBypass && /DOMPurify\.sanitize|sanitizeHtml|xss\(|escapeHtml/i.test(content)) return [];
1595
+ if (!hasSanitizerBypass && /(?:import|require)\s*\(?[^\n]{0,200}(?:DOMPurify|dompurify|sanitize|sanitizer)/i.test(content)) return [];
1533
1596
  const patterns = [
1534
1597
  // React dangerouslySetInnerHTML
1535
1598
  /dangerouslySetInnerHTML\s*=\s*\{\s*\{\s*__html\s*:/g,
@@ -1540,7 +1603,12 @@ var xssVulnerability = {
1540
1603
  // v-html in Vue
1541
1604
  /v-html\s*=/g,
1542
1605
  // {@html} in Svelte
1543
- /\{@html\s/g
1606
+ /\{@html\s/g,
1607
+ // Angular DomSanitizer escape hatches — bypassSecurityTrustHtml /
1608
+ // bypassSecurityTrustScript / ...Url / ...ResourceUrl / ...Style. These
1609
+ // mark a value as trusted and skip Angular's built-in sanitization;
1610
+ // passing user input through one is the canonical Angular DOM-XSS sink.
1611
+ /bypassSecurityTrust(?:Html|Script|Style|Url|ResourceUrl)\s*\(/g
1544
1612
  ];
1545
1613
  const allLines = content.split("\n");
1546
1614
  const matches = [];
@@ -1553,9 +1621,9 @@ var xssVulnerability = {
1553
1621
  () => "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."
1554
1622
  );
1555
1623
  for (const m of raw) {
1556
- const lineText = allLines[m.line - 1] || "";
1624
+ const lineText = (allLines[m.line - 1] || "").slice(0, 4e3);
1557
1625
  if (/\.innerHTML\s*=\s*['"]/.test(lineText) && !/\$\{/.test(lineText)) continue;
1558
- if (/\.innerHTML\s*=\s*['"][^'"]*['"]\s*$/.test(lineText)) continue;
1626
+ if (/\.innerHTML\s*=\s*['"][^'"]{0,2000}['"]\s*$/.test(lineText)) continue;
1559
1627
  if (/dangerouslySetInnerHTML/.test(lineText)) {
1560
1628
  const windowText = allLines.slice(m.line - 1, m.line + 2).join("\n");
1561
1629
  if (/JSON\.stringify/i.test(windowText)) continue;
@@ -1806,7 +1874,7 @@ var evalUsage = {
1806
1874
  /new\s+Function\s*\(\s*(?!["'`])/g
1807
1875
  ];
1808
1876
  const hasEvalInString = /["'`]eval(?:-source-map|["'`])/i.test(content);
1809
- if (hasEvalInString && !/\beval\s*\([^)]*(?:req\.|body\.|input|params|user|data)/i.test(content)) return [];
1877
+ if (hasEvalInString && !/\beval\s*\([^)]{0,500}(?:req\.|body\.|input|params|user|data)/i.test(content)) return [];
1810
1878
  const matches = [];
1811
1879
  for (const p of patterns) {
1812
1880
  const rawMatches = findMatches(
@@ -1847,7 +1915,7 @@ var unvalidatedRedirect = {
1847
1915
  const isPureLiteral = /^["'`][^"'`]*["'`]$/.test(value);
1848
1916
  if (!hasInterpolation && isPureLiteral) continue;
1849
1917
  if (isInlineSilenced(content, m.index, "VC016")) continue;
1850
- const line = content.substring(0, m.index).split("\n").length;
1918
+ const line = lineNumberAt(content, m.index);
1851
1919
  matches.push({
1852
1920
  rule: "VC016",
1853
1921
  title: unvalidatedRedirect.title,
@@ -2070,11 +2138,11 @@ var prototypePollution = {
2070
2138
  ];
2071
2139
  const hasValidation = /schema|validate|sanitize|whitelist|allowedKeys|pick\(|Object\.freeze|zod|yup|joi|ajv/i.test(content);
2072
2140
  if (!hasValidation) {
2073
- const hasUnsafeMerge = /Object\.assign\s*\([^)]*JSON\.parse|\.\.\.JSON\.parse|\{.*\.\.\.(?:stored|saved|cached|parsed|data)/i.test(content);
2141
+ const hasUnsafeMerge = /Object\.assign\s*\([^)]{0,500}JSON\.parse|\.\.\.JSON\.parse|\{[^\n]{0,300}\.\.\.(?:stored|saved|cached|parsed|data)/i.test(content);
2074
2142
  if (hasUnsafeMerge) {
2075
2143
  matches.push(...findMatches(
2076
2144
  content,
2077
- /Object\.assign\s*\([^)]*JSON\.parse|\.\.\.JSON\.parse/g,
2145
+ /Object\.assign\s*\([^)]{0,500}JSON\.parse|\.\.\.JSON\.parse/g,
2078
2146
  prototypePollution,
2079
2147
  filePath,
2080
2148
  () => "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."
@@ -3620,7 +3688,7 @@ var dangerousInnerHTML = {
3620
3688
  if (/^[A-Z][A-Z0-9_]+$/.test(value)) continue;
3621
3689
  if (/^["'`][^$]*["'`]$/.test(value)) continue;
3622
3690
  if (/JSON\.stringify/i.test(value)) continue;
3623
- const lineNum = content.substring(0, m.index).split("\n").length;
3691
+ const lineNum = lineNumberAt(content, m.index);
3624
3692
  findings.push({
3625
3693
  rule: "VC063",
3626
3694
  title: dangerousInnerHTML.title,
@@ -4283,17 +4351,37 @@ var ssti = {
4283
4351
  (call, line) => {
4284
4352
  const first = call.arguments[0];
4285
4353
  if (!first || first.type === "SpreadElement") return;
4286
- if (!taint.isTainted(first)) return;
4287
- if (matches.some((m) => m.line === line)) return;
4288
- matches.push(
4289
- astMatch(
4290
- content,
4291
- filePath,
4292
- line,
4293
- ssti,
4294
- "Compile templates from a trusted, static source (a file path or a constant string). Pass user data only as context values."
4295
- )
4296
- );
4354
+ if (taint.isTainted(first)) {
4355
+ if (matches.some((m) => m.line === line)) return;
4356
+ matches.push(
4357
+ astMatch(
4358
+ content,
4359
+ filePath,
4360
+ line,
4361
+ ssti,
4362
+ "Compile templates from a trusted, static source (a file path or a constant string). Pass user data only as context values."
4363
+ )
4364
+ );
4365
+ return;
4366
+ }
4367
+ const opts = call.arguments[1];
4368
+ if (opts && opts.type === "ObjectExpression") {
4369
+ const hasTaintedSpread = opts.properties.some(
4370
+ (p) => p.type === "SpreadElement" && taint.isTainted(p.argument)
4371
+ );
4372
+ if (hasTaintedSpread) {
4373
+ if (matches.some((m) => m.line === line)) return;
4374
+ matches.push(
4375
+ astMatch(
4376
+ content,
4377
+ filePath,
4378
+ line,
4379
+ ssti,
4380
+ "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 })."
4381
+ )
4382
+ );
4383
+ }
4384
+ }
4297
4385
  }
4298
4386
  );
4299
4387
  return filterSilenced(matches, content, "VC082");
@@ -4342,7 +4430,7 @@ var missingSRI = {
4342
4430
  const re = new RegExp(scriptPattern.source, scriptPattern.flags);
4343
4431
  while ((m = re.exec(content)) !== null) {
4344
4432
  if (!m[0].includes("integrity")) {
4345
- const lineNum = content.substring(0, m.index).split("\n").length;
4433
+ const lineNum = lineNumberAt(content, m.index);
4346
4434
  matches.push({
4347
4435
  rule: "VC084",
4348
4436
  title: missingSRI.title,
@@ -4556,16 +4644,61 @@ var unprotectedDownload = {
4556
4644
  if (!/(?:download|sendFile|send_file)/i.test(content)) return [];
4557
4645
  if (!isServerSideFile(filePath, content)) return [];
4558
4646
  const matches = [];
4559
- if (/(?:sendFile|download|send_file)\s*\([^)]*(?:req\.|params\.|query\.|body\.)/i.test(content)) {
4560
- const hasValidation = /path\.resolve|path\.normalize|path\.join.*__dirname|realpath|includes\s*\(\s*["']\.\./i.test(content);
4561
- if (!hasValidation) {
4562
- matches.push(...findMatches(
4563
- content,
4564
- /(?:sendFile|download|send_file)\s*\(/gi,
4565
- unprotectedDownload,
4566
- filePath,
4567
- () => "Validate file paths: const safePath = path.resolve(DOWNLOAD_DIR, filename); if (!safePath.startsWith(DOWNLOAD_DIR)) throw new Error('Invalid path');"
4568
- ));
4647
+ const hasContainmentCheck = /\.startsWith\s*\(/.test(content) || /realpath/i.test(content) || /includes\s*\(\s*["']\.\./.test(content);
4648
+ if (/(?:sendFile|download|send_file)\s*\([^)]*(?:req\.|params\.|query\.|body\.)/i.test(content) && !hasContainmentCheck) {
4649
+ matches.push(...findMatches(
4650
+ content,
4651
+ /(?:sendFile|download|send_file)\s*\(/gi,
4652
+ unprotectedDownload,
4653
+ filePath,
4654
+ () => "Validate file paths: const safePath = path.resolve(DOWNLOAD_DIR, filename); if (!safePath.startsWith(DOWNLOAD_DIR + path.sep)) throw new Error('Invalid path');"
4655
+ ));
4656
+ }
4657
+ if (!hasContainmentCheck && /\b(?:sendFile|download)\s*\(/.test(content)) {
4658
+ const ctx = tryParse(content, filePath);
4659
+ if (ctx) {
4660
+ const { parsed, taint } = ctx;
4661
+ 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);
4662
+ const isUnsafePathBuild = (node) => {
4663
+ if (node.type !== "CallExpression") return false;
4664
+ const callee = node.callee;
4665
+ 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");
4666
+ if (!isPathBuild) return false;
4667
+ const rootedAtDirname = node.arguments.some(
4668
+ (a) => a.type === "Identifier" && (a.name === "__dirname" || a.name === "__filename")
4669
+ );
4670
+ if (rootedAtDirname) return false;
4671
+ return node.arguments.some(
4672
+ (a) => a.type !== "SpreadElement" && a.type !== "StringLiteral" && a.type !== "TemplateLiteral"
4673
+ );
4674
+ };
4675
+ const argIsTainted = (node) => {
4676
+ if (taint.isTainted(node)) return true;
4677
+ if (readsUserInput && isUnsafePathBuild(node)) return true;
4678
+ if (node.type === "CallExpression") {
4679
+ return node.arguments.some((a) => a.type !== "SpreadElement" && argIsTainted(a));
4680
+ }
4681
+ return false;
4682
+ };
4683
+ visitCalls(
4684
+ parsed,
4685
+ (callee) => callee.type === "MemberExpression" && callee.property.type === "Identifier" && (callee.property.name === "sendFile" || callee.property.name === "download"),
4686
+ (call, line) => {
4687
+ const first = call.arguments[0];
4688
+ if (!first || first.type === "SpreadElement") return;
4689
+ if (!argIsTainted(first)) return;
4690
+ if (matches.some((m) => m.line === line)) return;
4691
+ matches.push(
4692
+ astMatch(
4693
+ content,
4694
+ filePath,
4695
+ line,
4696
+ unprotectedDownload,
4697
+ "Validate file paths: const safePath = path.resolve(DOWNLOAD_DIR, filename); if (!safePath.startsWith(DOWNLOAD_DIR + path.sep)) throw new Error('Invalid path');"
4698
+ )
4699
+ );
4700
+ }
4701
+ );
4569
4702
  }
4570
4703
  }
4571
4704
  return matches;
@@ -5168,7 +5301,7 @@ var s3BucketNoEncryption = {
5168
5301
  const blockEnd = Math.min(m.index + 2e3, content.length);
5169
5302
  const blockContent = content.substring(m.index, blockEnd);
5170
5303
  if (!/server_side_encryption/.test(blockContent)) {
5171
- const lineNum = content.substring(0, m.index).split("\n").length;
5304
+ const lineNum = lineNumberAt(content, m.index);
5172
5305
  matches.push({
5173
5306
  rule: "VC107",
5174
5307
  title: s3BucketNoEncryption.title,
@@ -5199,7 +5332,7 @@ var securityGroupAllInbound = {
5199
5332
  while ((m = ingressPattern.exec(content)) !== null) {
5200
5333
  if (isCommentLine(content, m.index)) continue;
5201
5334
  if (/from_port\s*=\s*0/.test(m[0]) || !/from_port/.test(m[0])) {
5202
- const lineNum = content.substring(0, m.index).split("\n").length;
5335
+ const lineNum = lineNumberAt(content, m.index);
5203
5336
  matches.push({
5204
5337
  rule: "VC108",
5205
5338
  title: securityGroupAllInbound.title,
@@ -5286,7 +5419,7 @@ var lambdaWithoutVPC = {
5286
5419
  const blockEnd = Math.min(m.index + 2e3, content.length);
5287
5420
  const blockContent = content.substring(m.index, blockEnd);
5288
5421
  if (!/vpc_config\s*\{/.test(blockContent)) {
5289
- const lineNum = content.substring(0, m.index).split("\n").length;
5422
+ const lineNum = lineNumberAt(content, m.index);
5290
5423
  matches.push({
5291
5424
  rule: "VC111",
5292
5425
  title: lambdaWithoutVPC.title,
@@ -5316,7 +5449,7 @@ var dockerLatestTag = {
5316
5449
  while ((m = fromPattern.exec(content)) !== null) {
5317
5450
  const image = m[1];
5318
5451
  if (image.endsWith(":latest") || !image.includes(":") && !image.startsWith("$")) {
5319
- const lineNum = content.substring(0, m.index).split("\n").length;
5452
+ const lineNum = lineNumberAt(content, m.index);
5320
5453
  matches.push({
5321
5454
  rule: "VC112",
5322
5455
  title: dockerLatestTag.title,
@@ -5444,17 +5577,26 @@ var pathTraversal = {
5444
5577
  const findings = [];
5445
5578
  const patterns = [
5446
5579
  /(?:readFile|readFileSync|createReadStream|writeFile|writeFileSync|appendFile|unlink|unlinkSync|access|stat|statSync|existsSync)\s*\(\s*(?:req\.|request\.|ctx\.|params\.|query\.)/gi,
5447
- /(?:path\.join|path\.resolve)\s*\([^)]*(?:req\.|request\.|ctx\.|params\.|query\.)[^)]*\)/gi,
5448
- /(?:res\.sendFile|res\.download)\s*\([^)]*(?:req\.|request\.)/gi,
5580
+ /(?:path\.join|path\.resolve)\s*\([^)]{0,500}(?:req\.|request\.|ctx\.|params\.|query\.)[^)]{0,500}\)/gi,
5581
+ /(?:res\.sendFile|res\.download)\s*\([^)]{0,500}(?:req\.|request\.)/gi,
5449
5582
  /open\s*\(\s*(?:request\.|params\[|args\.)/gi
5450
5583
  ];
5584
+ const MAX_MATCHES = 500;
5451
5585
  for (const pat of patterns) {
5586
+ if (findings.length >= MAX_MATCHES) break;
5452
5587
  let m;
5588
+ let lastIdx = -1;
5453
5589
  while ((m = pat.exec(content)) !== null) {
5590
+ if (findings.length >= MAX_MATCHES) break;
5591
+ if (m.index === lastIdx && m[0].length === 0) {
5592
+ pat.lastIndex++;
5593
+ continue;
5594
+ }
5595
+ lastIdx = m.index;
5454
5596
  if (isCommentLine(content, m.index)) continue;
5455
5597
  const surrounding = content.substring(Math.max(0, m.index - 200), m.index + 200);
5456
- if (/path\.normalize|sanitize|\.replace\(\s*['"]\.\./i.test(surrounding)) continue;
5457
- const lineNum = content.substring(0, m.index).split("\n").length;
5598
+ if (/path\.normalize|sanitize|\.replace\(\s*['"]\.\.|\.startsWith\s*\(|realpath/i.test(surrounding)) continue;
5599
+ const lineNum = lineNumberAt(content, m.index);
5458
5600
  findings.push({
5459
5601
  rule: "VC117",
5460
5602
  title: pathTraversal.title,
@@ -5485,7 +5627,7 @@ var piiInLogs = {
5485
5627
  while ((m = logPattern.exec(content)) !== null) {
5486
5628
  if (isCommentLine(content, m.index)) continue;
5487
5629
  if (isInsideFixMessage(content, m.index)) continue;
5488
- const lineNum = content.substring(0, m.index).split("\n").length;
5630
+ const lineNum = lineNumberAt(content, m.index);
5489
5631
  findings.push({
5490
5632
  rule: "VC118",
5491
5633
  title: piiInLogs.title,
@@ -5523,7 +5665,7 @@ var hardcodedOAuthSecret = {
5523
5665
  while ((m = pat.exec(content)) !== null) {
5524
5666
  if (isCommentLine(content, m.index)) continue;
5525
5667
  if (/process\.env|os\.environ|ENV\[|getenv|\$\{|<your|CHANGE_ME|xxx|placeholder/i.test(m[0])) continue;
5526
- const lineNum = content.substring(0, m.index).split("\n").length;
5668
+ const lineNum = lineNumberAt(content, m.index);
5527
5669
  findings.push({
5528
5670
  rule: "VC119",
5529
5671
  title: hardcodedOAuthSecret.title,
@@ -5555,7 +5697,7 @@ var missingOAuthState = {
5555
5697
  if (isCommentLine(content, m.index)) continue;
5556
5698
  const surrounding = content.substring(m.index, Math.min(content.length, m.index + 500));
5557
5699
  if (/state\s*[=:]/i.test(surrounding)) continue;
5558
- const lineNum = content.substring(0, m.index).split("\n").length;
5700
+ const lineNum = lineNumberAt(content, m.index);
5559
5701
  findings.push({
5560
5702
  rule: "VC120",
5561
5703
  title: missingOAuthState.title,
@@ -5583,7 +5725,7 @@ var unpinnedGitHubAction = {
5583
5725
  let m;
5584
5726
  while ((m = usesPattern.exec(content)) !== null) {
5585
5727
  if (isCommentLine(content, m.index)) continue;
5586
- const lineNum = content.substring(0, m.index).split("\n").length;
5728
+ const lineNum = lineNumberAt(content, m.index);
5587
5729
  findings.push({
5588
5730
  rule: "VC121",
5589
5731
  title: unpinnedGitHubAction.title,
@@ -5617,7 +5759,7 @@ var deprecatedTLS = {
5617
5759
  let m;
5618
5760
  while ((m = pat.exec(content)) !== null) {
5619
5761
  if (isCommentLine(content, m.index)) continue;
5620
- const lineNum = content.substring(0, m.index).split("\n").length;
5762
+ const lineNum = lineNumberAt(content, m.index);
5621
5763
  findings.push({
5622
5764
  rule: "VC122",
5623
5765
  title: deprecatedTLS.title,
@@ -5653,7 +5795,7 @@ var weakRSAKeySize = {
5653
5795
  let m;
5654
5796
  while ((m = pat.exec(content)) !== null) {
5655
5797
  if (isCommentLine(content, m.index)) continue;
5656
- const lineNum = content.substring(0, m.index).split("\n").length;
5798
+ const lineNum = lineNumberAt(content, m.index);
5657
5799
  findings.push({
5658
5800
  rule: "VC123",
5659
5801
  title: weakRSAKeySize.title,
@@ -5689,7 +5831,7 @@ var ecbModeEncryption = {
5689
5831
  let m;
5690
5832
  while ((m = pat.exec(content)) !== null) {
5691
5833
  if (isCommentLine(content, m.index)) continue;
5692
- const lineNum = content.substring(0, m.index).split("\n").length;
5834
+ const lineNum = lineNumberAt(content, m.index);
5693
5835
  findings.push({
5694
5836
  rule: "VC124",
5695
5837
  title: ecbModeEncryption.title,
@@ -5720,7 +5862,7 @@ var insecurePasswordReset = {
5720
5862
  let m;
5721
5863
  while ((m = weakTokens.exec(content)) !== null) {
5722
5864
  if (isCommentLine(content, m.index)) continue;
5723
- const lineNum = content.substring(0, m.index).split("\n").length;
5865
+ const lineNum = lineNumberAt(content, m.index);
5724
5866
  findings.push({
5725
5867
  rule: "VC125",
5726
5868
  title: insecurePasswordReset.title,
@@ -5737,7 +5879,7 @@ var insecurePasswordReset = {
5737
5879
  if (isCommentLine(content, m.index)) continue;
5738
5880
  const surrounding = content.substring(Math.max(0, m.index - 500), m.index);
5739
5881
  if (!/(?:reset|forgot).*password/i.test(surrounding)) continue;
5740
- const lineNum = content.substring(0, m.index).split("\n").length;
5882
+ const lineNum = lineNumberAt(content, m.index);
5741
5883
  findings.push({
5742
5884
  rule: "VC125",
5743
5885
  title: insecurePasswordReset.title,
@@ -5798,7 +5940,7 @@ var insecureHTTPMethods = {
5798
5940
  if (isCommentLine(content, m.index)) continue;
5799
5941
  const handlerBlock = content.substring(m.index, Math.min(content.length, m.index + 500));
5800
5942
  if (/auth|authenticate|authorize|requireAuth|isAuthenticated|protect|guard|middleware|session|jwt|verify|clerk|getAuth/i.test(handlerBlock)) continue;
5801
- const lineNum = content.substring(0, m.index).split("\n").length;
5943
+ const lineNum = lineNumberAt(content, m.index);
5802
5944
  findings.push({
5803
5945
  rule: "VC127",
5804
5946
  title: insecureHTTPMethods.title,
@@ -5832,7 +5974,7 @@ var httpRequestSmuggling = {
5832
5974
  let m;
5833
5975
  while ((m = pat.exec(content)) !== null) {
5834
5976
  if (isCommentLine(content, m.index)) continue;
5835
- const lineNum = content.substring(0, m.index).split("\n").length;
5977
+ const lineNum = lineNumberAt(content, m.index);
5836
5978
  findings.push({
5837
5979
  rule: "VC128",
5838
5980
  title: httpRequestSmuggling.title,
@@ -5866,7 +6008,7 @@ var unencryptedPII = {
5866
6008
  let m;
5867
6009
  while ((m = pat.exec(content)) !== null) {
5868
6010
  if (isCommentLine(content, m.index)) continue;
5869
- const lineNum = content.substring(0, m.index).split("\n").length;
6011
+ const lineNum = lineNumberAt(content, m.index);
5870
6012
  findings.push({
5871
6013
  rule: "VC129",
5872
6014
  title: unencryptedPII.title,
@@ -5898,7 +6040,7 @@ var missingAuthRateLimit = {
5898
6040
  if (isCommentLine(content, m.index)) continue;
5899
6041
  const surrounding = content.substring(Math.max(0, m.index - 300), Math.min(content.length, m.index + 300));
5900
6042
  if (/rateLimit|rateLimiter|throttle|slowDown|express-rate-limit|rate_limit|RateLimiter|limiter/i.test(surrounding)) continue;
5901
- const lineNum = content.substring(0, m.index).split("\n").length;
6043
+ const lineNum = lineNumberAt(content, m.index);
5902
6044
  findings.push({
5903
6045
  rule: "VC130",
5904
6046
  title: missingAuthRateLimit.title,
@@ -5936,7 +6078,7 @@ var vulnerableDependencies = {
5936
6078
  for (const [pat, message] of vulnerablePackages) {
5937
6079
  let m;
5938
6080
  while ((m = pat.exec(content)) !== null) {
5939
- const lineNum = content.substring(0, m.index).split("\n").length;
6081
+ const lineNum = lineNumberAt(content, m.index);
5940
6082
  findings.push({
5941
6083
  rule: "VC131",
5942
6084
  title: vulnerableDependencies.title,
@@ -5968,7 +6110,7 @@ function secretRuleCheck(content, filePath, pattern, ruleId, title, severity, fi
5968
6110
  const lineStart = content.lastIndexOf("\n", m.index - 1) + 1;
5969
6111
  const lineText = content.substring(lineStart, content.indexOf("\n", m.index));
5970
6112
  if (PLACEHOLDER_RE.test(lineText)) continue;
5971
- const lineNum = content.substring(0, m.index).split("\n").length;
6113
+ const lineNum = lineNumberAt(content, m.index);
5972
6114
  findings.push({
5973
6115
  rule: ruleId,
5974
6116
  title,
@@ -6069,7 +6211,7 @@ var hardcodedGCPServiceAccount = {
6069
6211
  const findings = [];
6070
6212
  const m = content.match(/"type"\s*:\s*"service_account"/);
6071
6213
  if (m && m.index !== void 0) {
6072
- const lineNum = content.substring(0, m.index).split("\n").length;
6214
+ const lineNum = lineNumberAt(content, m.index);
6073
6215
  findings.push({
6074
6216
  rule: "VC136",
6075
6217
  title: this.title,
@@ -6175,7 +6317,7 @@ var hardcodedDatadogKey = {
6175
6317
  const lineStart = content.lastIndexOf("\n", m.index - 1) + 1;
6176
6318
  const lineText = content.substring(lineStart, content.indexOf("\n", m.index));
6177
6319
  if (PLACEHOLDER_RE.test(lineText)) continue;
6178
- const lineNum = content.substring(0, m.index).split("\n").length;
6320
+ const lineNum = lineNumberAt(content, m.index);
6179
6321
  findings.push({
6180
6322
  rule: "VC141",
6181
6323
  title: this.title,
@@ -6209,7 +6351,7 @@ var hardcodedVercelToken = {
6209
6351
  const lineStart = content.lastIndexOf("\n", m.index - 1) + 1;
6210
6352
  const lineText = content.substring(lineStart, content.indexOf("\n", m.index));
6211
6353
  if (PLACEHOLDER_RE.test(lineText)) continue;
6212
- const lineNum = content.substring(0, m.index).split("\n").length;
6354
+ const lineNum = lineNumberAt(content, m.index);
6213
6355
  findings.push({
6214
6356
  rule: "VC142",
6215
6357
  title: this.title,
@@ -6243,7 +6385,7 @@ var hardcodedSupabaseServiceRole = {
6243
6385
  const lineStart = content.lastIndexOf("\n", m.index - 1) + 1;
6244
6386
  const lineText = content.substring(lineStart, content.indexOf("\n", m.index));
6245
6387
  if (PLACEHOLDER_RE.test(lineText)) continue;
6246
- const lineNum = content.substring(0, m.index).split("\n").length;
6388
+ const lineNum = lineNumberAt(content, m.index);
6247
6389
  findings.push({
6248
6390
  rule: "VC143",
6249
6391
  title: this.title,
@@ -6307,7 +6449,7 @@ function contextSecretRuleCheck(content, filePath, pattern, ruleId, title, sever
6307
6449
  const lineStart = content.lastIndexOf("\n", m.index - 1) + 1;
6308
6450
  const lineText = content.substring(lineStart, content.indexOf("\n", m.index));
6309
6451
  if (PLACEHOLDER_RE.test(lineText)) continue;
6310
- const lineNum = content.substring(0, m.index).split("\n").length;
6452
+ const lineNum = lineNumberAt(content, m.index);
6311
6453
  findings.push({
6312
6454
  rule: ruleId,
6313
6455
  title,
@@ -6787,7 +6929,7 @@ var ghaPullRequestTargetCheckout = {
6787
6929
  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;
6788
6930
  let m;
6789
6931
  while ((m = checkoutPattern.exec(content)) !== null) {
6790
- const lineNum = content.substring(0, m.index).split("\n").length;
6932
+ const lineNum = lineNumberAt(content, m.index);
6791
6933
  findings.push({
6792
6934
  rule: "VC184",
6793
6935
  title: ghaPullRequestTargetCheckout.title,
@@ -6871,7 +7013,7 @@ var ghaThirdPartyActionWithSecrets = {
6871
7013
  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;
6872
7014
  let m;
6873
7015
  while ((m = pattern.exec(content)) !== null) {
6874
- const lineNum = content.substring(0, m.index).split("\n").length;
7016
+ const lineNum = lineNumberAt(content, m.index);
6875
7017
  findings.push({
6876
7018
  rule: "VC187",
6877
7019
  title: ghaThirdPartyActionWithSecrets.title,
@@ -7036,7 +7178,7 @@ var pyRequestsVerifyFalse = {
7036
7178
  let m;
7037
7179
  while ((m = pattern.exec(content)) !== null) {
7038
7180
  if (isCommentLine(content, m.index)) continue;
7039
- const lineNum = content.substring(0, m.index).split("\n").length;
7181
+ const lineNum = lineNumberAt(content, m.index);
7040
7182
  findings.push({
7041
7183
  rule: "VC191",
7042
7184
  title: pyRequestsVerifyFalse.title,
@@ -7067,7 +7209,7 @@ var pyJinja2AutoescapeOff = {
7067
7209
  let m;
7068
7210
  while ((m = pattern.exec(content)) !== null) {
7069
7211
  if (isCommentLine(content, m.index)) continue;
7070
- const lineNum = content.substring(0, m.index).split("\n").length;
7212
+ const lineNum = lineNumberAt(content, m.index);
7071
7213
  findings.push({
7072
7214
  rule: "VC192",
7073
7215
  title: pyJinja2AutoescapeOff.title,
@@ -7096,7 +7238,7 @@ var pyTempfileMktemp = {
7096
7238
  let m;
7097
7239
  while ((m = pattern.exec(content)) !== null) {
7098
7240
  if (isCommentLine(content, m.index)) continue;
7099
- const lineNum = content.substring(0, m.index).split("\n").length;
7241
+ const lineNum = lineNumberAt(content, m.index);
7100
7242
  findings.push({
7101
7243
  rule: "VC193",
7102
7244
  title: pyTempfileMktemp.title,
@@ -7138,7 +7280,7 @@ var pyDjangoMarkSafe = {
7138
7280
  let m;
7139
7281
  while ((m = pattern.exec(content)) !== null) {
7140
7282
  if (isCommentLine(content, m.index)) continue;
7141
- const lineNum = content.substring(0, m.index).split("\n").length;
7283
+ const lineNum = lineNumberAt(content, m.index);
7142
7284
  if (seenLines.has(lineNum)) continue;
7143
7285
  seenLines.add(lineNum);
7144
7286
  findings.push({
@@ -7175,7 +7317,7 @@ var pyParamikoAutoAdd = {
7175
7317
  let m;
7176
7318
  while ((m = pattern.exec(content)) !== null) {
7177
7319
  if (isCommentLine(content, m.index)) continue;
7178
- const lineNum = content.substring(0, m.index).split("\n").length;
7320
+ const lineNum = lineNumberAt(content, m.index);
7179
7321
  if (seenLines.has(lineNum)) continue;
7180
7322
  seenLines.add(lineNum);
7181
7323
  findings.push({
@@ -7208,7 +7350,7 @@ var pyDjangoAllowedHostsWildcard = {
7208
7350
  let m;
7209
7351
  while ((m = pattern.exec(content)) !== null) {
7210
7352
  if (isCommentLine(content, m.index)) continue;
7211
- const lineNum = content.substring(0, m.index).split("\n").length;
7353
+ const lineNum = lineNumberAt(content, m.index);
7212
7354
  findings.push({
7213
7355
  rule: "VC196",
7214
7356
  title: pyDjangoAllowedHostsWildcard.title,
@@ -7246,7 +7388,7 @@ var pyJWTDecodeWeakConfig = {
7246
7388
  if (isCommentLine(content, m.index)) continue;
7247
7389
  const args = m[1];
7248
7390
  if (/\balgorithms\s*=/.test(args)) continue;
7249
- const lineNum = content.substring(0, m.index).split("\n").length;
7391
+ const lineNum = lineNumberAt(content, m.index);
7250
7392
  if (seenLines.has(lineNum)) continue;
7251
7393
  seenLines.add(lineNum);
7252
7394
  findings.push({
@@ -7297,7 +7439,7 @@ var llmPromptInjection = {
7297
7439
  let m;
7298
7440
  while ((m = pattern.exec(content)) !== null) {
7299
7441
  if (isCommentLine(content, m.index)) continue;
7300
- const lineNum = content.substring(0, m.index).split("\n").length;
7442
+ const lineNum = lineNumberAt(content, m.index);
7301
7443
  if (seenLines.has(lineNum)) continue;
7302
7444
  seenLines.add(lineNum);
7303
7445
  findings.push({
@@ -7336,7 +7478,7 @@ var llmSystemPromptInjection = {
7336
7478
  let m;
7337
7479
  while ((m = pattern.exec(content)) !== null) {
7338
7480
  if (isCommentLine(content, m.index)) continue;
7339
- const lineNum = content.substring(0, m.index).split("\n").length;
7481
+ const lineNum = lineNumberAt(content, m.index);
7340
7482
  findings.push({
7341
7483
  rule: "VC199",
7342
7484
  title: llmSystemPromptInjection.title,
@@ -7378,7 +7520,7 @@ var llmOutputAsHTML = {
7378
7520
  let m;
7379
7521
  while ((m = pattern.exec(content)) !== null) {
7380
7522
  if (isCommentLine(content, m.index)) continue;
7381
- const lineNum = content.substring(0, m.index).split("\n").length;
7523
+ const lineNum = lineNumberAt(content, m.index);
7382
7524
  if (seenLines.has(lineNum)) continue;
7383
7525
  seenLines.add(lineNum);
7384
7526
  findings.push({
@@ -7415,7 +7557,7 @@ var vectorStoreQueryNoUserFilter = {
7415
7557
  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;
7416
7558
  if (/\bnamespace\s*[:=]\s*[`"']?[^,)`"']*(?:user|tenant|org|owner|account|customer|workspace)/i.test(args)) continue;
7417
7559
  if (/\bfilter\s*[:=][\s\S]*?(?:\buser|\btenant|\borg|\bowner|\baccount|\bcustomer|\bworkspace)/i.test(args)) continue;
7418
- const lineNum = content.substring(0, m.index).split("\n").length;
7560
+ const lineNum = lineNumberAt(content, m.index);
7419
7561
  findings.push({
7420
7562
  rule: "VC201",
7421
7563
  title: vectorStoreQueryNoUserFilter.title,
@@ -7452,7 +7594,7 @@ var vectorStoreUpsertNoMetadata = {
7452
7594
  if (/\bnamespace\s*[:=]\s*[`"']?[^,)`"']*(?:user|tenant|org)/i.test(args)) {
7453
7595
  continue;
7454
7596
  }
7455
- const lineNum = content.substring(0, m.index).split("\n").length;
7597
+ const lineNum = lineNumberAt(content, m.index);
7456
7598
  findings.push({
7457
7599
  rule: "VC202",
7458
7600
  title: vectorStoreUpsertNoMetadata.title,
@@ -7506,7 +7648,7 @@ var llmCallNoMaxTokens = {
7506
7648
  if (depth !== 0) continue;
7507
7649
  const args = content.substring(openIdx + 1, i).replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/[^\n]*/g, "").replace(/#[^\n]*/g, "");
7508
7650
  if (TOKEN_KW_RE.test(args)) 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: "VC203",
7512
7654
  title: llmCallNoMaxTokens.title,
@@ -7546,7 +7688,7 @@ var graphqlNoDepthLimit = {
7546
7688
  let m;
7547
7689
  while ((m = anchorRe.exec(content)) !== null) {
7548
7690
  if (isCommentLine(content, m.index)) continue;
7549
- const lineNum = content.substring(0, m.index).split("\n").length;
7691
+ const lineNum = lineNumberAt(content, m.index);
7550
7692
  findings.push({
7551
7693
  rule: "VC204",
7552
7694
  title: graphqlNoDepthLimit.title,
@@ -7580,7 +7722,7 @@ var graphqlNoComplexityLimit = {
7580
7722
  let m;
7581
7723
  while ((m = anchorRe.exec(content)) !== null) {
7582
7724
  if (isCommentLine(content, m.index)) continue;
7583
- const lineNum = content.substring(0, m.index).split("\n").length;
7725
+ const lineNum = lineNumberAt(content, m.index);
7584
7726
  findings.push({
7585
7727
  rule: "VC205",
7586
7728
  title: graphqlNoComplexityLimit.title,
@@ -7610,7 +7752,7 @@ var graphqlCSRFDisabled = {
7610
7752
  let m;
7611
7753
  while ((m = pattern.exec(content)) !== null) {
7612
7754
  if (isCommentLine(content, m.index)) continue;
7613
- const lineNum = content.substring(0, m.index).split("\n").length;
7755
+ const lineNum = lineNumberAt(content, m.index);
7614
7756
  findings.push({
7615
7757
  rule: "VC206",
7616
7758
  title: graphqlCSRFDisabled.title,
@@ -7706,7 +7848,7 @@ var webhookMissingIdempotency = {
7706
7848
  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 [];
7707
7849
  const m = content.match(/constructEvent|new\s+Webhook\s*\(|verifyHeader/i);
7708
7850
  if (!m || m.index === void 0) return [];
7709
- const line = content.substring(0, m.index).split("\n").length;
7851
+ const line = lineNumberAt(content, m.index);
7710
7852
  return filterSilenced([{
7711
7853
  rule: "VC209",
7712
7854
  title: webhookMissingIdempotency.title,
@@ -7733,7 +7875,7 @@ var middlewareMatcherExcludesApi = {
7733
7875
  if (!/\(\?!\s*[^)]*\bapi\b/.test(content)) return [];
7734
7876
  const m = content.match(/matcher\s*:/);
7735
7877
  if (!m || m.index === void 0) return [];
7736
- const line = content.substring(0, m.index).split("\n").length;
7878
+ const line = lineNumberAt(content, m.index);
7737
7879
  return filterSilenced([{
7738
7880
  rule: "VC210",
7739
7881
  title: middlewareMatcherExcludesApi.title,
@@ -7771,7 +7913,7 @@ var secretInURLParam = {
7771
7913
  if (isCommentLine(content, m.index)) continue;
7772
7914
  if (isInsideFixMessage(content, m.index)) continue;
7773
7915
  if (isInlineSilenced(content, m.index, "VC146")) continue;
7774
- const lineNum = content.substring(0, m.index).split("\n").length;
7916
+ const lineNum = lineNumberAt(content, m.index);
7775
7917
  findings.push({
7776
7918
  rule: "VC146",
7777
7919
  title: this.title,
@@ -7802,7 +7944,7 @@ var secretLoggedToConsole = {
7802
7944
  while ((m = pattern.exec(content)) !== null) {
7803
7945
  if (isCommentLine(content, m.index)) continue;
7804
7946
  if (isInsideFixMessage(content, m.index)) continue;
7805
- const lineNum = content.substring(0, m.index).split("\n").length;
7947
+ const lineNum = lineNumberAt(content, m.index);
7806
7948
  findings.push({
7807
7949
  rule: "VC147",
7808
7950
  title: this.title,
@@ -7832,7 +7974,7 @@ var secretInErrorResponse = {
7832
7974
  while ((m = pattern.exec(content)) !== null) {
7833
7975
  if (isCommentLine(content, m.index)) continue;
7834
7976
  if (isInsideFixMessage(content, m.index)) continue;
7835
- const lineNum = content.substring(0, m.index).split("\n").length;
7977
+ const lineNum = lineNumberAt(content, m.index);
7836
7978
  findings.push({
7837
7979
  rule: "VC148",
7838
7980
  title: this.title,
@@ -7871,7 +8013,7 @@ var secretInBundleConfig = {
7871
8013
  while ((m = re.exec(content)) !== null) {
7872
8014
  if (isCommentLine(content, m.index)) continue;
7873
8015
  if (isInsideFixMessage(content, m.index)) continue;
7874
- const lineNum = content.substring(0, m.index).split("\n").length;
8016
+ const lineNum = lineNumberAt(content, m.index);
7875
8017
  findings.push({
7876
8018
  rule: "VC149",
7877
8019
  title: this.title,
@@ -7909,7 +8051,7 @@ var secretInHTMLAttribute = {
7909
8051
  while ((m = re.exec(content)) !== null) {
7910
8052
  if (isCommentLine(content, m.index)) continue;
7911
8053
  if (isInsideFixMessage(content, m.index)) continue;
7912
- const lineNum = content.substring(0, m.index).split("\n").length;
8054
+ const lineNum = lineNumberAt(content, m.index);
7913
8055
  findings.push({
7914
8056
  rule: "VC150",
7915
8057
  title: this.title,
@@ -7936,9 +8078,9 @@ var secretInCLIArgument = {
7936
8078
  if (!filePath.match(/\.(js|ts|jsx|tsx|py|rb)$/)) return [];
7937
8079
  const findings = [];
7938
8080
  const patterns = [
7939
- /(?:exec|execSync|spawn|spawnSync|child_process)\s*\([^)]*\$\{[^}]*(?:api[_-]?key|secret|token|password|credentials)/gi,
7940
- /(?:exec|execSync|spawn|spawnSync|child_process)\s*\([^)]*["']\s*\+\s*(?:api[_-]?key|secret|token|password|credentials)/gi,
7941
- /(?:subprocess|os\.system|os\.popen)\s*\([^)]*\{[^}]*(?:api[_-]?key|secret|token|password|credentials)/gi
8081
+ /(?:exec|execSync|spawn|spawnSync|child_process)\s*\([^)]{0,500}\$\{[^}]{0,200}(?:api[_-]?key|secret|token|password|credentials)/gi,
8082
+ /(?:exec|execSync|spawn|spawnSync|child_process)\s*\([^)]{0,500}["']\s*\+\s*(?:api[_-]?key|secret|token|password|credentials)/gi,
8083
+ /(?:subprocess|os\.system|os\.popen)\s*\([^)]{0,500}\{[^}]{0,200}(?:api[_-]?key|secret|token|password|credentials)/gi
7942
8084
  ];
7943
8085
  for (const p of patterns) {
7944
8086
  let m;
@@ -7946,7 +8088,7 @@ var secretInCLIArgument = {
7946
8088
  while ((m = re.exec(content)) !== null) {
7947
8089
  if (isCommentLine(content, m.index)) continue;
7948
8090
  if (isInsideFixMessage(content, m.index)) continue;
7949
- const lineNum = content.substring(0, m.index).split("\n").length;
8091
+ const lineNum = lineNumberAt(content, m.index);
7950
8092
  findings.push({
7951
8093
  rule: "VC151",
7952
8094
  title: this.title,
@@ -7992,7 +8134,7 @@ var webhookSignatureVerification = {
7992
8134
  if (svc.verify.test(content)) continue;
7993
8135
  const m = content.match(/export\s+(?:async\s+)?function\s+POST/);
7994
8136
  if (!m || m.index === void 0) continue;
7995
- const lineNum = content.substring(0, m.index).split("\n").length;
8137
+ const lineNum = lineNumberAt(content, m.index);
7996
8138
  findings.push({
7997
8139
  rule: "VC152",
7998
8140
  title: `${svc.name} Webhook Missing Signature Verification`,
@@ -8059,7 +8201,7 @@ var missingRequestValidation = {
8059
8201
  if (/if\s*\(\s*!(?:body|parsed|data|payload)\.|throw.*(?:missing|invalid|required)/i.test(content)) return [];
8060
8202
  const m = content.match(/export\s+(?:async\s+)?function\s+(?:POST|PUT|PATCH)/);
8061
8203
  if (!m || m.index === void 0) return [];
8062
- const lineNum = content.substring(0, m.index).split("\n").length;
8204
+ const lineNum = lineNumberAt(content, m.index);
8063
8205
  return [{
8064
8206
  rule: "VC154",
8065
8207
  title: this.title,
@@ -8086,7 +8228,7 @@ var missingAIRateLimit = {
8086
8228
  if (/rateLimit|rateLimiter|throttle|checkRateLimit|limiter|slowDown|express-rate-limit/i.test(content)) return [];
8087
8229
  const m = content.match(/export\s+(?:async\s+)?function\s+(?:POST|GET)/i) || content.match(/\.(post|get)\s*\(/i);
8088
8230
  if (!m || m.index === void 0) return [];
8089
- const lineNum = content.substring(0, m.index).split("\n").length;
8231
+ const lineNum = lineNumberAt(content, m.index);
8090
8232
  return [{
8091
8233
  rule: "VC155",
8092
8234
  title: this.title,
@@ -8115,7 +8257,7 @@ var missingPagination = {
8115
8257
  if (/findUnique|findFirst|findById|\.findOne|WHERE.*id\s*=/i.test(content)) return [];
8116
8258
  const m = content.match(/export\s+(?:async\s+)?function\s+GET/);
8117
8259
  if (!m || m.index === void 0) return [];
8118
- const lineNum = content.substring(0, m.index).split("\n").length;
8260
+ const lineNum = lineNumberAt(content, m.index);
8119
8261
  return [{
8120
8262
  rule: "VC156",
8121
8263
  title: this.title,
@@ -8176,7 +8318,7 @@ var insecureDirectObjectReference = {
8176
8318
  if (/requireUser|requireUserForApi|getServerSession|auth\(\)/i.test(content)) return [];
8177
8319
  const m = content.match(/params\.id|params\.(?:slug|uuid)|req\.params\./);
8178
8320
  if (!m || m.index === void 0) return [];
8179
- const lineNum = content.substring(0, m.index).split("\n").length;
8321
+ const lineNum = lineNumberAt(content, m.index);
8180
8322
  return [{
8181
8323
  rule: "VC158",
8182
8324
  title: this.title,
@@ -8482,6 +8624,17 @@ function runCustomRules(content, filePath, disabledRules = [], tier = "free", ex
8482
8624
  return findings;
8483
8625
  }
8484
8626
  if (/pro-rules-bundle|\.bundle\./i.test(filePath)) return findings;
8627
+ let maxLineLen = 0;
8628
+ {
8629
+ let lineStart = 0;
8630
+ for (let i = 0; i <= content.length; i++) {
8631
+ if (i === content.length || content.charCodeAt(i) === 10) {
8632
+ if (i - lineStart > maxLineLen) maxLineLen = i - lineStart;
8633
+ lineStart = i + 1;
8634
+ }
8635
+ }
8636
+ }
8637
+ if (maxLineLen > 5e4) return findings;
8485
8638
  if (/onboarding|demo-data|example-vulnerable|code-sample/i.test(filePath)) return findings;
8486
8639
  const ruleset = tier === "pro" && extraRules.length > 0 ? [...freeRules, ...extraRules] : freeRules;
8487
8640
  for (const rule of ruleset) {