opensecurity 0.2.1 → 0.3.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 +2 -1
- package/dist/adapters/semgrep.js +1 -1
- package/dist/core/config.js +72 -0
- package/dist/core/scan.js +1336 -0
- package/dist/engines/analysis/ast.js +20 -0
- package/dist/engines/analysis/graphs.js +300 -0
- package/dist/engines/analysis/infraPatterns.js +196 -0
- package/dist/engines/analysis/patterns.js +237 -0
- package/dist/engines/analysis/rules.js +48 -0
- package/dist/engines/analysis/taint.js +294 -0
- package/dist/engines/analysis/universalPatterns.js +56 -0
- package/dist/engines/deps/cve.js +102 -0
- package/dist/engines/deps/engine.js +27 -0
- package/dist/engines/deps/patch.js +11 -0
- package/dist/engines/deps/scanners.js +114 -0
- package/dist/engines/deps/scoring.js +46 -0
- package/dist/engines/deps/simulate.js +9 -0
- package/dist/engines/deps/types.js +1 -0
- package/dist/engines/native/languages.js +222 -0
- package/dist/engines/native/loader.js +61 -0
- package/dist/engines/native/rules.js +14 -0
- package/dist/engines/native/taint.js +312 -0
- package/dist/engines/rules/defaultRules.js +177 -0
- package/dist/engines/rules/loadRules.js +14 -0
- package/dist/io/fileWalker.js +27 -0
- package/dist/io/login.js +583 -0
- package/dist/io/oauthStore.js +48 -0
- package/dist/io/proxy.js +93 -0
- package/dist/io/telemetry.js +72 -0
- package/dist/ui/cli.js +410 -0
- package/dist/ui/pr-comment.js +118 -0
- package/dist/ui/progress.js +150 -0
- package/package.json +5 -5
- package/rules/taint/c.json +38 -2
- package/rules/taint/cpp.json +38 -2
- package/rules/taint/go.json +16 -0
- package/rules/taint/kotlin.json +15 -0
- package/rules/taint/rust.json +16 -0
- package/rules/taint/swift.json +15 -0
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import traverseImport from "@babel/traverse";
|
|
2
|
+
import * as t from "@babel/types";
|
|
3
|
+
const SECRET_NAME_REGEX = /^(api[-_]?key|secret|token|password|passwd|pwd|private[-_]?key|access[-_]?key|client[-_]?secret)$/i;
|
|
4
|
+
const SECRET_VALUE_MIN_LEN = 16;
|
|
5
|
+
const SECRET_ENTROPY_THRESHOLD = 3.7;
|
|
6
|
+
const SECRET_VALUE_PATTERNS = [
|
|
7
|
+
{ id: "secret-aws-access-key", pattern: /AKIA[0-9A-Z]{16}/, title: "Hardcoded AWS Access Key" },
|
|
8
|
+
{ id: "secret-github", pattern: /gh[pousr]_[A-Za-z0-9]{20,}/, title: "Hardcoded GitHub Token" },
|
|
9
|
+
{ id: "secret-slack", pattern: /xox[baprs]-[A-Za-z0-9-]{10,}/, title: "Hardcoded Slack Token" },
|
|
10
|
+
{ id: "secret-stripe", pattern: /sk_live_[A-Za-z0-9]{16,}/, title: "Hardcoded Stripe Secret Key" },
|
|
11
|
+
{ id: "secret-google-api", pattern: /AIza[0-9A-Za-z\-_]{35}/, title: "Hardcoded Google API Key" },
|
|
12
|
+
{ id: "secret-jwt", pattern: /eyJ[a-zA-Z0-9_-]{10,}\.[a-zA-Z0-9_-]{10,}\.[a-zA-Z0-9_-]{10,}/, title: "Hardcoded JWT" },
|
|
13
|
+
{ id: "secret-private-key", pattern: /-----BEGIN (RSA|EC|DSA|PRIVATE) KEY-----/, title: "Hardcoded Private Key" }
|
|
14
|
+
];
|
|
15
|
+
const WEAK_HASHES = new Set(["md5", "sha1", "md4"]);
|
|
16
|
+
const WEAK_CIPHERS = ["des", "3des", "rc2", "rc4", "bf", "blowfish", "idea"];
|
|
17
|
+
const INSECURE_RANDOM_CALLS = new Set(["Math.random", "crypto.pseudoRandomBytes", "pseudoRandomBytes"]);
|
|
18
|
+
const DESERIALIZATION_CALLEES = new Set([
|
|
19
|
+
"unserialize",
|
|
20
|
+
"deserialize",
|
|
21
|
+
"serialize.unserialize",
|
|
22
|
+
"serialize.deserialize",
|
|
23
|
+
"yaml.load",
|
|
24
|
+
"YAML.load",
|
|
25
|
+
"jsyaml.load"
|
|
26
|
+
]);
|
|
27
|
+
export function runPatternDetectors(ast, filePath) {
|
|
28
|
+
const traverse = normalizeTraverse(traverseImport);
|
|
29
|
+
const findings = [];
|
|
30
|
+
const toLoc = (node) => {
|
|
31
|
+
const loc = node.loc?.start;
|
|
32
|
+
return {
|
|
33
|
+
line: loc?.line,
|
|
34
|
+
column: typeof loc?.column === "number" ? loc.column + 1 : undefined
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
const pushFinding = (finding) => {
|
|
38
|
+
findings.push(finding);
|
|
39
|
+
};
|
|
40
|
+
const markSecret = (node, title, description, id = "hardcoded-secret") => {
|
|
41
|
+
const loc = toLoc(node);
|
|
42
|
+
pushFinding({
|
|
43
|
+
id,
|
|
44
|
+
severity: "high",
|
|
45
|
+
owasp: "A07:2021 Identification and Authentication Failures",
|
|
46
|
+
title,
|
|
47
|
+
description,
|
|
48
|
+
file: filePath,
|
|
49
|
+
line: loc.line,
|
|
50
|
+
column: loc.column
|
|
51
|
+
});
|
|
52
|
+
};
|
|
53
|
+
const markCrypto = (node, title, description, id = "insecure-crypto") => {
|
|
54
|
+
const loc = toLoc(node);
|
|
55
|
+
pushFinding({
|
|
56
|
+
id,
|
|
57
|
+
severity: "high",
|
|
58
|
+
owasp: "A02:2021 Cryptographic Failures",
|
|
59
|
+
title,
|
|
60
|
+
description,
|
|
61
|
+
file: filePath,
|
|
62
|
+
line: loc.line,
|
|
63
|
+
column: loc.column
|
|
64
|
+
});
|
|
65
|
+
};
|
|
66
|
+
const markDeserialize = (node, title, description, id = "unsafe-deserialization") => {
|
|
67
|
+
const loc = toLoc(node);
|
|
68
|
+
pushFinding({
|
|
69
|
+
id,
|
|
70
|
+
severity: "high",
|
|
71
|
+
owasp: "A08:2021 Software and Data Integrity Failures",
|
|
72
|
+
title,
|
|
73
|
+
description,
|
|
74
|
+
file: filePath,
|
|
75
|
+
line: loc.line,
|
|
76
|
+
column: loc.column
|
|
77
|
+
});
|
|
78
|
+
};
|
|
79
|
+
const getStringValue = (node) => {
|
|
80
|
+
if (t.isStringLiteral(node))
|
|
81
|
+
return node.value;
|
|
82
|
+
if (t.isTemplateLiteral(node) && node.expressions.length === 0) {
|
|
83
|
+
return node.quasis.map((q) => q.value.cooked ?? "").join("");
|
|
84
|
+
}
|
|
85
|
+
return null;
|
|
86
|
+
};
|
|
87
|
+
const isSecretKeyName = (name) => {
|
|
88
|
+
if (!name)
|
|
89
|
+
return false;
|
|
90
|
+
return SECRET_NAME_REGEX.test(name);
|
|
91
|
+
};
|
|
92
|
+
const isHighEntropySecret = (value) => {
|
|
93
|
+
if (value.length < SECRET_VALUE_MIN_LEN)
|
|
94
|
+
return false;
|
|
95
|
+
return shannonEntropy(value) >= SECRET_ENTROPY_THRESHOLD;
|
|
96
|
+
};
|
|
97
|
+
const matchesSecretValue = (value) => {
|
|
98
|
+
for (const entry of SECRET_VALUE_PATTERNS) {
|
|
99
|
+
if (entry.pattern.test(value))
|
|
100
|
+
return { id: entry.id, title: entry.title };
|
|
101
|
+
}
|
|
102
|
+
return null;
|
|
103
|
+
};
|
|
104
|
+
traverse(ast, {
|
|
105
|
+
VariableDeclarator(path) {
|
|
106
|
+
if (!t.isIdentifier(path.node.id))
|
|
107
|
+
return;
|
|
108
|
+
if (!path.node.init)
|
|
109
|
+
return;
|
|
110
|
+
const value = getStringValue(path.node.init);
|
|
111
|
+
if (!value)
|
|
112
|
+
return;
|
|
113
|
+
if (value.length < SECRET_VALUE_MIN_LEN)
|
|
114
|
+
return;
|
|
115
|
+
const name = path.node.id.name;
|
|
116
|
+
const matched = matchesSecretValue(value);
|
|
117
|
+
if (matched) {
|
|
118
|
+
markSecret(path.node.init, matched.title, `Detected ${matched.title.toLowerCase()} in code.`, matched.id);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (isSecretKeyName(name)) {
|
|
122
|
+
markSecret(path.node.init, "Hardcoded Secret", `Hardcoded value assigned to '${name}'.`);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
if (isHighEntropySecret(value)) {
|
|
126
|
+
markSecret(path.node.init, "Hardcoded Secret", "High-entropy string literal may be a secret.");
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
ObjectProperty(path) {
|
|
130
|
+
if (!t.isExpression(path.node.value))
|
|
131
|
+
return;
|
|
132
|
+
const value = getStringValue(path.node.value);
|
|
133
|
+
if (!value || value.length < SECRET_VALUE_MIN_LEN)
|
|
134
|
+
return;
|
|
135
|
+
let keyName = null;
|
|
136
|
+
if (t.isIdentifier(path.node.key))
|
|
137
|
+
keyName = path.node.key.name;
|
|
138
|
+
if (t.isStringLiteral(path.node.key))
|
|
139
|
+
keyName = path.node.key.value;
|
|
140
|
+
const matched = matchesSecretValue(value);
|
|
141
|
+
if (matched) {
|
|
142
|
+
markSecret(path.node.value, matched.title, `Detected ${matched.title.toLowerCase()} in object literal.`, matched.id);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
if (isSecretKeyName(keyName)) {
|
|
146
|
+
markSecret(path.node.value, "Hardcoded Secret", `Hardcoded value for object key '${keyName}'.`);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
if (isHighEntropySecret(value)) {
|
|
150
|
+
markSecret(path.node.value, "Hardcoded Secret", "High-entropy string literal may be a secret.");
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
CallExpression(path) {
|
|
154
|
+
const calleeName = getCalleeName(path.node);
|
|
155
|
+
if (!calleeName)
|
|
156
|
+
return;
|
|
157
|
+
if (calleeName === "crypto.createHash" || calleeName === "createHash") {
|
|
158
|
+
const arg = path.node.arguments[0];
|
|
159
|
+
if (arg && t.isExpression(arg)) {
|
|
160
|
+
const value = getStringValue(arg);
|
|
161
|
+
if (value && WEAK_HASHES.has(value.toLowerCase())) {
|
|
162
|
+
markCrypto(arg, "Weak Hash Function", `Hash algorithm '${value}' is considered weak.`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
if (INSECURE_RANDOM_CALLS.has(calleeName)) {
|
|
167
|
+
markCrypto(path.node, "Insecure Randomness", "Math.random or pseudoRandomBytes is not cryptographically secure.");
|
|
168
|
+
}
|
|
169
|
+
if (calleeName === "crypto.createCipher" || calleeName === "createCipher") {
|
|
170
|
+
markCrypto(path.node, "Insecure Cipher API", "crypto.createCipher is deprecated and insecure.");
|
|
171
|
+
}
|
|
172
|
+
if (calleeName === "crypto.createDecipher" || calleeName === "createDecipher") {
|
|
173
|
+
markCrypto(path.node, "Insecure Cipher API", "crypto.createDecipher is deprecated and insecure.");
|
|
174
|
+
}
|
|
175
|
+
if (calleeName === "crypto.createCipheriv" ||
|
|
176
|
+
calleeName === "crypto.createDecipheriv" ||
|
|
177
|
+
calleeName === "createCipheriv" ||
|
|
178
|
+
calleeName === "createDecipheriv") {
|
|
179
|
+
const arg = path.node.arguments[0];
|
|
180
|
+
if (arg && t.isExpression(arg)) {
|
|
181
|
+
const value = getStringValue(arg);
|
|
182
|
+
if (value) {
|
|
183
|
+
const lower = value.toLowerCase();
|
|
184
|
+
const weak = WEAK_CIPHERS.some((alg) => lower.includes(alg)) || lower.includes("-ecb") || lower.endsWith("ecb");
|
|
185
|
+
if (weak) {
|
|
186
|
+
markCrypto(arg, "Weak Cipher Algorithm", `Cipher algorithm '${value}' is considered weak.`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
if (DESERIALIZATION_CALLEES.has(calleeName)) {
|
|
192
|
+
markDeserialize(path.node, "Unsafe Deserialization", `Call to '${calleeName}' may deserialize untrusted data.`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
return findings;
|
|
197
|
+
}
|
|
198
|
+
function getCalleeName(node) {
|
|
199
|
+
const callee = node.callee;
|
|
200
|
+
if (t.isIdentifier(callee))
|
|
201
|
+
return callee.name;
|
|
202
|
+
if (t.isMemberExpression(callee))
|
|
203
|
+
return memberExpressionToString(callee);
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
function memberExpressionToString(node) {
|
|
207
|
+
if (node.computed)
|
|
208
|
+
return null;
|
|
209
|
+
const object = node.object;
|
|
210
|
+
const property = node.property;
|
|
211
|
+
const objectName = t.isIdentifier(object)
|
|
212
|
+
? object.name
|
|
213
|
+
: t.isMemberExpression(object)
|
|
214
|
+
? memberExpressionToString(object)
|
|
215
|
+
: null;
|
|
216
|
+
if (!objectName)
|
|
217
|
+
return null;
|
|
218
|
+
if (!t.isIdentifier(property))
|
|
219
|
+
return null;
|
|
220
|
+
return `${objectName}.${property.name}`;
|
|
221
|
+
}
|
|
222
|
+
function normalizeTraverse(value) {
|
|
223
|
+
return value.default ?? value;
|
|
224
|
+
}
|
|
225
|
+
function shannonEntropy(value) {
|
|
226
|
+
const counts = new Map();
|
|
227
|
+
for (const ch of value) {
|
|
228
|
+
counts.set(ch, (counts.get(ch) ?? 0) + 1);
|
|
229
|
+
}
|
|
230
|
+
const len = value.length;
|
|
231
|
+
let entropy = 0;
|
|
232
|
+
for (const count of counts.values()) {
|
|
233
|
+
const p = count / len;
|
|
234
|
+
entropy -= p * Math.log2(p);
|
|
235
|
+
}
|
|
236
|
+
return entropy;
|
|
237
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { runTaintAnalysis } from "./taint.js";
|
|
2
|
+
export const OWASP_TOP_10 = [
|
|
3
|
+
"A01:2021 Broken Access Control",
|
|
4
|
+
"A02:2021 Cryptographic Failures",
|
|
5
|
+
"A03:2021 Injection",
|
|
6
|
+
"A04:2021 Insecure Design",
|
|
7
|
+
"A05:2021 Security Misconfiguration",
|
|
8
|
+
"A06:2021 Vulnerable and Outdated Components",
|
|
9
|
+
"A07:2021 Identification and Authentication Failures",
|
|
10
|
+
"A08:2021 Software and Data Integrity Failures",
|
|
11
|
+
"A09:2021 Security Logging and Monitoring Failures",
|
|
12
|
+
"A10:2021 Server-Side Request Forgery"
|
|
13
|
+
];
|
|
14
|
+
export function runRuleEngine(ast, filePath, rules) {
|
|
15
|
+
const findings = [];
|
|
16
|
+
for (const rule of rules) {
|
|
17
|
+
const taintFindings = runTaintAnalysis(ast, filePath, {
|
|
18
|
+
sources: rule.sources,
|
|
19
|
+
sinks: rule.sinks,
|
|
20
|
+
sanitizers: rule.sanitizers
|
|
21
|
+
});
|
|
22
|
+
for (const finding of taintFindings) {
|
|
23
|
+
findings.push({
|
|
24
|
+
ruleId: rule.id,
|
|
25
|
+
ruleName: rule.name,
|
|
26
|
+
owasp: rule.owasp,
|
|
27
|
+
severity: rule.severity,
|
|
28
|
+
file: filePath,
|
|
29
|
+
line: finding.line,
|
|
30
|
+
column: finding.column,
|
|
31
|
+
message: `${rule.name}: ${finding.message}`
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return findings;
|
|
36
|
+
}
|
|
37
|
+
export function mapFindingsToOwasp(findings) {
|
|
38
|
+
const map = new Map();
|
|
39
|
+
for (const category of OWASP_TOP_10) {
|
|
40
|
+
map.set(category, []);
|
|
41
|
+
}
|
|
42
|
+
for (const finding of findings) {
|
|
43
|
+
const bucket = map.get(finding.owasp);
|
|
44
|
+
if (bucket)
|
|
45
|
+
bucket.push(finding);
|
|
46
|
+
}
|
|
47
|
+
return map;
|
|
48
|
+
}
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import traverseImport from "@babel/traverse";
|
|
2
|
+
import * as t from "@babel/types";
|
|
3
|
+
export function runTaintAnalysis(ast, filePath, rules) {
|
|
4
|
+
const traverse = normalizeTraverse(traverseImport);
|
|
5
|
+
const findings = [];
|
|
6
|
+
const taintedVarsStack = [];
|
|
7
|
+
const sanitizedVarsStack = [];
|
|
8
|
+
const taintedExpressions = new WeakSet();
|
|
9
|
+
const sanitizedExpressions = new WeakSet();
|
|
10
|
+
const pushScope = () => taintedVarsStack.push(new Set());
|
|
11
|
+
const popScope = () => taintedVarsStack.pop();
|
|
12
|
+
const pushSanitizedScope = () => sanitizedVarsStack.push(new Set());
|
|
13
|
+
const popSanitizedScope = () => sanitizedVarsStack.pop();
|
|
14
|
+
const currentScope = () => taintedVarsStack[taintedVarsStack.length - 1];
|
|
15
|
+
const currentSanitizedScope = () => sanitizedVarsStack[sanitizedVarsStack.length - 1];
|
|
16
|
+
const isTainted = (name) => currentScope()?.has(name) ?? false;
|
|
17
|
+
const taint = (name) => currentScope()?.add(name);
|
|
18
|
+
const untaint = (name) => currentScope()?.delete(name);
|
|
19
|
+
const isSanitized = (name) => currentSanitizedScope()?.has(name) ?? false;
|
|
20
|
+
const sanitize = (name) => currentSanitizedScope()?.add(name);
|
|
21
|
+
const unsanitize = (name) => currentSanitizedScope()?.delete(name);
|
|
22
|
+
const matchEndpoint = (node, endpoints) => {
|
|
23
|
+
const calleeNames = getCalleeNames(node);
|
|
24
|
+
if (!calleeNames.length)
|
|
25
|
+
return null;
|
|
26
|
+
return endpoints.find((endpoint) => matchesAnyCallee(calleeNames, endpoint.matcher));
|
|
27
|
+
};
|
|
28
|
+
const isSanitizerCall = (node) => matchEndpoint(node, rules.sanitizers);
|
|
29
|
+
const isSourceCall = (node) => matchEndpoint(node, rules.sources);
|
|
30
|
+
const isSinkCall = (node) => matchEndpoint(node, rules.sinks);
|
|
31
|
+
const valueIsSanitized = (node) => {
|
|
32
|
+
if (sanitizedExpressions.has(node))
|
|
33
|
+
return true;
|
|
34
|
+
if (t.isIdentifier(node))
|
|
35
|
+
return isSanitized(node.name);
|
|
36
|
+
if (t.isMemberExpression(node)) {
|
|
37
|
+
return t.isExpression(node.object) ? valueIsSanitized(node.object) : false;
|
|
38
|
+
}
|
|
39
|
+
if (t.isCallExpression(node)) {
|
|
40
|
+
if (isSanitizerCall(node)) {
|
|
41
|
+
sanitizedExpressions.add(node);
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
if (t.isBinaryExpression(node) || t.isLogicalExpression(node)) {
|
|
47
|
+
return valueIsSanitized(node.left) || valueIsSanitized(node.right);
|
|
48
|
+
}
|
|
49
|
+
if (t.isConditionalExpression(node)) {
|
|
50
|
+
return (valueIsSanitized(node.test) ||
|
|
51
|
+
valueIsSanitized(node.consequent) ||
|
|
52
|
+
valueIsSanitized(node.alternate));
|
|
53
|
+
}
|
|
54
|
+
if (t.isTemplateLiteral(node)) {
|
|
55
|
+
return node.expressions.some((expr) => valueIsSanitized(expr));
|
|
56
|
+
}
|
|
57
|
+
if (t.isArrayExpression(node)) {
|
|
58
|
+
return node.elements.some((el) => (t.isExpression(el) ? valueIsSanitized(el) : false));
|
|
59
|
+
}
|
|
60
|
+
if (t.isObjectExpression(node)) {
|
|
61
|
+
return node.properties.some((prop) => {
|
|
62
|
+
if (t.isObjectProperty(prop) && t.isExpression(prop.value))
|
|
63
|
+
return valueIsSanitized(prop.value);
|
|
64
|
+
if (t.isSpreadElement(prop) && t.isExpression(prop.argument))
|
|
65
|
+
return valueIsSanitized(prop.argument);
|
|
66
|
+
return false;
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
if (t.isUnaryExpression(node)) {
|
|
70
|
+
return t.isExpression(node.argument) ? valueIsSanitized(node.argument) : false;
|
|
71
|
+
}
|
|
72
|
+
if (t.isSequenceExpression(node)) {
|
|
73
|
+
return node.expressions.some((expr) => valueIsSanitized(expr));
|
|
74
|
+
}
|
|
75
|
+
return false;
|
|
76
|
+
};
|
|
77
|
+
const valueIsTainted = (node) => {
|
|
78
|
+
if (valueIsSanitized(node))
|
|
79
|
+
return false;
|
|
80
|
+
if (taintedExpressions.has(node))
|
|
81
|
+
return true;
|
|
82
|
+
if (t.isIdentifier(node))
|
|
83
|
+
return isTainted(node.name);
|
|
84
|
+
if (t.isMemberExpression(node)) {
|
|
85
|
+
return t.isExpression(node.object) ? valueIsTainted(node.object) : false;
|
|
86
|
+
}
|
|
87
|
+
if (t.isCallExpression(node)) {
|
|
88
|
+
if (isSanitizerCall(node)) {
|
|
89
|
+
sanitizedExpressions.add(node);
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
if (isSourceCall(node))
|
|
93
|
+
return true;
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
if (t.isBinaryExpression(node) || t.isLogicalExpression(node)) {
|
|
97
|
+
return valueIsTainted(node.left) || valueIsTainted(node.right);
|
|
98
|
+
}
|
|
99
|
+
if (t.isConditionalExpression(node)) {
|
|
100
|
+
return valueIsTainted(node.test) || valueIsTainted(node.consequent) || valueIsTainted(node.alternate);
|
|
101
|
+
}
|
|
102
|
+
if (t.isTemplateLiteral(node)) {
|
|
103
|
+
return node.expressions.some((expr) => valueIsTainted(expr));
|
|
104
|
+
}
|
|
105
|
+
if (t.isArrayExpression(node)) {
|
|
106
|
+
return node.elements.some((el) => (t.isExpression(el) ? valueIsTainted(el) : false));
|
|
107
|
+
}
|
|
108
|
+
if (t.isObjectExpression(node)) {
|
|
109
|
+
return node.properties.some((prop) => {
|
|
110
|
+
if (t.isObjectProperty(prop) && t.isExpression(prop.value))
|
|
111
|
+
return valueIsTainted(prop.value);
|
|
112
|
+
if (t.isSpreadElement(prop) && t.isExpression(prop.argument))
|
|
113
|
+
return valueIsTainted(prop.argument);
|
|
114
|
+
return false;
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
if (t.isUnaryExpression(node)) {
|
|
118
|
+
return t.isExpression(node.argument) ? valueIsTainted(node.argument) : false;
|
|
119
|
+
}
|
|
120
|
+
if (t.isSequenceExpression(node)) {
|
|
121
|
+
return node.expressions.some((expr) => valueIsTainted(expr));
|
|
122
|
+
}
|
|
123
|
+
return false;
|
|
124
|
+
};
|
|
125
|
+
const handleAssignment = (id, value) => {
|
|
126
|
+
if (t.isCallExpression(value)) {
|
|
127
|
+
if (isSanitizerCall(value)) {
|
|
128
|
+
untaint(id.name);
|
|
129
|
+
sanitize(id.name);
|
|
130
|
+
sanitizedExpressions.add(value);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
if (isSourceCall(value)) {
|
|
134
|
+
taint(id.name);
|
|
135
|
+
unsanitize(id.name);
|
|
136
|
+
taintedExpressions.add(value);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (valueIsSanitized(value)) {
|
|
141
|
+
untaint(id.name);
|
|
142
|
+
sanitize(id.name);
|
|
143
|
+
}
|
|
144
|
+
else if (valueIsTainted(value)) {
|
|
145
|
+
taint(id.name);
|
|
146
|
+
unsanitize(id.name);
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
untaint(id.name);
|
|
150
|
+
unsanitize(id.name);
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
traverse(ast, {
|
|
154
|
+
Program: {
|
|
155
|
+
enter() {
|
|
156
|
+
pushScope();
|
|
157
|
+
pushSanitizedScope();
|
|
158
|
+
},
|
|
159
|
+
exit() {
|
|
160
|
+
popScope();
|
|
161
|
+
popSanitizedScope();
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
Function: {
|
|
165
|
+
enter() {
|
|
166
|
+
pushScope();
|
|
167
|
+
pushSanitizedScope();
|
|
168
|
+
},
|
|
169
|
+
exit() {
|
|
170
|
+
popScope();
|
|
171
|
+
popSanitizedScope();
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
VariableDeclarator(path) {
|
|
175
|
+
if (!path.node.init)
|
|
176
|
+
return;
|
|
177
|
+
if (!t.isIdentifier(path.node.id))
|
|
178
|
+
return;
|
|
179
|
+
handleAssignment(path.node.id, path.node.init);
|
|
180
|
+
},
|
|
181
|
+
AssignmentExpression(path) {
|
|
182
|
+
const left = path.node.left;
|
|
183
|
+
if (!t.isIdentifier(left))
|
|
184
|
+
return;
|
|
185
|
+
handleAssignment(left, path.node.right);
|
|
186
|
+
},
|
|
187
|
+
CallExpression(path) {
|
|
188
|
+
const sink = isSinkCall(path.node);
|
|
189
|
+
if (!sink)
|
|
190
|
+
return;
|
|
191
|
+
const args = path.node.arguments;
|
|
192
|
+
const hasTaintedArg = args.some((arg) => Boolean(t.isExpression(arg) && valueIsTainted(arg)));
|
|
193
|
+
if (!hasTaintedArg)
|
|
194
|
+
return;
|
|
195
|
+
const loc = path.node.loc?.start;
|
|
196
|
+
findings.push({
|
|
197
|
+
sourceId: "tainted",
|
|
198
|
+
sinkId: sink.id,
|
|
199
|
+
file: filePath,
|
|
200
|
+
line: loc?.line,
|
|
201
|
+
column: typeof loc?.column === "number" ? loc.column + 1 : undefined,
|
|
202
|
+
message: `Tainted data reaches sink ${sink.name}`
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
return findings;
|
|
207
|
+
}
|
|
208
|
+
function getCalleeNames(node) {
|
|
209
|
+
const callee = node.callee;
|
|
210
|
+
if (t.isIdentifier(callee))
|
|
211
|
+
return [callee.name];
|
|
212
|
+
if (t.isMemberExpression(callee) || t.isOptionalMemberExpression(callee)) {
|
|
213
|
+
return memberExpressionToNames(callee);
|
|
214
|
+
}
|
|
215
|
+
return [];
|
|
216
|
+
}
|
|
217
|
+
function memberExpressionToNames(node) {
|
|
218
|
+
const chain = extractMemberChain(node);
|
|
219
|
+
if (!chain.length)
|
|
220
|
+
return [];
|
|
221
|
+
const variants = new Set();
|
|
222
|
+
for (let i = 0; i < chain.length; i += 1) {
|
|
223
|
+
variants.add(chain.slice(i).join("."));
|
|
224
|
+
}
|
|
225
|
+
return Array.from(variants);
|
|
226
|
+
}
|
|
227
|
+
function extractMemberChain(node) {
|
|
228
|
+
if (node.computed) {
|
|
229
|
+
if (t.isStringLiteral(node.property)) {
|
|
230
|
+
const objectNames = extractMemberObjectNames(node.object);
|
|
231
|
+
return objectNames.length ? [...objectNames, node.property.value] : [];
|
|
232
|
+
}
|
|
233
|
+
return [];
|
|
234
|
+
}
|
|
235
|
+
const objectNames = extractMemberObjectNames(node.object);
|
|
236
|
+
if (!t.isIdentifier(node.property))
|
|
237
|
+
return [];
|
|
238
|
+
return objectNames.length ? [...objectNames, node.property.name] : [];
|
|
239
|
+
}
|
|
240
|
+
function extractMemberObjectNames(object) {
|
|
241
|
+
if (t.isIdentifier(object))
|
|
242
|
+
return [object.name];
|
|
243
|
+
if (t.isThisExpression(object))
|
|
244
|
+
return ["this"];
|
|
245
|
+
if (t.isMemberExpression(object) || t.isOptionalMemberExpression(object))
|
|
246
|
+
return extractMemberChain(object);
|
|
247
|
+
return [];
|
|
248
|
+
}
|
|
249
|
+
function matchesAnyCallee(calleeNames, matcher) {
|
|
250
|
+
for (const calleeName of calleeNames) {
|
|
251
|
+
if (matchesCallee(calleeName, matcher))
|
|
252
|
+
return true;
|
|
253
|
+
}
|
|
254
|
+
return false;
|
|
255
|
+
}
|
|
256
|
+
function matchesCallee(calleeName, matcher) {
|
|
257
|
+
if (matcher.callee) {
|
|
258
|
+
const match = matcher.callee;
|
|
259
|
+
if (Array.isArray(match) && match.includes(calleeName))
|
|
260
|
+
return true;
|
|
261
|
+
if (match === calleeName)
|
|
262
|
+
return true;
|
|
263
|
+
}
|
|
264
|
+
if (matcher.calleePrefix) {
|
|
265
|
+
const match = matcher.calleePrefix;
|
|
266
|
+
if (Array.isArray(match) && match.some((prefix) => calleeName.startsWith(prefix)))
|
|
267
|
+
return true;
|
|
268
|
+
if (typeof match === "string" && calleeName.startsWith(match))
|
|
269
|
+
return true;
|
|
270
|
+
}
|
|
271
|
+
if (matcher.calleePattern) {
|
|
272
|
+
const match = matcher.calleePattern;
|
|
273
|
+
if (Array.isArray(match) && match.some((pattern) => matchGlob(calleeName, pattern)))
|
|
274
|
+
return true;
|
|
275
|
+
if (typeof match === "string" && matchGlob(calleeName, match))
|
|
276
|
+
return true;
|
|
277
|
+
}
|
|
278
|
+
return false;
|
|
279
|
+
}
|
|
280
|
+
function matchGlob(value, pattern) {
|
|
281
|
+
if (pattern === value)
|
|
282
|
+
return true;
|
|
283
|
+
try {
|
|
284
|
+
const picomatch = require("picomatch");
|
|
285
|
+
const isMatch = picomatch(pattern, { nocase: false, dot: true });
|
|
286
|
+
return isMatch(value);
|
|
287
|
+
}
|
|
288
|
+
catch {
|
|
289
|
+
return false;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
function normalizeTraverse(value) {
|
|
293
|
+
return value.default ?? value;
|
|
294
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
const PATTERNS = [
|
|
2
|
+
{
|
|
3
|
+
id: "exec-os-system",
|
|
4
|
+
title: "Command Execution",
|
|
5
|
+
description: "Potential command execution via system call.",
|
|
6
|
+
severity: "critical",
|
|
7
|
+
owasp: "A03:2021 Injection",
|
|
8
|
+
pattern: /\b(os\.system|subprocess\.(call|run|Popen)|Runtime\.getRuntime\(\)\.exec|ProcessBuilder|exec\.Command|system\s*\(|popen\s*\(|Process\.Start|ShellExecute)\b/gi
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
id: "eval-exec",
|
|
12
|
+
title: "Dynamic Code Execution",
|
|
13
|
+
description: "Potential dynamic code execution detected.",
|
|
14
|
+
severity: "high",
|
|
15
|
+
owasp: "A03:2021 Injection",
|
|
16
|
+
pattern: /\b(eval|exec|Function|vm\.runInThisContext|loadstring)\s*\(/gi
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
id: "unsafe-deserialization",
|
|
20
|
+
title: "Unsafe Deserialization",
|
|
21
|
+
description: "Potential unsafe deserialization API usage.",
|
|
22
|
+
severity: "high",
|
|
23
|
+
owasp: "A08:2021 Software and Data Integrity Failures",
|
|
24
|
+
pattern: /\b(pickle\.loads?|yaml\.load|YAML\.load|ObjectInputStream|BinaryFormatter|XmlSerializer|Marshal\.load|PHP\s*unserialize|unserialize\s*\()\b/gi
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
id: "weak-crypto",
|
|
28
|
+
title: "Weak Crypto",
|
|
29
|
+
description: "Use of weak or deprecated crypto primitives.",
|
|
30
|
+
severity: "medium",
|
|
31
|
+
owasp: "A02:2021 Cryptographic Failures",
|
|
32
|
+
pattern: /\b(MD5|SHA1|RC4|DES|3DES|ECB)\b/gi
|
|
33
|
+
}
|
|
34
|
+
];
|
|
35
|
+
export function runUniversalPatterns(code, filePath) {
|
|
36
|
+
const findings = [];
|
|
37
|
+
const lines = code.split(/\r?\n/);
|
|
38
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
39
|
+
const line = lines[i];
|
|
40
|
+
for (const rule of PATTERNS) {
|
|
41
|
+
if (rule.pattern.test(line)) {
|
|
42
|
+
findings.push({
|
|
43
|
+
id: rule.id,
|
|
44
|
+
severity: rule.severity,
|
|
45
|
+
owasp: rule.owasp,
|
|
46
|
+
title: rule.title,
|
|
47
|
+
description: rule.description,
|
|
48
|
+
file: filePath,
|
|
49
|
+
line: i + 1
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
rule.pattern.lastIndex = 0;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return findings;
|
|
56
|
+
}
|