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.
- package/README.md +29 -5
- package/dist/{api-GHG3FCFB.js → api-MFCSQQ2Y.js} +2 -2
- package/dist/{chunk-HYXMH2H6.js → chunk-4OFR3SIS.js} +388 -26
- package/dist/{chunk-HYXMH2H6.js.map → chunk-4OFR3SIS.js.map} +1 -1
- package/dist/index.js +146 -57
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- /package/dist/{api-GHG3FCFB.js.map → api-MFCSQQ2Y.js.map} +0 -0
|
@@ -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();
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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*[:=
|
|
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
|
-
|
|
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
|
-
|
|
43800
|
-
|
|
43801
|
-
|
|
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*\(
|
|
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*['"][^'"]
|
|
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*\([^)]
|
|
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
|
|
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
|
|
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-
|
|
46261
|
+
//# sourceMappingURL=chunk-4OFR3SIS.js.map
|