xploitscan 1.2.0 → 1.2.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/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
 
6
6
  **Security scanner for AI-generated code.** Find vulnerabilities before attackers do.
7
7
 
8
- Built for developers shipping code via Cursor, Lovable, Bolt, Replit, and Claude Code. 210 security rules. Plain-English results. Copy-paste fixes.
8
+ Built for developers shipping code via Cursor, Lovable, Bolt, Replit, and Claude Code. 210+ security rules. Plain-English results. Copy-paste fixes.
9
9
 
10
10
  ## Quick Start
11
11
 
@@ -17,7 +17,7 @@ No install, no config, no account required. Your code stays 100% local.
17
17
 
18
18
  ## What It Catches
19
19
 
20
- 210 rules across 15+ categories:
20
+ 210+ rules across 15+ categories:
21
21
 
22
22
  | Category | Examples | Rules |
23
23
  |----------|---------|-------|
@@ -36,8 +36,8 @@ Every finding includes OWASP Top 10 and CWE compliance mappings.
36
36
 
37
37
  Detection is scored publicly on a labeled fixture corpus that's refreshed on every commit. Current numbers live at **[xploitscan.com/benchmark](https://xploitscan.com/benchmark)**:
38
38
 
39
- - **100% precision** (zero false positives) across 151 labeled fixtures covering 25+ vulnerability classes
40
- - **80%+ recall** on rules with active test coverage
39
+ - **100% precision** (zero false positives) across a 200+ fixture labeled corpus covering 25+ vulnerability classes
40
+ - **98%+ recall** live numbers at [xploitscan.com/benchmark](https://xploitscan.com/benchmark)
41
41
  - Side-by-side comparison with Semgrep (community rulesets) and Bearer on the same corpus
42
42
 
43
43
  The scanner uses a two-layer architecture: a fast regex pre-filter for pattern-based rules (secrets, missing headers, container misconfigs), and a Babel-parsed AST layer with local taint tracking for data-flow rules (SSRF, prototype pollution, mass assignment, SSTI, command injection from user input). Recognized taint sources: Express / Fastify / Koa / Next.js App Router / Web Fetch API / AWS Lambda.
@@ -166,7 +166,7 @@ Scan via the web at [xploitscan.com](https://xploitscan.com):
166
166
  - SOC2/ISO27001 compliance mapping
167
167
  - Slack and Discord webhook notifications
168
168
 
169
- **Free**: 5 scans/day, 30 core rules. **Indie** ($9/mo): 500 scans/month, all 210 rules, scan history. **Pro** ($19/mo): unlimited scans, all 210 rules, PDF reports, compliance mapping, webhooks, AI false-positive filter. **Team** ($99/mo): everything in Pro plus 5 seats, shared scan history, RBAC, and portfolio reports. Annual plans save 40%.
169
+ **Free**: 5 scans/day, 30 core rules. **Indie** ($9/mo): 500 scans/month, all 210+ rules, scan history. **Pro** ($19/mo): unlimited scans, all 210+ rules, PDF reports, compliance mapping, webhooks, AI false-positive filter. **Team** ($99/mo): everything in Pro plus 5 seats, shared scan history, RBAC, and portfolio reports. Annual plans save 40%.
170
170
 
171
171
  ## Supported Languages
172
172
 
@@ -14,7 +14,7 @@ import {
14
14
  storeToken,
15
15
  syncUser,
16
16
  uploadScanResults
17
- } from "./chunk-KE2IZNH6.js";
17
+ } from "./chunk-4OFR3SIS.js";
18
18
  export {
19
19
  checkUsage,
20
20
  clearProRulesCache,
@@ -29,4 +29,4 @@ export {
29
29
  syncUser,
30
30
  uploadScanResults
31
31
  };
32
- //# sourceMappingURL=api-4UFZFIDO.js.map
32
+ //# sourceMappingURL=api-MFCSQQ2Y.js.map
@@ -14,7 +14,11 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
14
14
  throw Error('Dynamic require of "' + x + '" is not supported');
15
15
  });
16
16
  var __commonJS = (cb, mod) => function __require2() {
17
- return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
17
+ try {
18
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
19
+ } catch (e) {
20
+ throw mod = 0, e;
21
+ }
18
22
  };
19
23
  var __copyProps = (to, from, except, desc) => {
20
24
  if (from && typeof from === "object" || typeof from === "function") {
@@ -43420,15 +43424,50 @@ var import_parser = __toESM(require_lib(), 1);
43420
43424
  var import_traverse = __toESM(require_lib8(), 1);
43421
43425
  var import_traverse2 = __toESM(require_lib8(), 1);
43422
43426
  import Anthropic from "@anthropic-ai/sdk";
43423
- function getSnippet(content, line, contextLines = 2) {
43427
+ var cacheContent = null;
43428
+ var cacheLines = [];
43429
+ var cacheOffsets = [];
43430
+ function lineIndex(content) {
43431
+ if (content === cacheContent) return { lines: cacheLines, offsets: cacheOffsets };
43424
43432
  const lines = content.split("\n");
43433
+ const offsets = new Array(lines.length);
43434
+ let off = 0;
43435
+ for (let i = 0; i < lines.length; i++) {
43436
+ offsets[i] = off;
43437
+ off += lines[i].length + 1;
43438
+ }
43439
+ cacheContent = content;
43440
+ cacheLines = lines;
43441
+ cacheOffsets = offsets;
43442
+ return { lines, offsets };
43443
+ }
43444
+ function lineNumberAt(content, index) {
43445
+ const { offsets } = lineIndex(content);
43446
+ let lo = 0;
43447
+ let hi = offsets.length - 1;
43448
+ let ans = 0;
43449
+ while (lo <= hi) {
43450
+ const mid = lo + hi >> 1;
43451
+ if (offsets[mid] <= index) {
43452
+ ans = mid;
43453
+ lo = mid + 1;
43454
+ } else {
43455
+ hi = mid - 1;
43456
+ }
43457
+ }
43458
+ return ans + 1;
43459
+ }
43460
+ function getSnippet(content, line, contextLines = 2) {
43461
+ const { lines } = lineIndex(content);
43425
43462
  const start = Math.max(0, line - 1 - contextLines);
43426
43463
  const end = Math.min(lines.length, line + contextLines);
43427
- return lines.slice(start, end).map((l, i) => {
43428
- const lineNum = start + i + 1;
43464
+ const out = [];
43465
+ for (let i = start; i < end; i++) {
43466
+ const lineNum = i + 1;
43429
43467
  const marker = lineNum === line ? ">" : " ";
43430
- return `${marker} ${lineNum.toString().padStart(4)} | ${l}`;
43431
- }).join("\n");
43468
+ out.push(`${marker} ${lineNum.toString().padStart(4)} | ${lines[i]}`);
43469
+ }
43470
+ return out.join("\n");
43432
43471
  }
43433
43472
  var MAX_CACHE = 256;
43434
43473
  var cache = /* @__PURE__ */ new Map();
@@ -43550,6 +43589,77 @@ function callPreservesTaint(node, rec) {
43550
43589
  }
43551
43590
  return false;
43552
43591
  }
43592
+ function isParamDerived(n, p) {
43593
+ if (!n) return false;
43594
+ if (n.type === "Identifier") return n.name === p;
43595
+ if (n.type === "MemberExpression") return isParamDerived(n.object, p);
43596
+ return false;
43597
+ }
43598
+ function formattedFlow(n, p) {
43599
+ if (!n) return false;
43600
+ switch (n.type) {
43601
+ case "TemplateLiteral":
43602
+ return n.expressions.some(
43603
+ (e) => isParamDerived(e, p) || formattedFlow(e, p)
43604
+ );
43605
+ case "BinaryExpression":
43606
+ if (n.operator !== "+") return false;
43607
+ return isParamDerived(n.left, p) || isParamDerived(n.right, p) || formattedFlow(n.left, p) || formattedFlow(n.right, p);
43608
+ case "ObjectExpression":
43609
+ return n.properties.some(
43610
+ (pr) => pr.type === "SpreadElement" && isParamDerived(pr.argument, p)
43611
+ );
43612
+ case "ArrayExpression":
43613
+ return n.elements.some(
43614
+ (el) => el != null && el.type === "SpreadElement" && isParamDerived(el.argument, p)
43615
+ );
43616
+ case "ConditionalExpression":
43617
+ return formattedFlow(n.consequent, p) || formattedFlow(n.alternate, p);
43618
+ case "AwaitExpression":
43619
+ return formattedFlow(n.argument, p);
43620
+ default:
43621
+ return false;
43622
+ }
43623
+ }
43624
+ function collectReturnArgs(fn) {
43625
+ const out = [];
43626
+ if (fn.type === "ArrowFunctionExpression" && fn.body.type !== "BlockStatement") {
43627
+ out.push(fn.body);
43628
+ return out;
43629
+ }
43630
+ const visit = (n) => {
43631
+ if (!n || typeof n !== "object") return;
43632
+ const node = n;
43633
+ if (typeof node.type !== "string") return;
43634
+ if (node.type === "ReturnStatement") {
43635
+ const arg = node.argument;
43636
+ if (arg) out.push(arg);
43637
+ return;
43638
+ }
43639
+ if (node.type === "FunctionDeclaration" || node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression") {
43640
+ return;
43641
+ }
43642
+ for (const key of Object.keys(node)) {
43643
+ const v = node[key];
43644
+ if (Array.isArray(v)) v.forEach(visit);
43645
+ else visit(v);
43646
+ }
43647
+ };
43648
+ visit(fn.body);
43649
+ return out;
43650
+ }
43651
+ function computePassthrough(fn) {
43652
+ const params = fn.params ?? [];
43653
+ const returns = collectReturnArgs(fn);
43654
+ const out = /* @__PURE__ */ new Set();
43655
+ for (let i = 0; i < params.length; i++) {
43656
+ const param = params[i];
43657
+ if (param.type !== "Identifier") continue;
43658
+ const name = param.name;
43659
+ if (returns.some((r) => formattedFlow(r, name))) out.add(i);
43660
+ }
43661
+ return out;
43662
+ }
43553
43663
  function buildTaintMap(parsed) {
43554
43664
  const tainted = /* @__PURE__ */ new Set();
43555
43665
  function markPatternTainted(target) {
@@ -43643,6 +43753,7 @@ function buildTaintMap(parsed) {
43643
43753
  if (node.type === "CallExpression") {
43644
43754
  if (nodeIsTaintedSource(node)) return true;
43645
43755
  if (callPreservesTaint(node, exprIsTainted)) return true;
43756
+ if (localCallPropagates(node, exprIsTainted)) return true;
43646
43757
  if (node.callee.type === "MemberExpression") {
43647
43758
  if (exprIsTainted(node.callee.object)) return true;
43648
43759
  const obj = node.callee.object;
@@ -43675,6 +43786,34 @@ function buildTaintMap(parsed) {
43675
43786
  }
43676
43787
  return false;
43677
43788
  }
43789
+ const fnPassthrough = /* @__PURE__ */ new Map();
43790
+ const recordFn = (name, fn) => {
43791
+ const pt = computePassthrough(fn);
43792
+ if (pt.size > 0) fnPassthrough.set(name, pt);
43793
+ };
43794
+ traverse(parsed.ast, {
43795
+ FunctionDeclaration(path) {
43796
+ const n = path.node;
43797
+ if (n.type === "FunctionDeclaration" && n.id && n.id.type === "Identifier") {
43798
+ recordFn(n.id.name, n);
43799
+ }
43800
+ },
43801
+ VariableDeclarator(path) {
43802
+ const n = path.node;
43803
+ if (n.type !== "VariableDeclarator" || n.id.type !== "Identifier" || !n.init) return;
43804
+ if (n.init.type === "ArrowFunctionExpression" || n.init.type === "FunctionExpression") {
43805
+ recordFn(n.id.name, n.init);
43806
+ }
43807
+ }
43808
+ });
43809
+ function localCallPropagates(node, rec) {
43810
+ if (node.callee.type !== "Identifier") return false;
43811
+ const pt = fnPassthrough.get(node.callee.name);
43812
+ if (!pt) return false;
43813
+ return node.arguments.some(
43814
+ (a, i) => pt.has(i) && a.type !== "SpreadElement" && rec(a)
43815
+ );
43816
+ }
43678
43817
  traverse(parsed.ast, {
43679
43818
  VariableDeclarator(path) {
43680
43819
  const node = path.node;
@@ -43759,6 +43898,7 @@ function buildTaintMap(parsed) {
43759
43898
  }
43760
43899
  if (node.type === "CallExpression") {
43761
43900
  if (callPreservesTaint(node, isTainted)) return true;
43901
+ if (localCallPropagates(node, isTainted)) return true;
43762
43902
  if (node.callee.type === "MemberExpression") {
43763
43903
  if (isTainted(node.callee.object)) return true;
43764
43904
  const obj = node.callee.object;
@@ -43834,9 +43974,12 @@ var SERVER_SIDE_PATH_RE = new RegExp(
43834
43974
  ].join("|"),
43835
43975
  "i"
43836
43976
  );
43837
- function isServerSideFile(filePath) {
43977
+ 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/;
43978
+ function isServerSideFile(filePath, content) {
43838
43979
  if (isTestFile(filePath)) return false;
43839
- return SERVER_SIDE_PATH_RE.test(filePath);
43980
+ if (SERVER_SIDE_PATH_RE.test(filePath)) return true;
43981
+ if (content && SERVER_SIDE_CONTENT_RE.test(content)) return true;
43982
+ return false;
43840
43983
  }
43841
43984
  var CONFIG_FILE_PATTERN = new RegExp(
43842
43985
  [
@@ -43857,7 +44000,7 @@ function isCommentLine(content, matchIndex) {
43857
44000
  function isInsideFixMessage(content, matchIndex) {
43858
44001
  const lineStart = content.lastIndexOf("\n", matchIndex - 1) + 1;
43859
44002
  const lineText = content.substring(lineStart, content.indexOf("\n", matchIndex));
43860
- return /(?:fix|description|message|suggestion|hint|help|example|doc|comment)\s*[:=(]/i.test(lineText) || /return\s*["'`].*\b(?:Use|Replace|Add|Move|Set|Enable|Disable|Never|Don't|Do not|Instead)\b/i.test(lineText);
44003
+ return /(?:fix|description|message|suggestion|hint|help|example|doc|comment)\s*[:=]\s*["'`]/i.test(lineText) || /return\s*["'`].*\b(?:Use|Replace|Add|Move|Set|Enable|Disable|Never|Don't|Do not|Instead)\b/i.test(lineText);
43861
44004
  }
43862
44005
  function isInlineSilenced(content, matchIndex, ruleId) {
43863
44006
  const lines = content.split("\n");
@@ -43872,13 +44015,23 @@ function isInlineSilenced(content, matchIndex, ruleId) {
43872
44015
  }
43873
44016
  function findMatches(content, pattern, rule, filePath, fixTemplate) {
43874
44017
  const matches = [];
43875
- const lines = content.split("\n");
43876
44018
  let m;
43877
44019
  const re = new RegExp(pattern.source, pattern.flags.includes("g") ? pattern.flags : `${pattern.flags}g`);
44020
+ let scanned = 0;
44021
+ let lineNum = 1;
44022
+ let lastMatchIndex = -1;
44023
+ const MAX_MATCHES = 500;
43878
44024
  while ((m = re.exec(content)) !== null) {
44025
+ if (m.index === lastMatchIndex && m[0].length === 0) {
44026
+ re.lastIndex++;
44027
+ continue;
44028
+ }
44029
+ lastMatchIndex = m.index;
43879
44030
  if (isCommentLine(content, m.index)) continue;
43880
44031
  if (isInsideFixMessage(content, m.index)) continue;
43881
- const lineNum = content.substring(0, m.index).split("\n").length;
44032
+ for (; scanned < m.index; scanned++) {
44033
+ if (content.charCodeAt(scanned) === 10) lineNum++;
44034
+ }
43882
44035
  matches.push({
43883
44036
  rule: rule.id,
43884
44037
  title: rule.title,
@@ -43889,6 +44042,7 @@ function findMatches(content, pattern, rule, filePath, fixTemplate) {
43889
44042
  snippet: getSnippet(content, lineNum),
43890
44043
  fix: fixTemplate?.(m)
43891
44044
  });
44045
+ if (matches.length >= MAX_MATCHES) break;
43892
44046
  }
43893
44047
  return matches;
43894
44048
  }
@@ -43904,10 +44058,26 @@ function astMatch(content, filePath, line, rule, fix) {
43904
44058
  fix
43905
44059
  };
43906
44060
  }
44061
+ var parseCacheContent = null;
44062
+ var parseCachePath = null;
44063
+ var parseCacheResult = null;
44064
+ var parseCacheValid = false;
43907
44065
  function tryParse(content, filePath) {
43908
- const parsed = parseFile(content, filePath);
43909
- if (!parsed) return null;
43910
- return { parsed, taint: buildTaintMap(parsed) };
44066
+ if (parseCacheValid && content === parseCacheContent && filePath === parseCachePath) {
44067
+ return parseCacheResult;
44068
+ }
44069
+ let result = null;
44070
+ try {
44071
+ const parsed = parseFile(content, filePath);
44072
+ result = parsed ? { parsed, taint: buildTaintMap(parsed) } : null;
44073
+ } catch {
44074
+ result = null;
44075
+ }
44076
+ parseCacheResult = result;
44077
+ parseCacheContent = content;
44078
+ parseCachePath = filePath;
44079
+ parseCacheValid = true;
44080
+ return result;
43911
44081
  }
43912
44082
  var hardcodedSecrets = {
43913
44083
  id: "VC001",
@@ -44043,7 +44213,7 @@ var missingAuthMiddleware = {
44043
44213
  category: "Authentication",
44044
44214
  description: "API routes without authentication checks allow unauthorized access.",
44045
44215
  check(content, filePath) {
44046
- if (!isServerSideFile(filePath)) return [];
44216
+ if (!isServerSideFile(filePath, content)) return [];
44047
44217
  const routePatterns = [
44048
44218
  // Express/Hono style
44049
44219
  /\.(get|post|put|patch|delete)\s*\(\s*["'`][^"'`]+["'`]\s*,\s*(?:async\s+)?\(?(?:req|c|ctx)/gi,
@@ -44227,6 +44397,41 @@ var sqlInjection = {
44227
44397
  matches.push(m);
44228
44398
  }
44229
44399
  }
44400
+ if (/\.\s*(?:query|execute|raw|queryRawUnsafe|executeRawUnsafe|\$queryRawUnsafe|\$executeRawUnsafe)\s*\(/.test(content)) {
44401
+ const ctx = tryParse(content, filePath);
44402
+ if (ctx) {
44403
+ const { parsed, taint } = ctx;
44404
+ const SQL_SINKS = /* @__PURE__ */ new Set([
44405
+ "query",
44406
+ "execute",
44407
+ "raw",
44408
+ "queryRawUnsafe",
44409
+ "executeRawUnsafe",
44410
+ "$queryRawUnsafe",
44411
+ "$executeRawUnsafe"
44412
+ ]);
44413
+ visitCalls(
44414
+ parsed,
44415
+ (callee) => callee.type === "MemberExpression" && callee.property.type === "Identifier" && SQL_SINKS.has(callee.property.name),
44416
+ (call, line) => {
44417
+ const first = call.arguments[0];
44418
+ if (!first || first.type === "SpreadElement") return;
44419
+ if (first.type === "StringLiteral" || first.type === "TemplateLiteral") return;
44420
+ if (!taint.isTainted(first)) return;
44421
+ if (matches.some((m) => m.line === line)) return;
44422
+ matches.push(
44423
+ astMatch(
44424
+ content,
44425
+ filePath,
44426
+ line,
44427
+ sqlInjection,
44428
+ "Use parameterized queries instead of building SQL from user input in a variable. Example: db.query('SELECT * FROM users WHERE login = ?', [req.body.login])"
44429
+ )
44430
+ );
44431
+ }
44432
+ );
44433
+ }
44434
+ }
44230
44435
  return matches;
44231
44436
  }
44232
44437
  };
@@ -44237,9 +44442,10 @@ var xssVulnerability = {
44237
44442
  category: "Injection",
44238
44443
  description: "Rendering user input without sanitization allows attackers to inject malicious scripts.",
44239
44444
  check(content, filePath) {
44445
+ const hasSanitizerBypass = /bypassSecurityTrust(?:Html|Script|Style|Url|ResourceUrl)\s*\(/.test(content);
44240
44446
  if (/(?:sanitize|purify|escape|xss)/i.test(filePath)) return [];
44241
- if (/DOMPurify\.sanitize|sanitizeHtml|xss\(|escapeHtml/i.test(content)) return [];
44242
- if (/(?:import|require)\s*\(?.*(?:DOMPurify|dompurify|sanitize|sanitizer)/i.test(content)) return [];
44447
+ if (!hasSanitizerBypass && /DOMPurify\.sanitize|sanitizeHtml|xss\(|escapeHtml/i.test(content)) return [];
44448
+ if (!hasSanitizerBypass && /(?:import|require)\s*\(?[^\n]{0,200}(?:DOMPurify|dompurify|sanitize|sanitizer)/i.test(content)) return [];
44243
44449
  const patterns = [
44244
44450
  // React dangerouslySetInnerHTML
44245
44451
  /dangerouslySetInnerHTML\s*=\s*\{\s*\{\s*__html\s*:/g,
@@ -44250,7 +44456,12 @@ var xssVulnerability = {
44250
44456
  // v-html in Vue
44251
44457
  /v-html\s*=/g,
44252
44458
  // {@html} in Svelte
44253
- /\{@html\s/g
44459
+ /\{@html\s/g,
44460
+ // Angular DomSanitizer escape hatches — bypassSecurityTrustHtml /
44461
+ // bypassSecurityTrustScript / ...Url / ...ResourceUrl / ...Style. These
44462
+ // mark a value as trusted and skip Angular's built-in sanitization;
44463
+ // passing user input through one is the canonical Angular DOM-XSS sink.
44464
+ /bypassSecurityTrust(?:Html|Script|Style|Url|ResourceUrl)\s*\(/g
44254
44465
  ];
44255
44466
  const allLines = content.split("\n");
44256
44467
  const matches = [];
@@ -44263,9 +44474,9 @@ var xssVulnerability = {
44263
44474
  () => "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."
44264
44475
  );
44265
44476
  for (const m of raw) {
44266
- const lineText = allLines[m.line - 1] || "";
44477
+ const lineText = (allLines[m.line - 1] || "").slice(0, 4e3);
44267
44478
  if (/\.innerHTML\s*=\s*['"]/.test(lineText) && !/\$\{/.test(lineText)) continue;
44268
- if (/\.innerHTML\s*=\s*['"][^'"]*['"]\s*$/.test(lineText)) continue;
44479
+ if (/\.innerHTML\s*=\s*['"][^'"]{0,2000}['"]\s*$/.test(lineText)) continue;
44269
44480
  if (/dangerouslySetInnerHTML/.test(lineText)) {
44270
44481
  const windowText = allLines.slice(m.line - 1, m.line + 2).join("\n");
44271
44482
  if (/JSON\.stringify/i.test(windowText)) continue;
@@ -44464,7 +44675,7 @@ var evalUsage = {
44464
44675
  /new\s+Function\s*\(\s*(?!["'`])/g
44465
44676
  ];
44466
44677
  const hasEvalInString = /["'`]eval(?:-source-map|["'`])/i.test(content);
44467
- if (hasEvalInString && !/\beval\s*\([^)]*(?:req\.|body\.|input|params|user|data)/i.test(content)) return [];
44678
+ if (hasEvalInString && !/\beval\s*\([^)]{0,500}(?:req\.|body\.|input|params|user|data)/i.test(content)) return [];
44468
44679
  const matches = [];
44469
44680
  for (const p of patterns) {
44470
44681
  const rawMatches = findMatches(
@@ -44505,7 +44716,7 @@ var unvalidatedRedirect = {
44505
44716
  const isPureLiteral = /^["'`][^"'`]*["'`]$/.test(value);
44506
44717
  if (!hasInterpolation && isPureLiteral) continue;
44507
44718
  if (isInlineSilenced(content, m.index, "VC016")) continue;
44508
- const line = content.substring(0, m.index).split("\n").length;
44719
+ const line = lineNumberAt(content, m.index);
44509
44720
  matches.push({
44510
44721
  rule: "VC016",
44511
44722
  title: unvalidatedRedirect.title,
@@ -44795,7 +45006,7 @@ var exposedStackTraces = {
44795
45006
  category: "Information Leakage",
44796
45007
  description: "Returning error.stack or detailed error messages in API responses reveals internal code paths, file structure, and dependencies to attackers.",
44797
45008
  check(content, filePath) {
44798
- if (!isServerSideFile(filePath)) return [];
45009
+ if (!isServerSideFile(filePath, content)) return [];
44799
45010
  const matches = [];
44800
45011
  const patterns = [
44801
45012
  // Sending stack trace in response
@@ -44940,7 +45151,7 @@ var dangerousInnerHTML = {
44940
45151
  if (/^[A-Z][A-Z0-9_]+$/.test(value)) continue;
44941
45152
  if (/^["'`][^$]*["'`]$/.test(value)) continue;
44942
45153
  if (/JSON\.stringify/i.test(value)) continue;
44943
- const lineNum = content.substring(0, m.index).split("\n").length;
45154
+ const lineNum = lineNumberAt(content, m.index);
44944
45155
  findings.push({
44945
45156
  rule: "VC063",
44946
45157
  title: dangerousInnerHTML.title,
@@ -44973,6 +45184,31 @@ function detectFramework(files) {
44973
45184
  if (frameworks.size === 0) frameworks.add("unknown");
44974
45185
  return [...frameworks];
44975
45186
  }
45187
+ function detectPlatform(files) {
45188
+ const platforms = /* @__PURE__ */ new Set();
45189
+ for (const { path, content } of files) {
45190
+ const p = path.toLowerCase();
45191
+ if (/(^|\/)lovable\.config\./.test(p) || /lovable-tagger/i.test(content) || /lovable\.dev/i.test(content)) {
45192
+ platforms.add("lovable");
45193
+ }
45194
+ if (/(^|\/)\.bolt\//.test(p) || /(^|\/)\.stackblitzrc$/.test(p) || /bolt\.new/i.test(content)) {
45195
+ platforms.add("bolt");
45196
+ }
45197
+ if (/(^|\/)\.replit$/.test(p) || /(^|\/)replit\.nix$/.test(p)) {
45198
+ platforms.add("replit");
45199
+ }
45200
+ if (/(^|\/)\.cursorrules$/.test(p) || /(^|\/)\.cursor\//.test(p)) {
45201
+ platforms.add("cursor");
45202
+ }
45203
+ if (/(^|\/)claude\.md$/.test(p) || /(^|\/)\.claude\//.test(p)) {
45204
+ platforms.add("claude-code");
45205
+ }
45206
+ if (/v0\.dev/i.test(content) || /generated by v0\b/i.test(content)) {
45207
+ platforms.add("v0");
45208
+ }
45209
+ }
45210
+ return [...platforms];
45211
+ }
44976
45212
  function calculateGrade(findings, _totalFiles) {
44977
45213
  if (findings.length === 0) {
44978
45214
  return { grade: "A+", score: 100, summary: "No security issues detected. Excellent." };
@@ -45294,8 +45530,13 @@ var complianceMap = {
45294
45530
  // VC209–VC210: advisory heuristics
45295
45531
  VC209: { owasp: "A04:2021", cwe: "CWE-799" },
45296
45532
  // webhook missing idempotency
45297
- VC210: { owasp: "A01:2021", cwe: "CWE-862" }
45533
+ VC210: { owasp: "A01:2021", cwe: "CWE-862" },
45298
45534
  // middleware matcher excludes /api
45535
+ // VC211–VC212: hardcoded sensitive personal data (PII) in source
45536
+ VC211: { owasp: "A02:2021", cwe: "CWE-540" },
45537
+ // hardcoded credit card number
45538
+ VC212: { owasp: "A02:2021", cwe: "CWE-540" }
45539
+ // hardcoded US SSN
45299
45540
  };
45300
45541
  var consoleLogProduction = {
45301
45542
  id: "VC097",
@@ -45431,6 +45672,17 @@ function runCustomRules(content, filePath, disabledRules = [], tier = "free", ex
45431
45672
  return findings;
45432
45673
  }
45433
45674
  if (/pro-rules-bundle|\.bundle\./i.test(filePath)) return findings;
45675
+ let maxLineLen = 0;
45676
+ {
45677
+ let lineStart = 0;
45678
+ for (let i = 0; i <= content.length; i++) {
45679
+ if (i === content.length || content.charCodeAt(i) === 10) {
45680
+ if (i - lineStart > maxLineLen) maxLineLen = i - lineStart;
45681
+ lineStart = i + 1;
45682
+ }
45683
+ }
45684
+ }
45685
+ if (maxLineLen > 5e4) return findings;
45434
45686
  if (/onboarding|demo-data|example-vulnerable|code-sample/i.test(filePath)) return findings;
45435
45687
  const ruleset = tier === "pro" && extraRules.length > 0 ? [...freeRules, ...extraRules] : freeRules;
45436
45688
  for (const rule of ruleset) {
@@ -45988,6 +46240,7 @@ function clearProRulesCache() {
45988
46240
 
45989
46241
  export {
45990
46242
  detectFramework,
46243
+ detectPlatform,
45991
46244
  calculateGrade,
45992
46245
  runCustomRules,
45993
46246
  filterFalsePositives,
@@ -46005,4 +46258,4 @@ export {
46005
46258
  loadCachedProRules,
46006
46259
  clearProRulesCache
46007
46260
  };
46008
- //# sourceMappingURL=chunk-KE2IZNH6.js.map
46261
+ //# sourceMappingURL=chunk-4OFR3SIS.js.map