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 +5 -5
- package/dist/{api-4UFZFIDO.js → api-MFCSQQ2Y.js} +2 -2
- package/dist/{chunk-KE2IZNH6.js → chunk-4OFR3SIS.js} +279 -26
- package/dist/{chunk-KE2IZNH6.js.map → chunk-4OFR3SIS.js.map} +1 -1
- package/dist/index.js +39 -12
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- /package/dist/{api-4UFZFIDO.js.map → api-MFCSQQ2Y.js.map} +0 -0
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
|
|
40
|
-
- **
|
|
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-
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
43428
|
-
|
|
43464
|
+
const out = [];
|
|
43465
|
+
for (let i = start; i < end; i++) {
|
|
43466
|
+
const lineNum = i + 1;
|
|
43429
43467
|
const marker = lineNum === line ? ">" : " ";
|
|
43430
|
-
|
|
43431
|
-
}
|
|
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
|
-
|
|
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
|
-
|
|
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*[:=
|
|
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
|
-
|
|
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
|
-
|
|
43909
|
-
|
|
43910
|
-
|
|
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*\(
|
|
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*['"][^'"]
|
|
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*\([^)]
|
|
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
|
|
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
|
|
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-
|
|
46261
|
+
//# sourceMappingURL=chunk-4OFR3SIS.js.map
|