Skip to content

Commit 08ba476

Browse files
BuZZ-TBastian Gebhardt
andauthored
feat: extend detect non literal fs filename (#92)
* feat: rewritten detect-non-literal-fs-filename rule * detects multiple cases of possible imports of fs methods * many tests added to cover that cases * refactor: extract searching for ImportDeclaration and VariableDeclaration in utils * chore: added JSdoc, inlined fsPackagesNames, renamed sink terminology * chore: changed report params to object * chore: rename 'sink', remove '?.' for node 12 Co-authored-by: Bastian Gebhardt <Bastian [email protected]>
1 parent 2e849e9 commit 08ba476

File tree

4 files changed

+440
-30
lines changed

4 files changed

+440
-30
lines changed

package-lock.json

Lines changed: 0 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

rules/detect-non-literal-fs-filename.js

Lines changed: 277 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,210 @@
55

66
'use strict';
77

8+
const fsMetaData = require('./data/fsFunctionData.json');
9+
const funcNames = Object.keys(fsMetaData);
10+
const fsPackageNames = ['fs', 'node:fs', 'fs/promises', 'node:fs/promises', 'fs-extra'];
11+
12+
const { getImportDeclaration, getVariableDeclaration } = require('./utils/import-utils');
13+
814
//------------------------------------------------------------------------------
9-
// Rule Definition
15+
// Utils
1016
//------------------------------------------------------------------------------
1117

12-
const fsMetaData = require('./data/fsFunctionData.json');
13-
const funcNames = Object.keys(fsMetaData);
18+
function getIndices(node, argMeta) {
19+
return (argMeta || []).filter((argIndex) => node.arguments[argIndex].type !== 'Literal');
20+
}
21+
22+
function generateReport({ context, node, packageName, methodName, indices }) {
23+
if (!indices || indices.length === 0) {
24+
return null;
25+
}
26+
return context.report({ node, message: `Found ${methodName} from package "${packageName}" with non literal argument at index ${indices.join(',')}` });
27+
}
28+
29+
/**
30+
* Detects:
31+
* | var something = require('fs').readFile;
32+
* | something(a);
33+
*/
34+
function detectOnRequireWithProperty({ context, methodName, node, program }) {
35+
const declaration = getVariableDeclaration({
36+
condition: (declaration) => declaration.init.parent.id.name === methodName,
37+
hasObject: true,
38+
methodName,
39+
packageNames: fsPackageNames,
40+
program,
41+
});
42+
43+
if (!declaration) {
44+
return null;
45+
}
46+
47+
// we found the require for our method!
48+
const fsFunction = declaration.init.property.name;
49+
const packageName = declaration.init.object.arguments[0].value;
50+
const fnName = declaration.init.property.name;
51+
52+
const indices = getIndices(node, fsMetaData[fsFunction]);
53+
54+
return generateReport({
55+
context,
56+
node,
57+
packageName,
58+
methodName: fnName,
59+
indices,
60+
});
61+
}
62+
63+
/**
64+
* Detects:
65+
* | var something = require('fs');
66+
* | something.readFile(c);
67+
*/
68+
function detectOnMethodCall({ context, node, program, methodName }) {
69+
const declaration = getVariableDeclaration({
70+
packageNames: fsPackageNames,
71+
hasObject: false,
72+
program,
73+
});
74+
75+
if (!declaration) {
76+
return null;
77+
}
78+
79+
const indices = getIndices(node.parent, fsMetaData[methodName]);
80+
81+
return generateReport({
82+
context,
83+
node,
84+
packageName: declaration.init.arguments[0].value,
85+
methodName,
86+
indices,
87+
});
88+
}
89+
90+
/**
91+
* Detects:
92+
* | var { readFile: something } = require('fs')
93+
* | readFile(filename)
94+
*/
95+
function detectOnDestructuredRequire({ context, methodName, node, program }) {
96+
const declaration = getVariableDeclaration({
97+
condition: (declaration) => declaration && declaration.id && declaration.id.properties && declaration.id.properties.some((p) => p.value.name === methodName),
98+
hasObject: false,
99+
methodName,
100+
packageNames: fsPackageNames,
101+
program,
102+
});
103+
104+
if (!declaration) {
105+
return null;
106+
}
107+
108+
const realMethodName = declaration.id.properties.find((p) => p.value.name === methodName).key.name;
109+
110+
const meta = fsMetaData[realMethodName];
111+
const indices = getIndices(node, meta);
112+
113+
return generateReport({
114+
context,
115+
node,
116+
packageName: declaration.init.arguments[0].value,
117+
methodName: realMethodName,
118+
indices,
119+
});
120+
}
121+
122+
/**
123+
* Detects:
124+
* | import { readFile as something } from 'fs';
125+
* | something(filename);
126+
*/
127+
function detectOnDestructuredImport({ context, methodName, node, program }) {
128+
const importDeclaration = getImportDeclaration({ methodName, packageNames: fsPackageNames, program });
129+
130+
const specifier = importDeclaration && importDeclaration.specifiers && importDeclaration.specifiers.find((s) => !!funcNames.includes(s.imported.name));
131+
132+
if (!specifier) {
133+
return null;
134+
}
135+
136+
const fnName = specifier.imported.name;
137+
const meta = fsMetaData[fnName];
138+
const indices = getIndices(node, meta);
139+
140+
return generateReport({
141+
context,
142+
node,
143+
packageName: specifier.parent.source.value,
144+
methodName: fnName,
145+
indices,
146+
});
147+
}
148+
149+
/**
150+
* Detects:
151+
* | import * as something from 'fs';
152+
* | something.readFile(c);
153+
*/
154+
function detectOnDefaultImport({ context, methodName, node, objectName, program }) {
155+
if (!funcNames.includes(methodName)) {
156+
return null;
157+
}
158+
159+
const importDeclaration = getImportDeclaration({ methodName: objectName, packageNames: fsPackageNames, program });
160+
161+
if (!importDeclaration) {
162+
return null;
163+
}
164+
165+
const meta = fsMetaData[methodName];
166+
const indices = getIndices(node.parent, meta);
167+
168+
return generateReport({
169+
context,
170+
node,
171+
packageName: importDeclaration.source.value,
172+
methodName,
173+
indices,
174+
});
175+
}
176+
177+
/**
178+
* Detects:
179+
* | var something = require('fs').promises;
180+
* | something.readFile(filename)
181+
*/
182+
function detectOnPromiseProperty({ context, methodName, node, objectName, program }) {
183+
const declaration = program.body
184+
.filter((entry) => entry.type === 'VariableDeclaration')
185+
.flatMap((entry) => entry.declarations)
186+
.find(
187+
(declaration) =>
188+
declaration.id.name === objectName &&
189+
declaration.init.type === 'MemberExpression' &&
190+
// package name is fs / fs-extra
191+
fsPackageNames.includes(declaration.init.object.arguments[0].value)
192+
);
193+
194+
if (!declaration) {
195+
return null;
196+
}
197+
const meta = fsMetaData[methodName];
198+
const indices = getIndices(node.parent, meta);
199+
200+
return generateReport({
201+
context,
202+
node,
203+
packageName: declaration.init.object.arguments[0].value,
204+
methodName,
205+
indices,
206+
});
207+
}
208+
209+
//------------------------------------------------------------------------------
210+
// Rule Definition
211+
//------------------------------------------------------------------------------
14212

15213
module.exports = {
16214
meta: {
@@ -24,30 +222,88 @@ module.exports = {
24222
},
25223
create: function (context) {
26224
return {
225+
CallExpression: function (node) {
226+
// readFile/open/... (but might be renamed)
227+
const localMethodName = node.callee.name;
228+
229+
// don't check require. If all arguments are Literals, it's surely safe!
230+
if (!localMethodName || localMethodName === 'require' || node.arguments.every((argument) => argument.type === 'Literal')) {
231+
return;
232+
}
233+
234+
// this only works, when imports are on top level!
235+
const program = context.getAncestors()[0];
236+
237+
const requireReport = detectOnRequireWithProperty({
238+
context,
239+
methodName: localMethodName,
240+
node,
241+
program,
242+
});
243+
if (requireReport) {
244+
return requireReport;
245+
}
246+
247+
const destructuredRequireReport = detectOnDestructuredRequire({
248+
context,
249+
methodName: localMethodName,
250+
node,
251+
program,
252+
});
253+
if (destructuredRequireReport) {
254+
return destructuredRequireReport;
255+
}
256+
257+
const importReport = detectOnDestructuredImport({
258+
context,
259+
methodName: localMethodName,
260+
node,
261+
program,
262+
});
263+
if (importReport) {
264+
return importReport;
265+
}
266+
},
27267
MemberExpression: function (node) {
28-
const result = [];
29-
if (funcNames.indexOf(node.property.name) !== -1) {
30-
const meta = fsMetaData[node.property.name];
31-
const args = node.parent.arguments;
32-
meta.forEach((i) => {
33-
if (args && args.length > i) {
34-
if (args[i].type !== 'Literal') {
35-
result.push(i);
36-
}
37-
}
38-
});
268+
const realMethodName = node.property.name; // readFile/open/... (not renamed)
269+
const localObjectName = node.object.name; // fs/node:fs/... (but might be renamed)
270+
271+
// this only works, when imports are on top level!
272+
const program = context.getAncestors()[0];
273+
274+
const methodCallReport = detectOnMethodCall({
275+
context,
276+
methodName: realMethodName,
277+
node,
278+
program,
279+
});
280+
if (methodCallReport) {
281+
return methodCallReport;
39282
}
40283

41-
if (result.length > 0) {
42-
return context.report({ node: node, message: `Found fs.${node.property.name} with non literal argument at index ${result.join(',')}` });
284+
const defaultImportReport = detectOnDefaultImport({
285+
program,
286+
objectName: localObjectName,
287+
methodName: realMethodName,
288+
context,
289+
node,
290+
});
291+
292+
if (defaultImportReport) {
293+
return defaultImportReport;
43294
}
44295

45-
/*
46-
if (node.parent && node.parent.arguments && node.parent.arguments[index].value) {
47-
return context.report(node, 'found Buffer.' + node.property.name + ' with noAssert flag set true');
296+
const promisePropertyReport = detectOnPromiseProperty({
297+
context,
298+
methodName: realMethodName,
299+
node,
300+
objectName: localObjectName,
301+
program,
302+
});
48303

49-
}
50-
*/
304+
if (promisePropertyReport) {
305+
return promisePropertyReport;
306+
}
51307
},
52308
};
53309
},

rules/utils/import-utils.js

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/**
2+
* Returns the ImportDeclaration for the import of the methodName from one of the packageNames
3+
* import { methodName as a } from 'packageName';
4+
*
5+
* @param {Object} param0
6+
* @param {string} param0.methodName
7+
* @param {string[]} param0.packageNames
8+
* @param {Object} param0.program The AST program object
9+
* @returns The ImportDeclaration for the import of the methodName from one of the packageNames
10+
*/
11+
module.exports.getImportDeclaration = ({ methodName, packageNames, program }) =>
12+
program.body.find((entry) => entry.type === 'ImportDeclaration' && packageNames.includes(entry.source.value) && entry.specifiers.some((s) => s.local.name === methodName));
13+
14+
/**
15+
* Returns the VariableDeclaration for a require based import
16+
*
17+
* @param {Object} param0
18+
* @param {Function} param0.condition Optional function to check additional conditions on the resulting VariableDeclaration
19+
* @param {boolean} param0.hasObject Whether the information is received by declaration.init or declaration.init.object
20+
* @param {string[]} param0.packageNames The interesting packages the method is imported from
21+
* @param {Object} param0.program The AST program object
22+
* @returns
23+
*/
24+
module.exports.getVariableDeclaration = ({ condition, hasObject, packageNames, program }) =>
25+
program.body
26+
// a node import is a variable declaration
27+
.filter((entry) => entry.type === 'VariableDeclaration')
28+
// one var/let/const may contain multiple declarations, separated by comma, after the "=" sign
29+
.flatMap((d) => d.declarations)
30+
.find((d) => {
31+
const init = hasObject ? d.init.object : d.init;
32+
33+
return (
34+
init &&
35+
init.callee &&
36+
init.callee.name === 'require' &&
37+
init.arguments[0].type === 'Literal' &&
38+
packageNames.includes(init.arguments[0].value) &&
39+
(!condition || condition(d))
40+
);
41+
});

0 commit comments

Comments
 (0)