xploitscan 1.1.11 → 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.
@@ -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();
@@ -43509,8 +43548,140 @@ var TAINTED_REQUEST_OBJECTS = /* @__PURE__ */ new Set([
43509
43548
  "event"
43510
43549
  // Lambda
43511
43550
  ]);
43551
+ var FS_READ_METHODS = /* @__PURE__ */ new Set(["readFile", "readFileSync"]);
43552
+ var PARSE_OBJECTS = /* @__PURE__ */ new Set([
43553
+ "JSON",
43554
+ "csv",
43555
+ "papa",
43556
+ "Papa",
43557
+ "yaml",
43558
+ "YAML",
43559
+ "toml",
43560
+ "qs",
43561
+ "querystring"
43562
+ ]);
43563
+ var PARSE_BARE = /* @__PURE__ */ new Set(["parse", "parseSync"]);
43564
+ var ARRAY_ELEMENT_METHODS = /* @__PURE__ */ new Set([
43565
+ "map",
43566
+ "filter",
43567
+ "forEach",
43568
+ "find",
43569
+ "findIndex",
43570
+ "flatMap",
43571
+ "some",
43572
+ "every"
43573
+ ]);
43574
+ var ARRAY_REDUCE_METHODS = /* @__PURE__ */ new Set(["reduce", "reduceRight"]);
43575
+ function callPreservesTaint(node, rec) {
43576
+ const callee = node.callee;
43577
+ const anyArgTainted = () => node.arguments.some((a) => a.type !== "SpreadElement" && rec(a));
43578
+ if (callee.type === "MemberExpression" && callee.property.type === "Identifier") {
43579
+ const pname = callee.property.name;
43580
+ if (FS_READ_METHODS.has(pname)) return anyArgTainted();
43581
+ if (pname === "parse" || pname === "parseSync") {
43582
+ const obj = callee.object;
43583
+ if (obj.type === "Identifier" && PARSE_OBJECTS.has(obj.name)) return anyArgTainted();
43584
+ }
43585
+ }
43586
+ if (callee.type === "Identifier") {
43587
+ if (FS_READ_METHODS.has(callee.name)) return anyArgTainted();
43588
+ if (PARSE_BARE.has(callee.name)) return anyArgTainted();
43589
+ }
43590
+ return false;
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
+ }
43512
43663
  function buildTaintMap(parsed) {
43513
43664
  const tainted = /* @__PURE__ */ new Set();
43665
+ function markPatternTainted(target) {
43666
+ if (target.type === "Identifier") {
43667
+ tainted.add(target.name);
43668
+ } else if (target.type === "ObjectPattern") {
43669
+ for (const prop of target.properties) {
43670
+ if (prop.type === "ObjectProperty" && prop.value.type === "Identifier") {
43671
+ tainted.add(prop.value.name);
43672
+ } else if (prop.type === "RestElement" && prop.argument.type === "Identifier") {
43673
+ tainted.add(prop.argument.name);
43674
+ }
43675
+ }
43676
+ } else if (target.type === "ArrayPattern") {
43677
+ for (const el of target.elements) {
43678
+ if (el && el.type === "Identifier") tainted.add(el.name);
43679
+ else if (el && el.type === "RestElement" && el.argument.type === "Identifier") {
43680
+ tainted.add(el.argument.name);
43681
+ }
43682
+ }
43683
+ }
43684
+ }
43514
43685
  function reachesRequestIdent(node) {
43515
43686
  if (!node) return false;
43516
43687
  if (node.type === "Identifier") return TAINTED_REQUEST_OBJECTS.has(node.name);
@@ -43581,6 +43752,8 @@ function buildTaintMap(parsed) {
43581
43752
  }
43582
43753
  if (node.type === "CallExpression") {
43583
43754
  if (nodeIsTaintedSource(node)) return true;
43755
+ if (callPreservesTaint(node, exprIsTainted)) return true;
43756
+ if (localCallPropagates(node, exprIsTainted)) return true;
43584
43757
  if (node.callee.type === "MemberExpression") {
43585
43758
  if (exprIsTainted(node.callee.object)) return true;
43586
43759
  const obj = node.callee.object;
@@ -43601,8 +43774,46 @@ function buildTaintMap(parsed) {
43601
43774
  if (node.type === "AwaitExpression") {
43602
43775
  return exprIsTainted(node.argument);
43603
43776
  }
43777
+ if (node.type === "ArrayExpression") {
43778
+ return node.elements.some(
43779
+ (el) => el != null && el.type === "SpreadElement" && exprIsTainted(el.argument)
43780
+ );
43781
+ }
43782
+ if (node.type === "ObjectExpression") {
43783
+ return node.properties.some(
43784
+ (p) => p.type === "SpreadElement" && exprIsTainted(p.argument)
43785
+ );
43786
+ }
43604
43787
  return false;
43605
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
+ }
43606
43817
  traverse(parsed.ast, {
43607
43818
  VariableDeclarator(path) {
43608
43819
  const node = path.node;
@@ -43635,6 +43846,32 @@ function buildTaintMap(parsed) {
43635
43846
  if (exprIsTainted(node.right)) {
43636
43847
  tainted.add(node.left.name);
43637
43848
  }
43849
+ },
43850
+ // for (const row of <tainted>) → row tainted (and destructured names).
43851
+ ForOfStatement(path) {
43852
+ const node = path.node;
43853
+ if (node.type !== "ForOfStatement") return;
43854
+ if (!exprIsTainted(node.right)) return;
43855
+ const decl = node.left;
43856
+ const target = decl.type === "VariableDeclaration" && decl.declarations[0] ? decl.declarations[0].id : decl;
43857
+ markPatternTainted(target);
43858
+ },
43859
+ // Array-iteration element taint: <tainted>.map((row) => …) marks `row`
43860
+ // tainted inside the callback. Gated on the receiver already being
43861
+ // tainted, so this never originates taint.
43862
+ CallExpression(path) {
43863
+ const node = path.node;
43864
+ if (node.type !== "CallExpression") return;
43865
+ const callee = node.callee;
43866
+ if (callee.type !== "MemberExpression" || callee.property.type !== "Identifier") return;
43867
+ const method = callee.property.name;
43868
+ const isReduce = ARRAY_REDUCE_METHODS.has(method);
43869
+ if (!ARRAY_ELEMENT_METHODS.has(method) && !isReduce) return;
43870
+ if (!exprIsTainted(callee.object)) return;
43871
+ const cb = node.arguments[0];
43872
+ if (!cb || cb.type !== "ArrowFunctionExpression" && cb.type !== "FunctionExpression") return;
43873
+ const elemParam = cb.params[isReduce ? 1 : 0];
43874
+ if (elemParam) markPatternTainted(elemParam);
43638
43875
  }
43639
43876
  });
43640
43877
  const isTainted = (node) => {
@@ -43660,6 +43897,8 @@ function buildTaintMap(parsed) {
43660
43897
  return isTainted(node.object);
43661
43898
  }
43662
43899
  if (node.type === "CallExpression") {
43900
+ if (callPreservesTaint(node, isTainted)) return true;
43901
+ if (localCallPropagates(node, isTainted)) return true;
43663
43902
  if (node.callee.type === "MemberExpression") {
43664
43903
  if (isTainted(node.callee.object)) return true;
43665
43904
  const obj = node.callee.object;
@@ -43677,6 +43916,16 @@ function buildTaintMap(parsed) {
43677
43916
  }
43678
43917
  }
43679
43918
  }
43919
+ if (node.type === "ArrayExpression") {
43920
+ return node.elements.some(
43921
+ (el) => el != null && el.type === "SpreadElement" && isTainted(el.argument)
43922
+ );
43923
+ }
43924
+ if (node.type === "ObjectExpression") {
43925
+ return node.properties.some(
43926
+ (p) => p.type === "SpreadElement" && isTainted(p.argument)
43927
+ );
43928
+ }
43680
43929
  return false;
43681
43930
  };
43682
43931
  return {
@@ -43725,9 +43974,12 @@ var SERVER_SIDE_PATH_RE = new RegExp(
43725
43974
  ].join("|"),
43726
43975
  "i"
43727
43976
  );
43728
- 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) {
43729
43979
  if (isTestFile(filePath)) return false;
43730
- 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;
43731
43983
  }
43732
43984
  var CONFIG_FILE_PATTERN = new RegExp(
43733
43985
  [
@@ -43748,7 +44000,7 @@ function isCommentLine(content, matchIndex) {
43748
44000
  function isInsideFixMessage(content, matchIndex) {
43749
44001
  const lineStart = content.lastIndexOf("\n", matchIndex - 1) + 1;
43750
44002
  const lineText = content.substring(lineStart, content.indexOf("\n", matchIndex));
43751
- 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);
43752
44004
  }
43753
44005
  function isInlineSilenced(content, matchIndex, ruleId) {
43754
44006
  const lines = content.split("\n");
@@ -43763,13 +44015,23 @@ function isInlineSilenced(content, matchIndex, ruleId) {
43763
44015
  }
43764
44016
  function findMatches(content, pattern, rule, filePath, fixTemplate) {
43765
44017
  const matches = [];
43766
- const lines = content.split("\n");
43767
44018
  let m;
43768
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;
43769
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;
43770
44030
  if (isCommentLine(content, m.index)) continue;
43771
44031
  if (isInsideFixMessage(content, m.index)) continue;
43772
- const lineNum = content.substring(0, m.index).split("\n").length;
44032
+ for (; scanned < m.index; scanned++) {
44033
+ if (content.charCodeAt(scanned) === 10) lineNum++;
44034
+ }
43773
44035
  matches.push({
43774
44036
  rule: rule.id,
43775
44037
  title: rule.title,
@@ -43780,6 +44042,7 @@ function findMatches(content, pattern, rule, filePath, fixTemplate) {
43780
44042
  snippet: getSnippet(content, lineNum),
43781
44043
  fix: fixTemplate?.(m)
43782
44044
  });
44045
+ if (matches.length >= MAX_MATCHES) break;
43783
44046
  }
43784
44047
  return matches;
43785
44048
  }
@@ -43795,10 +44058,26 @@ function astMatch(content, filePath, line, rule, fix) {
43795
44058
  fix
43796
44059
  };
43797
44060
  }
44061
+ var parseCacheContent = null;
44062
+ var parseCachePath = null;
44063
+ var parseCacheResult = null;
44064
+ var parseCacheValid = false;
43798
44065
  function tryParse(content, filePath) {
43799
- const parsed = parseFile(content, filePath);
43800
- if (!parsed) return null;
43801
- 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;
43802
44081
  }
43803
44082
  var hardcodedSecrets = {
43804
44083
  id: "VC001",
@@ -43934,7 +44213,7 @@ var missingAuthMiddleware = {
43934
44213
  category: "Authentication",
43935
44214
  description: "API routes without authentication checks allow unauthorized access.",
43936
44215
  check(content, filePath) {
43937
- if (!isServerSideFile(filePath)) return [];
44216
+ if (!isServerSideFile(filePath, content)) return [];
43938
44217
  const routePatterns = [
43939
44218
  // Express/Hono style
43940
44219
  /\.(get|post|put|patch|delete)\s*\(\s*["'`][^"'`]+["'`]\s*,\s*(?:async\s+)?\(?(?:req|c|ctx)/gi,
@@ -44118,6 +44397,41 @@ var sqlInjection = {
44118
44397
  matches.push(m);
44119
44398
  }
44120
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
+ }
44121
44435
  return matches;
44122
44436
  }
44123
44437
  };
@@ -44128,9 +44442,10 @@ var xssVulnerability = {
44128
44442
  category: "Injection",
44129
44443
  description: "Rendering user input without sanitization allows attackers to inject malicious scripts.",
44130
44444
  check(content, filePath) {
44445
+ const hasSanitizerBypass = /bypassSecurityTrust(?:Html|Script|Style|Url|ResourceUrl)\s*\(/.test(content);
44131
44446
  if (/(?:sanitize|purify|escape|xss)/i.test(filePath)) return [];
44132
- if (/DOMPurify\.sanitize|sanitizeHtml|xss\(|escapeHtml/i.test(content)) return [];
44133
- 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 [];
44134
44449
  const patterns = [
44135
44450
  // React dangerouslySetInnerHTML
44136
44451
  /dangerouslySetInnerHTML\s*=\s*\{\s*\{\s*__html\s*:/g,
@@ -44141,7 +44456,12 @@ var xssVulnerability = {
44141
44456
  // v-html in Vue
44142
44457
  /v-html\s*=/g,
44143
44458
  // {@html} in Svelte
44144
- /\{@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
44145
44465
  ];
44146
44466
  const allLines = content.split("\n");
44147
44467
  const matches = [];
@@ -44154,9 +44474,9 @@ var xssVulnerability = {
44154
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."
44155
44475
  );
44156
44476
  for (const m of raw) {
44157
- const lineText = allLines[m.line - 1] || "";
44477
+ const lineText = (allLines[m.line - 1] || "").slice(0, 4e3);
44158
44478
  if (/\.innerHTML\s*=\s*['"]/.test(lineText) && !/\$\{/.test(lineText)) continue;
44159
- if (/\.innerHTML\s*=\s*['"][^'"]*['"]\s*$/.test(lineText)) continue;
44479
+ if (/\.innerHTML\s*=\s*['"][^'"]{0,2000}['"]\s*$/.test(lineText)) continue;
44160
44480
  if (/dangerouslySetInnerHTML/.test(lineText)) {
44161
44481
  const windowText = allLines.slice(m.line - 1, m.line + 2).join("\n");
44162
44482
  if (/JSON\.stringify/i.test(windowText)) continue;
@@ -44355,7 +44675,7 @@ var evalUsage = {
44355
44675
  /new\s+Function\s*\(\s*(?!["'`])/g
44356
44676
  ];
44357
44677
  const hasEvalInString = /["'`]eval(?:-source-map|["'`])/i.test(content);
44358
- 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 [];
44359
44679
  const matches = [];
44360
44680
  for (const p of patterns) {
44361
44681
  const rawMatches = findMatches(
@@ -44396,7 +44716,7 @@ var unvalidatedRedirect = {
44396
44716
  const isPureLiteral = /^["'`][^"'`]*["'`]$/.test(value);
44397
44717
  if (!hasInterpolation && isPureLiteral) continue;
44398
44718
  if (isInlineSilenced(content, m.index, "VC016")) continue;
44399
- const line = content.substring(0, m.index).split("\n").length;
44719
+ const line = lineNumberAt(content, m.index);
44400
44720
  matches.push({
44401
44721
  rule: "VC016",
44402
44722
  title: unvalidatedRedirect.title,
@@ -44686,7 +45006,7 @@ var exposedStackTraces = {
44686
45006
  category: "Information Leakage",
44687
45007
  description: "Returning error.stack or detailed error messages in API responses reveals internal code paths, file structure, and dependencies to attackers.",
44688
45008
  check(content, filePath) {
44689
- if (!isServerSideFile(filePath)) return [];
45009
+ if (!isServerSideFile(filePath, content)) return [];
44690
45010
  const matches = [];
44691
45011
  const patterns = [
44692
45012
  // Sending stack trace in response
@@ -44831,7 +45151,7 @@ var dangerousInnerHTML = {
44831
45151
  if (/^[A-Z][A-Z0-9_]+$/.test(value)) continue;
44832
45152
  if (/^["'`][^$]*["'`]$/.test(value)) continue;
44833
45153
  if (/JSON\.stringify/i.test(value)) continue;
44834
- const lineNum = content.substring(0, m.index).split("\n").length;
45154
+ const lineNum = lineNumberAt(content, m.index);
44835
45155
  findings.push({
44836
45156
  rule: "VC063",
44837
45157
  title: dangerousInnerHTML.title,
@@ -44864,6 +45184,31 @@ function detectFramework(files) {
44864
45184
  if (frameworks.size === 0) frameworks.add("unknown");
44865
45185
  return [...frameworks];
44866
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
+ }
44867
45212
  function calculateGrade(findings, _totalFiles) {
44868
45213
  if (findings.length === 0) {
44869
45214
  return { grade: "A+", score: 100, summary: "No security issues detected. Excellent." };
@@ -45185,8 +45530,13 @@ var complianceMap = {
45185
45530
  // VC209–VC210: advisory heuristics
45186
45531
  VC209: { owasp: "A04:2021", cwe: "CWE-799" },
45187
45532
  // webhook missing idempotency
45188
- VC210: { owasp: "A01:2021", cwe: "CWE-862" }
45533
+ VC210: { owasp: "A01:2021", cwe: "CWE-862" },
45189
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
45190
45540
  };
45191
45541
  var consoleLogProduction = {
45192
45542
  id: "VC097",
@@ -45322,6 +45672,17 @@ function runCustomRules(content, filePath, disabledRules = [], tier = "free", ex
45322
45672
  return findings;
45323
45673
  }
45324
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;
45325
45686
  if (/onboarding|demo-data|example-vulnerable|code-sample/i.test(filePath)) return findings;
45326
45687
  const ruleset = tier === "pro" && extraRules.length > 0 ? [...freeRules, ...extraRules] : freeRules;
45327
45688
  for (const rule of ruleset) {
@@ -45879,6 +46240,7 @@ function clearProRulesCache() {
45879
46240
 
45880
46241
  export {
45881
46242
  detectFramework,
46243
+ detectPlatform,
45882
46244
  calculateGrade,
45883
46245
  runCustomRules,
45884
46246
  filterFalsePositives,
@@ -45896,4 +46258,4 @@ export {
45896
46258
  loadCachedProRules,
45897
46259
  clearProRulesCache
45898
46260
  };
45899
- //# sourceMappingURL=chunk-HYXMH2H6.js.map
46261
+ //# sourceMappingURL=chunk-4OFR3SIS.js.map