opensecurity 0.2.0 → 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 +30 -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,312 @@
|
|
|
1
|
+
function getNodeText(node, source) {
|
|
2
|
+
return source.slice(node.startIndex, node.endIndex);
|
|
3
|
+
}
|
|
4
|
+
function normalizeTraverseNode(node) {
|
|
5
|
+
return node;
|
|
6
|
+
}
|
|
7
|
+
function matchesCallee(name, matcher) {
|
|
8
|
+
if (matcher.callee) {
|
|
9
|
+
const match = matcher.callee;
|
|
10
|
+
if (Array.isArray(match) && match.includes(name))
|
|
11
|
+
return true;
|
|
12
|
+
if (match === name)
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
if (matcher.calleePrefix) {
|
|
16
|
+
const match = matcher.calleePrefix;
|
|
17
|
+
if (Array.isArray(match) && match.some((prefix) => name.startsWith(prefix)))
|
|
18
|
+
return true;
|
|
19
|
+
if (typeof match === "string" && name.startsWith(match))
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
if (matcher.calleePattern) {
|
|
23
|
+
const match = matcher.calleePattern;
|
|
24
|
+
const patterns = Array.isArray(match) ? match : [match];
|
|
25
|
+
return patterns.some((pattern) => matchGlob(name, pattern));
|
|
26
|
+
}
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
function matchGlob(value, pattern) {
|
|
30
|
+
if (pattern === value)
|
|
31
|
+
return true;
|
|
32
|
+
try {
|
|
33
|
+
const picomatch = require("picomatch");
|
|
34
|
+
const isMatch = picomatch(pattern, { nocase: false, dot: true });
|
|
35
|
+
return isMatch(value);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function getNodeNames(node, lang, source) {
|
|
42
|
+
if (lang.identifierNodes.includes(node.type)) {
|
|
43
|
+
return [getNodeText(node, source)];
|
|
44
|
+
}
|
|
45
|
+
if (lang.memberNodes.includes(node.type)) {
|
|
46
|
+
const objectNode = pickField(node, lang.memberObjectFields);
|
|
47
|
+
const propNode = pickField(node, lang.memberPropertyFields);
|
|
48
|
+
const objectNames = objectNode ? getNodeNames(objectNode, lang, source) : [];
|
|
49
|
+
const propName = propNode ? (getNodeNames(propNode, lang, source)[0] ?? getNodeText(propNode, source)) : null;
|
|
50
|
+
if (!propName)
|
|
51
|
+
return [];
|
|
52
|
+
const fullNames = objectNames.length ? objectNames.map((name) => `${name}.${propName}`) : [propName];
|
|
53
|
+
const variants = new Set();
|
|
54
|
+
for (const fullName of fullNames) {
|
|
55
|
+
const parts = fullName.split(".");
|
|
56
|
+
for (let i = 0; i < parts.length; i += 1) {
|
|
57
|
+
variants.add(parts.slice(i).join("."));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return Array.from(variants);
|
|
61
|
+
}
|
|
62
|
+
return [];
|
|
63
|
+
}
|
|
64
|
+
function pickField(node, fields) {
|
|
65
|
+
for (const field of fields) {
|
|
66
|
+
const child = node.childForFieldName?.(field);
|
|
67
|
+
if (child)
|
|
68
|
+
return normalizeTraverseNode(child);
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
function getCallNames(node, lang, source) {
|
|
73
|
+
const callee = pickField(node, lang.callCalleeFields) ??
|
|
74
|
+
(node.namedChildren?.[0] ? normalizeTraverseNode(node.namedChildren[0]) : null);
|
|
75
|
+
if (!callee)
|
|
76
|
+
return [];
|
|
77
|
+
const names = getNodeNames(callee, lang, source);
|
|
78
|
+
if (names.length)
|
|
79
|
+
return names;
|
|
80
|
+
return [getNodeText(callee, source)];
|
|
81
|
+
}
|
|
82
|
+
function getCallArguments(node, lang) {
|
|
83
|
+
const argNode = pickField(node, lang.callArgumentFields);
|
|
84
|
+
if (argNode?.namedChildren?.length) {
|
|
85
|
+
return argNode.namedChildren.map(normalizeTraverseNode);
|
|
86
|
+
}
|
|
87
|
+
return [];
|
|
88
|
+
}
|
|
89
|
+
function getAssignmentSides(node, lang) {
|
|
90
|
+
const left = pickField(node, lang.assignmentLeftFields);
|
|
91
|
+
const right = pickField(node, lang.assignmentRightFields);
|
|
92
|
+
return { left: left ? normalizeTraverseNode(left) : null, right: right ? normalizeTraverseNode(right) : null };
|
|
93
|
+
}
|
|
94
|
+
function isStringNode(node, lang) {
|
|
95
|
+
return lang.stringNodes.includes(node.type);
|
|
96
|
+
}
|
|
97
|
+
function walk(node, fn) {
|
|
98
|
+
fn(node);
|
|
99
|
+
const children = node.namedChildren ?? [];
|
|
100
|
+
for (const child of children) {
|
|
101
|
+
walk(normalizeTraverseNode(child), fn);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
function walkWithScopes(node, isScopeNode, onEnterScope, onExitScope, fn) {
|
|
105
|
+
const scoped = isScopeNode(node);
|
|
106
|
+
if (scoped)
|
|
107
|
+
onEnterScope();
|
|
108
|
+
fn(node);
|
|
109
|
+
const children = node.namedChildren ?? [];
|
|
110
|
+
for (const child of children) {
|
|
111
|
+
walkWithScopes(normalizeTraverseNode(child), isScopeNode, onEnterScope, onExitScope, fn);
|
|
112
|
+
}
|
|
113
|
+
if (scoped)
|
|
114
|
+
onExitScope();
|
|
115
|
+
}
|
|
116
|
+
function findIdentifiers(node, lang, source) {
|
|
117
|
+
const names = [];
|
|
118
|
+
walk(node, (n) => {
|
|
119
|
+
const name = getNodeNames(n, lang, source)[0];
|
|
120
|
+
if (name && lang.identifierNodes.includes(n.type))
|
|
121
|
+
names.push(name);
|
|
122
|
+
});
|
|
123
|
+
return names;
|
|
124
|
+
}
|
|
125
|
+
function matchesAnyCallee(names, matcher) {
|
|
126
|
+
for (const name of names) {
|
|
127
|
+
if (matchesCallee(name, matcher))
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
function indexToLineColumn(source, index) {
|
|
133
|
+
const before = source.slice(0, Math.max(0, index));
|
|
134
|
+
const lines = before.split("\n");
|
|
135
|
+
const line = lines.length;
|
|
136
|
+
const column = lines[lines.length - 1]?.length ?? 0;
|
|
137
|
+
return { line, column: column + 1 };
|
|
138
|
+
}
|
|
139
|
+
export function runNativeTaint(tree, source, lang, ruleSet, filePath) {
|
|
140
|
+
const root = normalizeTraverseNode(tree.rootNode ?? tree);
|
|
141
|
+
const findings = [];
|
|
142
|
+
const taintedVarsStack = [new Set()];
|
|
143
|
+
const taintedExpressions = new WeakSet();
|
|
144
|
+
const sanitizedVarsStack = [new Set()];
|
|
145
|
+
const sanitizedExpressions = new WeakSet();
|
|
146
|
+
const pushScope = () => taintedVarsStack.push(new Set());
|
|
147
|
+
const popScope = () => taintedVarsStack.pop();
|
|
148
|
+
const currentScope = () => taintedVarsStack[taintedVarsStack.length - 1];
|
|
149
|
+
const taint = (name) => currentScope()?.add(name);
|
|
150
|
+
const untaint = (name) => currentScope()?.delete(name);
|
|
151
|
+
const isTainted = (name) => currentScope()?.has(name) ?? false;
|
|
152
|
+
const pushSanitizedScope = () => sanitizedVarsStack.push(new Set());
|
|
153
|
+
const popSanitizedScope = () => sanitizedVarsStack.pop();
|
|
154
|
+
const currentSanitizedScope = () => sanitizedVarsStack[sanitizedVarsStack.length - 1];
|
|
155
|
+
const sanitize = (name) => currentSanitizedScope()?.add(name);
|
|
156
|
+
const unsanitize = (name) => currentSanitizedScope()?.delete(name);
|
|
157
|
+
const isSanitized = (name) => currentSanitizedScope()?.has(name) ?? false;
|
|
158
|
+
const isScopeNode = (node) => (lang.functionNodes ?? []).includes(node.type);
|
|
159
|
+
const valueIsSanitized = (node, rule) => {
|
|
160
|
+
if (sanitizedExpressions.has(node))
|
|
161
|
+
return true;
|
|
162
|
+
const nodeNames = getNodeNames(node, lang, source);
|
|
163
|
+
const primaryName = nodeNames[0];
|
|
164
|
+
if (lang.identifierNodes.includes(node.type) && primaryName) {
|
|
165
|
+
return isSanitized(primaryName);
|
|
166
|
+
}
|
|
167
|
+
if (lang.memberNodes.includes(node.type) && primaryName) {
|
|
168
|
+
return isSanitized(primaryName);
|
|
169
|
+
}
|
|
170
|
+
if (lang.callNodes.includes(node.type)) {
|
|
171
|
+
const calleeNames = getCallNames(node, lang, source);
|
|
172
|
+
if (calleeNames.length && rule.sanitizers?.some((san) => matchesAnyCallee(calleeNames, san.matcher))) {
|
|
173
|
+
sanitizedExpressions.add(node);
|
|
174
|
+
return true;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
const children = node.namedChildren ?? [];
|
|
178
|
+
return children.some((child) => valueIsSanitized(normalizeTraverseNode(child), rule));
|
|
179
|
+
};
|
|
180
|
+
const valueIsTainted = (node, rule) => {
|
|
181
|
+
if (valueIsSanitized(node, rule))
|
|
182
|
+
return false;
|
|
183
|
+
if (taintedExpressions.has(node))
|
|
184
|
+
return true;
|
|
185
|
+
const nodeNames = getNodeNames(node, lang, source);
|
|
186
|
+
if (nodeNames.length && rule.sources?.some((src) => matchesAnyCallee(nodeNames, src.matcher))) {
|
|
187
|
+
return true;
|
|
188
|
+
}
|
|
189
|
+
const primaryName = nodeNames[0];
|
|
190
|
+
if (lang.identifierNodes.includes(node.type) && primaryName) {
|
|
191
|
+
return isTainted(primaryName);
|
|
192
|
+
}
|
|
193
|
+
if (lang.memberNodes.includes(node.type) && primaryName) {
|
|
194
|
+
return isTainted(primaryName);
|
|
195
|
+
}
|
|
196
|
+
if (lang.callNodes.includes(node.type)) {
|
|
197
|
+
const calleeNames = getCallNames(node, lang, source);
|
|
198
|
+
if (calleeNames.length) {
|
|
199
|
+
if (rule.sanitizers?.some((san) => matchesAnyCallee(calleeNames, san.matcher))) {
|
|
200
|
+
sanitizedExpressions.add(node);
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
if (rule.sources?.some((src) => matchesAnyCallee(calleeNames, src.matcher)))
|
|
204
|
+
return true;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
const children = node.namedChildren ?? [];
|
|
208
|
+
return children.some((child) => valueIsTainted(normalizeTraverseNode(child), rule));
|
|
209
|
+
};
|
|
210
|
+
const handleAssignment = (node, rule) => {
|
|
211
|
+
const { left, right } = getAssignmentSides(node, lang);
|
|
212
|
+
if (!left || !right)
|
|
213
|
+
return;
|
|
214
|
+
const targetNames = findIdentifiers(left, lang, source);
|
|
215
|
+
const sanitized = valueIsSanitized(right, rule);
|
|
216
|
+
const tainted = valueIsTainted(right, rule);
|
|
217
|
+
for (const name of targetNames) {
|
|
218
|
+
if (sanitized) {
|
|
219
|
+
untaint(name);
|
|
220
|
+
sanitize(name);
|
|
221
|
+
}
|
|
222
|
+
else if (tainted) {
|
|
223
|
+
taint(name);
|
|
224
|
+
taintedExpressions.add(right);
|
|
225
|
+
unsanitize(name);
|
|
226
|
+
}
|
|
227
|
+
else {
|
|
228
|
+
untaint(name);
|
|
229
|
+
unsanitize(name);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
const reportFinding = (rule, node, message) => {
|
|
234
|
+
const loc = node.startPosition;
|
|
235
|
+
const fallback = loc ? null : indexToLineColumn(source, node.startIndex);
|
|
236
|
+
findings.push({
|
|
237
|
+
ruleId: rule.id,
|
|
238
|
+
ruleTitle: rule.title,
|
|
239
|
+
severity: rule.severity,
|
|
240
|
+
owasp: rule.owasp,
|
|
241
|
+
file: filePath,
|
|
242
|
+
line: loc ? loc.row + 1 : fallback?.line,
|
|
243
|
+
column: loc ? loc.column + 1 : fallback?.column,
|
|
244
|
+
message
|
|
245
|
+
});
|
|
246
|
+
};
|
|
247
|
+
const runRule = (rule) => {
|
|
248
|
+
if (rule.kind === "secret") {
|
|
249
|
+
const pattern = rule.literalPattern ? new RegExp(rule.literalPattern, "i") : null;
|
|
250
|
+
if (!pattern)
|
|
251
|
+
return;
|
|
252
|
+
walk(root, (node) => {
|
|
253
|
+
if (!isStringNode(node, lang))
|
|
254
|
+
return;
|
|
255
|
+
const text = getNodeText(node, source);
|
|
256
|
+
if (pattern.test(text)) {
|
|
257
|
+
reportFinding(rule, node, rule.title);
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
if (rule.kind === "direct") {
|
|
263
|
+
const matcher = {
|
|
264
|
+
callee: rule.callee,
|
|
265
|
+
calleePrefix: rule.calleePrefix,
|
|
266
|
+
calleePattern: rule.calleePattern
|
|
267
|
+
};
|
|
268
|
+
walk(root, (node) => {
|
|
269
|
+
if (!lang.callNodes.includes(node.type))
|
|
270
|
+
return;
|
|
271
|
+
const names = getCallNames(node, lang, source);
|
|
272
|
+
if (names.length && matchesAnyCallee(names, matcher)) {
|
|
273
|
+
reportFinding(rule, node, rule.title);
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
walkWithScopes(root, isScopeNode, () => {
|
|
279
|
+
pushScope();
|
|
280
|
+
pushSanitizedScope();
|
|
281
|
+
}, () => {
|
|
282
|
+
popScope();
|
|
283
|
+
popSanitizedScope();
|
|
284
|
+
}, (node) => {
|
|
285
|
+
if (lang.assignmentNodes.includes(node.type)) {
|
|
286
|
+
handleAssignment(node, rule);
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
if (lang.callNodes.includes(node.type)) {
|
|
290
|
+
const calleeNames = getCallNames(node, lang, source);
|
|
291
|
+
if (!calleeNames.length)
|
|
292
|
+
return;
|
|
293
|
+
const sink = rule.sinks?.find((candidate) => matchesAnyCallee(calleeNames, candidate.matcher));
|
|
294
|
+
if (!sink)
|
|
295
|
+
return;
|
|
296
|
+
const args = getCallArguments(node, lang);
|
|
297
|
+
const hasTainted = args.some((arg) => valueIsTainted(arg, rule));
|
|
298
|
+
if (hasTainted) {
|
|
299
|
+
reportFinding(rule, node, `Tainted data reaches sink ${sink.name}`);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
};
|
|
304
|
+
for (const rule of ruleSet.rules) {
|
|
305
|
+
taintedVarsStack.length = 0;
|
|
306
|
+
taintedVarsStack.push(new Set());
|
|
307
|
+
sanitizedVarsStack.length = 0;
|
|
308
|
+
sanitizedVarsStack.push(new Set());
|
|
309
|
+
runRule(rule);
|
|
310
|
+
}
|
|
311
|
+
return findings;
|
|
312
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
export const DEFAULT_RULES = [
|
|
2
|
+
{
|
|
3
|
+
id: "js-eval-injection",
|
|
4
|
+
name: "Eval Injection",
|
|
5
|
+
description: "Untrusted data reaches eval or Function",
|
|
6
|
+
severity: "high",
|
|
7
|
+
owasp: "A03:2021 Injection",
|
|
8
|
+
sources: [
|
|
9
|
+
{ id: "src-getUserInput", name: "getUserInput", matcher: { callee: "getUserInput" } },
|
|
10
|
+
{ id: "src-req-param", name: "req.param", matcher: { callee: "req.param" } },
|
|
11
|
+
{ id: "src-prompt", name: "prompt", matcher: { callee: "prompt" } },
|
|
12
|
+
{ id: "src-readline", name: "readline.question", matcher: { callee: "readline.question" } }
|
|
13
|
+
],
|
|
14
|
+
sinks: [
|
|
15
|
+
{ id: "sink-eval", name: "eval", matcher: { callee: "eval" } },
|
|
16
|
+
{ id: "sink-function", name: "Function", matcher: { callee: "Function" } }
|
|
17
|
+
],
|
|
18
|
+
sanitizers: [
|
|
19
|
+
{ id: "san-sanitize", name: "sanitize", matcher: { callee: "sanitize" } },
|
|
20
|
+
{ id: "san-escape", name: "escape", matcher: { callee: "escape" } },
|
|
21
|
+
{ id: "san-validator-escape", name: "validator.escape", matcher: { callee: "validator.escape" } }
|
|
22
|
+
]
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
id: "js-command-injection",
|
|
26
|
+
name: "Command Injection",
|
|
27
|
+
description: "Untrusted data reaches child_process execution",
|
|
28
|
+
severity: "critical",
|
|
29
|
+
owasp: "A03:2021 Injection",
|
|
30
|
+
sources: [
|
|
31
|
+
{ id: "src-getUserInput", name: "getUserInput", matcher: { callee: "getUserInput" } },
|
|
32
|
+
{ id: "src-req-param", name: "req.param", matcher: { callee: "req.param" } },
|
|
33
|
+
{ id: "src-prompt", name: "prompt", matcher: { callee: "prompt" } }
|
|
34
|
+
],
|
|
35
|
+
sinks: [
|
|
36
|
+
{ id: "sink-exec", name: "child_process.exec", matcher: { callee: "child_process.exec" } },
|
|
37
|
+
{ id: "sink-execsync", name: "child_process.execSync", matcher: { callee: "child_process.execSync" } },
|
|
38
|
+
{ id: "sink-spawn", name: "child_process.spawn", matcher: { callee: "child_process.spawn" } },
|
|
39
|
+
{ id: "sink-spawnsync", name: "child_process.spawnSync", matcher: { callee: "child_process.spawnSync" } },
|
|
40
|
+
{ id: "sink-execfile", name: "child_process.execFile", matcher: { callee: "child_process.execFile" } },
|
|
41
|
+
{ id: "sink-execfilesync", name: "child_process.execFileSync", matcher: { callee: "child_process.execFileSync" } }
|
|
42
|
+
],
|
|
43
|
+
sanitizers: [
|
|
44
|
+
{ id: "san-shellescape", name: "shellescape", matcher: { callee: "shellescape" } },
|
|
45
|
+
{ id: "san-escapeshellarg", name: "escapeShellArg", matcher: { callee: "escapeShellArg" } }
|
|
46
|
+
]
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
id: "js-ssrf",
|
|
50
|
+
name: "Server-Side Request Forgery",
|
|
51
|
+
description: "Untrusted data reaches network request",
|
|
52
|
+
severity: "high",
|
|
53
|
+
owasp: "A10:2021 Server-Side Request Forgery",
|
|
54
|
+
sources: [
|
|
55
|
+
{ id: "src-getUserInput", name: "getUserInput", matcher: { callee: "getUserInput" } },
|
|
56
|
+
{ id: "src-req-param", name: "req.param", matcher: { callee: "req.param" } },
|
|
57
|
+
{ id: "src-prompt", name: "prompt", matcher: { callee: "prompt" } }
|
|
58
|
+
],
|
|
59
|
+
sinks: [
|
|
60
|
+
{ id: "sink-fetch", name: "fetch", matcher: { callee: "fetch" } },
|
|
61
|
+
{ id: "sink-axios", name: "axios", matcher: { callee: "axios" } },
|
|
62
|
+
{ id: "sink-axios-get", name: "axios.get", matcher: { callee: "axios.get" } },
|
|
63
|
+
{ id: "sink-axios-post", name: "axios.post", matcher: { callee: "axios.post" } },
|
|
64
|
+
{ id: "sink-axios-request", name: "axios.request", matcher: { callee: "axios.request" } },
|
|
65
|
+
{ id: "sink-got", name: "got", matcher: { callee: "got" } },
|
|
66
|
+
{ id: "sink-got-get", name: "got.get", matcher: { callee: "got.get" } },
|
|
67
|
+
{ id: "sink-got-post", name: "got.post", matcher: { callee: "got.post" } },
|
|
68
|
+
{ id: "sink-undici-request", name: "undici.request", matcher: { callee: "undici.request" } },
|
|
69
|
+
{ id: "sink-undici-fetch", name: "undici.fetch", matcher: { callee: "undici.fetch" } },
|
|
70
|
+
{ id: "sink-http-get", name: "http.get", matcher: { callee: "http.get" } },
|
|
71
|
+
{ id: "sink-https-get", name: "https.get", matcher: { callee: "https.get" } },
|
|
72
|
+
{ id: "sink-request", name: "request", matcher: { callee: "request" } },
|
|
73
|
+
{ id: "sink-request-get", name: "request.get", matcher: { callee: "request.get" } },
|
|
74
|
+
{ id: "sink-request-post", name: "request.post", matcher: { callee: "request.post" } },
|
|
75
|
+
{ id: "sink-superagent-get", name: "superagent.get", matcher: { callee: "superagent.get" } },
|
|
76
|
+
{ id: "sink-superagent-post", name: "superagent.post", matcher: { callee: "superagent.post" } }
|
|
77
|
+
],
|
|
78
|
+
sanitizers: [
|
|
79
|
+
{ id: "san-sanitizeurl", name: "sanitizeUrl", matcher: { callee: "sanitizeUrl" } },
|
|
80
|
+
{ id: "san-validateurl", name: "validateUrl", matcher: { callee: "validateUrl" } },
|
|
81
|
+
{ id: "san-encodeuri", name: "encodeURI", matcher: { callee: "encodeURI" } },
|
|
82
|
+
{ id: "san-encodeuricomp", name: "encodeURIComponent", matcher: { callee: "encodeURIComponent" } }
|
|
83
|
+
]
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
id: "js-path-traversal",
|
|
87
|
+
name: "Path Traversal",
|
|
88
|
+
description: "Untrusted data reaches filesystem APIs",
|
|
89
|
+
severity: "high",
|
|
90
|
+
owasp: "A01:2021 Broken Access Control",
|
|
91
|
+
sources: [
|
|
92
|
+
{ id: "src-getUserInput", name: "getUserInput", matcher: { callee: "getUserInput" } },
|
|
93
|
+
{ id: "src-req-param", name: "req.param", matcher: { callee: "req.param" } },
|
|
94
|
+
{ id: "src-prompt", name: "prompt", matcher: { callee: "prompt" } },
|
|
95
|
+
{ id: "src-readline", name: "readline.question", matcher: { callee: "readline.question" } }
|
|
96
|
+
],
|
|
97
|
+
sinks: [
|
|
98
|
+
{ id: "sink-readfile", name: "fs.readFile", matcher: { callee: "fs.readFile" } },
|
|
99
|
+
{ id: "sink-readfilesync", name: "fs.readFileSync", matcher: { callee: "fs.readFileSync" } },
|
|
100
|
+
{ id: "sink-writefile", name: "fs.writeFile", matcher: { callee: "fs.writeFile" } },
|
|
101
|
+
{ id: "sink-writefilesync", name: "fs.writeFileSync", matcher: { callee: "fs.writeFileSync" } },
|
|
102
|
+
{ id: "sink-appendfile", name: "fs.appendFile", matcher: { callee: "fs.appendFile" } },
|
|
103
|
+
{ id: "sink-appendfilesync", name: "fs.appendFileSync", matcher: { callee: "fs.appendFileSync" } },
|
|
104
|
+
{ id: "sink-createreadstream", name: "fs.createReadStream", matcher: { callee: "fs.createReadStream" } },
|
|
105
|
+
{ id: "sink-createwritestream", name: "fs.createWriteStream", matcher: { callee: "fs.createWriteStream" } },
|
|
106
|
+
{ id: "sink-readdir", name: "fs.readdir", matcher: { callee: "fs.readdir" } },
|
|
107
|
+
{ id: "sink-readdirsync", name: "fs.readdirSync", matcher: { callee: "fs.readdirSync" } },
|
|
108
|
+
{ id: "sink-stat", name: "fs.stat", matcher: { callee: "fs.stat" } },
|
|
109
|
+
{ id: "sink-statsync", name: "fs.statSync", matcher: { callee: "fs.statSync" } },
|
|
110
|
+
{ id: "sink-lstat", name: "fs.lstat", matcher: { callee: "fs.lstat" } },
|
|
111
|
+
{ id: "sink-lstatsync", name: "fs.lstatSync", matcher: { callee: "fs.lstatSync" } },
|
|
112
|
+
{ id: "sink-rm", name: "fs.rm", matcher: { callee: "fs.rm" } },
|
|
113
|
+
{ id: "sink-rmsync", name: "fs.rmSync", matcher: { callee: "fs.rmSync" } },
|
|
114
|
+
{ id: "sink-unlink", name: "fs.unlink", matcher: { callee: "fs.unlink" } },
|
|
115
|
+
{ id: "sink-unlinksync", name: "fs.unlinkSync", matcher: { callee: "fs.unlinkSync" } },
|
|
116
|
+
{ id: "sink-rmdir", name: "fs.rmdir", matcher: { callee: "fs.rmdir" } },
|
|
117
|
+
{ id: "sink-rmdirsync", name: "fs.rmdirSync", matcher: { callee: "fs.rmdirSync" } }
|
|
118
|
+
],
|
|
119
|
+
sanitizers: [
|
|
120
|
+
{ id: "san-path-normalize", name: "path.normalize", matcher: { callee: "path.normalize" } },
|
|
121
|
+
{ id: "san-path-resolve", name: "path.resolve", matcher: { callee: "path.resolve" } },
|
|
122
|
+
{ id: "san-path-join", name: "path.join", matcher: { callee: "path.join" } }
|
|
123
|
+
]
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
id: "js-sqli",
|
|
127
|
+
name: "SQL Injection",
|
|
128
|
+
description: "Untrusted data reaches database query execution",
|
|
129
|
+
severity: "critical",
|
|
130
|
+
owasp: "A03:2021 Injection",
|
|
131
|
+
sources: [
|
|
132
|
+
{ id: "src-getUserInput", name: "getUserInput", matcher: { callee: "getUserInput" } },
|
|
133
|
+
{ id: "src-req-param", name: "req.param", matcher: { callee: "req.param" } },
|
|
134
|
+
{ id: "src-prompt", name: "prompt", matcher: { callee: "prompt" } }
|
|
135
|
+
],
|
|
136
|
+
sinks: [
|
|
137
|
+
{ id: "sink-mysql-query", name: "mysql.query", matcher: { callee: "mysql.query" } },
|
|
138
|
+
{ id: "sink-mysql2-query", name: "mysql2.query", matcher: { callee: "mysql2.query" } },
|
|
139
|
+
{ id: "sink-pg-query", name: "pg.query", matcher: { callee: "pg.query" } },
|
|
140
|
+
{ id: "sink-client-query", name: "client.query", matcher: { callee: "client.query" } },
|
|
141
|
+
{ id: "sink-pool-query", name: "pool.query", matcher: { callee: "pool.query" } },
|
|
142
|
+
{ id: "sink-connection-query", name: "connection.query", matcher: { callee: "connection.query" } },
|
|
143
|
+
{ id: "sink-db-query", name: "db.query", matcher: { callee: "db.query" } },
|
|
144
|
+
{ id: "sink-sequelize-query", name: "sequelize.query", matcher: { callee: "sequelize.query" } },
|
|
145
|
+
{ id: "sink-knex-raw", name: "knex.raw", matcher: { callee: "knex.raw" } }
|
|
146
|
+
],
|
|
147
|
+
sanitizers: [
|
|
148
|
+
{ id: "san-sql-escape", name: "escape", matcher: { callee: "escape" } },
|
|
149
|
+
{ id: "san-sql-escapeid", name: "escapeId", matcher: { callee: "escapeId" } },
|
|
150
|
+
{ id: "san-sql-parameterize", name: "parameterize", matcher: { callee: "parameterize" } }
|
|
151
|
+
]
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
id: "js-xss-template",
|
|
155
|
+
name: "XSS (Server Templates)",
|
|
156
|
+
description: "Untrusted data reaches server response rendering",
|
|
157
|
+
severity: "high",
|
|
158
|
+
owasp: "A03:2021 Injection",
|
|
159
|
+
sources: [
|
|
160
|
+
{ id: "src-getUserInput", name: "getUserInput", matcher: { callee: "getUserInput" } },
|
|
161
|
+
{ id: "src-req-param", name: "req.param", matcher: { callee: "req.param" } },
|
|
162
|
+
{ id: "src-prompt", name: "prompt", matcher: { callee: "prompt" } }
|
|
163
|
+
],
|
|
164
|
+
sinks: [
|
|
165
|
+
{ id: "sink-res-send", name: "res.send", matcher: { callee: "res.send" } },
|
|
166
|
+
{ id: "sink-res-write", name: "res.write", matcher: { callee: "res.write" } },
|
|
167
|
+
{ id: "sink-res-end", name: "res.end", matcher: { callee: "res.end" } },
|
|
168
|
+
{ id: "sink-res-render", name: "res.render", matcher: { callee: "res.render" } },
|
|
169
|
+
{ id: "sink-reply-send", name: "reply.send", matcher: { callee: "reply.send" } }
|
|
170
|
+
],
|
|
171
|
+
sanitizers: [
|
|
172
|
+
{ id: "san-escape", name: "escape", matcher: { callee: "escape" } },
|
|
173
|
+
{ id: "san-encodeuri", name: "encodeURI", matcher: { callee: "encodeURI" } },
|
|
174
|
+
{ id: "san-encodeuricomp", name: "encodeURIComponent", matcher: { callee: "encodeURIComponent" } }
|
|
175
|
+
]
|
|
176
|
+
}
|
|
177
|
+
];
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { DEFAULT_RULES } from "./defaultRules.js";
|
|
4
|
+
export async function loadRules(rulesPath, cwd) {
|
|
5
|
+
if (!rulesPath)
|
|
6
|
+
return DEFAULT_RULES;
|
|
7
|
+
const resolved = path.isAbsolute(rulesPath) ? rulesPath : path.join(cwd, rulesPath);
|
|
8
|
+
const raw = await fs.readFile(resolved, "utf8");
|
|
9
|
+
const parsed = JSON.parse(raw);
|
|
10
|
+
if (!Array.isArray(parsed)) {
|
|
11
|
+
throw new Error("Rules file must be a JSON array");
|
|
12
|
+
}
|
|
13
|
+
return parsed;
|
|
14
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import picomatch from "picomatch";
|
|
4
|
+
export async function walkFiles(rootDir, options) {
|
|
5
|
+
const includeMatchers = options.include.map((p) => picomatch(p, { dot: true }));
|
|
6
|
+
const excludeMatchers = options.exclude.map((p) => picomatch(p, { dot: true }));
|
|
7
|
+
const results = [];
|
|
8
|
+
async function visit(currentDir) {
|
|
9
|
+
const entries = await fs.readdir(currentDir, { withFileTypes: true });
|
|
10
|
+
for (const entry of entries) {
|
|
11
|
+
const fullPath = path.join(currentDir, entry.name);
|
|
12
|
+
const relPath = path.relative(rootDir, fullPath).split(path.sep).join("/");
|
|
13
|
+
if (excludeMatchers.some((m) => m(relPath))) {
|
|
14
|
+
continue;
|
|
15
|
+
}
|
|
16
|
+
if (entry.isDirectory()) {
|
|
17
|
+
await visit(fullPath);
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
if (includeMatchers.length === 0 || includeMatchers.some((m) => m(relPath))) {
|
|
21
|
+
results.push(fullPath);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
await visit(rootDir);
|
|
26
|
+
return results;
|
|
27
|
+
}
|