xploitscan-shared-rules 1.8.1 → 1.10.0
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 +1 -1
- package/dist/index.cjs +210 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +210 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ This package is the source of truth for XploitScan's detection logic. It's consu
|
|
|
6
6
|
|
|
7
7
|
## What's in here
|
|
8
8
|
|
|
9
|
-
- **
|
|
9
|
+
- **210 security rules** — pattern + AST-based detection for hardcoded secrets, SQL injection, XSS, SSRF, prototype pollution, crypto misuse, deserialization, JWT alg confusion, and more.
|
|
10
10
|
- **Compliance mappings** — each rule tagged with the SOC2, ISO 27001, and OWASP Top 10 controls it covers.
|
|
11
11
|
- **AI false-positive filter** — optional Claude Haiku integration that re-evaluates findings to suppress benign matches before reporting.
|
|
12
12
|
- **Entropy-based secret scanner** — detects high-entropy strings that look like credentials but don't match a known service-key pattern.
|
package/dist/index.cjs
CHANGED
|
@@ -594,8 +594,140 @@ var TAINTED_REQUEST_OBJECTS = /* @__PURE__ */ new Set([
|
|
|
594
594
|
"event"
|
|
595
595
|
// Lambda
|
|
596
596
|
]);
|
|
597
|
+
var FS_READ_METHODS = /* @__PURE__ */ new Set(["readFile", "readFileSync"]);
|
|
598
|
+
var PARSE_OBJECTS = /* @__PURE__ */ new Set([
|
|
599
|
+
"JSON",
|
|
600
|
+
"csv",
|
|
601
|
+
"papa",
|
|
602
|
+
"Papa",
|
|
603
|
+
"yaml",
|
|
604
|
+
"YAML",
|
|
605
|
+
"toml",
|
|
606
|
+
"qs",
|
|
607
|
+
"querystring"
|
|
608
|
+
]);
|
|
609
|
+
var PARSE_BARE = /* @__PURE__ */ new Set(["parse", "parseSync"]);
|
|
610
|
+
var ARRAY_ELEMENT_METHODS = /* @__PURE__ */ new Set([
|
|
611
|
+
"map",
|
|
612
|
+
"filter",
|
|
613
|
+
"forEach",
|
|
614
|
+
"find",
|
|
615
|
+
"findIndex",
|
|
616
|
+
"flatMap",
|
|
617
|
+
"some",
|
|
618
|
+
"every"
|
|
619
|
+
]);
|
|
620
|
+
var ARRAY_REDUCE_METHODS = /* @__PURE__ */ new Set(["reduce", "reduceRight"]);
|
|
621
|
+
function callPreservesTaint(node, rec) {
|
|
622
|
+
const callee = node.callee;
|
|
623
|
+
const anyArgTainted = () => node.arguments.some((a) => a.type !== "SpreadElement" && rec(a));
|
|
624
|
+
if (callee.type === "MemberExpression" && callee.property.type === "Identifier") {
|
|
625
|
+
const pname = callee.property.name;
|
|
626
|
+
if (FS_READ_METHODS.has(pname)) return anyArgTainted();
|
|
627
|
+
if (pname === "parse" || pname === "parseSync") {
|
|
628
|
+
const obj = callee.object;
|
|
629
|
+
if (obj.type === "Identifier" && PARSE_OBJECTS.has(obj.name)) return anyArgTainted();
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
if (callee.type === "Identifier") {
|
|
633
|
+
if (FS_READ_METHODS.has(callee.name)) return anyArgTainted();
|
|
634
|
+
if (PARSE_BARE.has(callee.name)) return anyArgTainted();
|
|
635
|
+
}
|
|
636
|
+
return false;
|
|
637
|
+
}
|
|
638
|
+
function isParamDerived(n, p) {
|
|
639
|
+
if (!n) return false;
|
|
640
|
+
if (n.type === "Identifier") return n.name === p;
|
|
641
|
+
if (n.type === "MemberExpression") return isParamDerived(n.object, p);
|
|
642
|
+
return false;
|
|
643
|
+
}
|
|
644
|
+
function formattedFlow(n, p) {
|
|
645
|
+
if (!n) return false;
|
|
646
|
+
switch (n.type) {
|
|
647
|
+
case "TemplateLiteral":
|
|
648
|
+
return n.expressions.some(
|
|
649
|
+
(e) => isParamDerived(e, p) || formattedFlow(e, p)
|
|
650
|
+
);
|
|
651
|
+
case "BinaryExpression":
|
|
652
|
+
if (n.operator !== "+") return false;
|
|
653
|
+
return isParamDerived(n.left, p) || isParamDerived(n.right, p) || formattedFlow(n.left, p) || formattedFlow(n.right, p);
|
|
654
|
+
case "ObjectExpression":
|
|
655
|
+
return n.properties.some(
|
|
656
|
+
(pr) => pr.type === "SpreadElement" && isParamDerived(pr.argument, p)
|
|
657
|
+
);
|
|
658
|
+
case "ArrayExpression":
|
|
659
|
+
return n.elements.some(
|
|
660
|
+
(el) => el != null && el.type === "SpreadElement" && isParamDerived(el.argument, p)
|
|
661
|
+
);
|
|
662
|
+
case "ConditionalExpression":
|
|
663
|
+
return formattedFlow(n.consequent, p) || formattedFlow(n.alternate, p);
|
|
664
|
+
case "AwaitExpression":
|
|
665
|
+
return formattedFlow(n.argument, p);
|
|
666
|
+
default:
|
|
667
|
+
return false;
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
function collectReturnArgs(fn) {
|
|
671
|
+
const out = [];
|
|
672
|
+
if (fn.type === "ArrowFunctionExpression" && fn.body.type !== "BlockStatement") {
|
|
673
|
+
out.push(fn.body);
|
|
674
|
+
return out;
|
|
675
|
+
}
|
|
676
|
+
const visit = (n) => {
|
|
677
|
+
if (!n || typeof n !== "object") return;
|
|
678
|
+
const node = n;
|
|
679
|
+
if (typeof node.type !== "string") return;
|
|
680
|
+
if (node.type === "ReturnStatement") {
|
|
681
|
+
const arg = node.argument;
|
|
682
|
+
if (arg) out.push(arg);
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
if (node.type === "FunctionDeclaration" || node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression") {
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
for (const key of Object.keys(node)) {
|
|
689
|
+
const v = node[key];
|
|
690
|
+
if (Array.isArray(v)) v.forEach(visit);
|
|
691
|
+
else visit(v);
|
|
692
|
+
}
|
|
693
|
+
};
|
|
694
|
+
visit(fn.body);
|
|
695
|
+
return out;
|
|
696
|
+
}
|
|
697
|
+
function computePassthrough(fn) {
|
|
698
|
+
const params = fn.params ?? [];
|
|
699
|
+
const returns = collectReturnArgs(fn);
|
|
700
|
+
const out = /* @__PURE__ */ new Set();
|
|
701
|
+
for (let i = 0; i < params.length; i++) {
|
|
702
|
+
const param = params[i];
|
|
703
|
+
if (param.type !== "Identifier") continue;
|
|
704
|
+
const name = param.name;
|
|
705
|
+
if (returns.some((r) => formattedFlow(r, name))) out.add(i);
|
|
706
|
+
}
|
|
707
|
+
return out;
|
|
708
|
+
}
|
|
597
709
|
function buildTaintMap(parsed) {
|
|
598
710
|
const tainted = /* @__PURE__ */ new Set();
|
|
711
|
+
function markPatternTainted(target) {
|
|
712
|
+
if (target.type === "Identifier") {
|
|
713
|
+
tainted.add(target.name);
|
|
714
|
+
} else if (target.type === "ObjectPattern") {
|
|
715
|
+
for (const prop of target.properties) {
|
|
716
|
+
if (prop.type === "ObjectProperty" && prop.value.type === "Identifier") {
|
|
717
|
+
tainted.add(prop.value.name);
|
|
718
|
+
} else if (prop.type === "RestElement" && prop.argument.type === "Identifier") {
|
|
719
|
+
tainted.add(prop.argument.name);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
} else if (target.type === "ArrayPattern") {
|
|
723
|
+
for (const el of target.elements) {
|
|
724
|
+
if (el && el.type === "Identifier") tainted.add(el.name);
|
|
725
|
+
else if (el && el.type === "RestElement" && el.argument.type === "Identifier") {
|
|
726
|
+
tainted.add(el.argument.name);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
}
|
|
599
731
|
function reachesRequestIdent(node) {
|
|
600
732
|
if (!node) return false;
|
|
601
733
|
if (node.type === "Identifier") return TAINTED_REQUEST_OBJECTS.has(node.name);
|
|
@@ -666,6 +798,8 @@ function buildTaintMap(parsed) {
|
|
|
666
798
|
}
|
|
667
799
|
if (node.type === "CallExpression") {
|
|
668
800
|
if (nodeIsTaintedSource(node)) return true;
|
|
801
|
+
if (callPreservesTaint(node, exprIsTainted)) return true;
|
|
802
|
+
if (localCallPropagates(node, exprIsTainted)) return true;
|
|
669
803
|
if (node.callee.type === "MemberExpression") {
|
|
670
804
|
if (exprIsTainted(node.callee.object)) return true;
|
|
671
805
|
const obj = node.callee.object;
|
|
@@ -686,8 +820,46 @@ function buildTaintMap(parsed) {
|
|
|
686
820
|
if (node.type === "AwaitExpression") {
|
|
687
821
|
return exprIsTainted(node.argument);
|
|
688
822
|
}
|
|
823
|
+
if (node.type === "ArrayExpression") {
|
|
824
|
+
return node.elements.some(
|
|
825
|
+
(el) => el != null && el.type === "SpreadElement" && exprIsTainted(el.argument)
|
|
826
|
+
);
|
|
827
|
+
}
|
|
828
|
+
if (node.type === "ObjectExpression") {
|
|
829
|
+
return node.properties.some(
|
|
830
|
+
(p) => p.type === "SpreadElement" && exprIsTainted(p.argument)
|
|
831
|
+
);
|
|
832
|
+
}
|
|
689
833
|
return false;
|
|
690
834
|
}
|
|
835
|
+
const fnPassthrough = /* @__PURE__ */ new Map();
|
|
836
|
+
const recordFn = (name, fn) => {
|
|
837
|
+
const pt = computePassthrough(fn);
|
|
838
|
+
if (pt.size > 0) fnPassthrough.set(name, pt);
|
|
839
|
+
};
|
|
840
|
+
traverse(parsed.ast, {
|
|
841
|
+
FunctionDeclaration(path) {
|
|
842
|
+
const n = path.node;
|
|
843
|
+
if (n.type === "FunctionDeclaration" && n.id && n.id.type === "Identifier") {
|
|
844
|
+
recordFn(n.id.name, n);
|
|
845
|
+
}
|
|
846
|
+
},
|
|
847
|
+
VariableDeclarator(path) {
|
|
848
|
+
const n = path.node;
|
|
849
|
+
if (n.type !== "VariableDeclarator" || n.id.type !== "Identifier" || !n.init) return;
|
|
850
|
+
if (n.init.type === "ArrowFunctionExpression" || n.init.type === "FunctionExpression") {
|
|
851
|
+
recordFn(n.id.name, n.init);
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
});
|
|
855
|
+
function localCallPropagates(node, rec) {
|
|
856
|
+
if (node.callee.type !== "Identifier") return false;
|
|
857
|
+
const pt = fnPassthrough.get(node.callee.name);
|
|
858
|
+
if (!pt) return false;
|
|
859
|
+
return node.arguments.some(
|
|
860
|
+
(a, i) => pt.has(i) && a.type !== "SpreadElement" && rec(a)
|
|
861
|
+
);
|
|
862
|
+
}
|
|
691
863
|
traverse(parsed.ast, {
|
|
692
864
|
VariableDeclarator(path) {
|
|
693
865
|
const node = path.node;
|
|
@@ -720,6 +892,32 @@ function buildTaintMap(parsed) {
|
|
|
720
892
|
if (exprIsTainted(node.right)) {
|
|
721
893
|
tainted.add(node.left.name);
|
|
722
894
|
}
|
|
895
|
+
},
|
|
896
|
+
// for (const row of <tainted>) → row tainted (and destructured names).
|
|
897
|
+
ForOfStatement(path) {
|
|
898
|
+
const node = path.node;
|
|
899
|
+
if (node.type !== "ForOfStatement") return;
|
|
900
|
+
if (!exprIsTainted(node.right)) return;
|
|
901
|
+
const decl = node.left;
|
|
902
|
+
const target = decl.type === "VariableDeclaration" && decl.declarations[0] ? decl.declarations[0].id : decl;
|
|
903
|
+
markPatternTainted(target);
|
|
904
|
+
},
|
|
905
|
+
// Array-iteration element taint: <tainted>.map((row) => …) marks `row`
|
|
906
|
+
// tainted inside the callback. Gated on the receiver already being
|
|
907
|
+
// tainted, so this never originates taint.
|
|
908
|
+
CallExpression(path) {
|
|
909
|
+
const node = path.node;
|
|
910
|
+
if (node.type !== "CallExpression") return;
|
|
911
|
+
const callee = node.callee;
|
|
912
|
+
if (callee.type !== "MemberExpression" || callee.property.type !== "Identifier") return;
|
|
913
|
+
const method = callee.property.name;
|
|
914
|
+
const isReduce = ARRAY_REDUCE_METHODS.has(method);
|
|
915
|
+
if (!ARRAY_ELEMENT_METHODS.has(method) && !isReduce) return;
|
|
916
|
+
if (!exprIsTainted(callee.object)) return;
|
|
917
|
+
const cb = node.arguments[0];
|
|
918
|
+
if (!cb || cb.type !== "ArrowFunctionExpression" && cb.type !== "FunctionExpression") return;
|
|
919
|
+
const elemParam = cb.params[isReduce ? 1 : 0];
|
|
920
|
+
if (elemParam) markPatternTainted(elemParam);
|
|
723
921
|
}
|
|
724
922
|
});
|
|
725
923
|
const isTainted = (node) => {
|
|
@@ -745,6 +943,8 @@ function buildTaintMap(parsed) {
|
|
|
745
943
|
return isTainted(node.object);
|
|
746
944
|
}
|
|
747
945
|
if (node.type === "CallExpression") {
|
|
946
|
+
if (callPreservesTaint(node, isTainted)) return true;
|
|
947
|
+
if (localCallPropagates(node, isTainted)) return true;
|
|
748
948
|
if (node.callee.type === "MemberExpression") {
|
|
749
949
|
if (isTainted(node.callee.object)) return true;
|
|
750
950
|
const obj = node.callee.object;
|
|
@@ -762,6 +962,16 @@ function buildTaintMap(parsed) {
|
|
|
762
962
|
}
|
|
763
963
|
}
|
|
764
964
|
}
|
|
965
|
+
if (node.type === "ArrayExpression") {
|
|
966
|
+
return node.elements.some(
|
|
967
|
+
(el) => el != null && el.type === "SpreadElement" && isTainted(el.argument)
|
|
968
|
+
);
|
|
969
|
+
}
|
|
970
|
+
if (node.type === "ObjectExpression") {
|
|
971
|
+
return node.properties.some(
|
|
972
|
+
(p) => p.type === "SpreadElement" && isTainted(p.argument)
|
|
973
|
+
);
|
|
974
|
+
}
|
|
765
975
|
return false;
|
|
766
976
|
};
|
|
767
977
|
return {
|