[init]
This commit is contained in:
42
src/ast/analysis.mjs
Normal file
42
src/ast/analysis.mjs
Normal file
@@ -0,0 +1,42 @@
|
||||
import { parseScript} from 'esprima';
|
||||
import fs from 'node:fs';
|
||||
import * as eslintScope from 'eslint-scope';
|
||||
import { prependString, appendString } from '../utils/constants.mjs';
|
||||
|
||||
// const modulesImported
|
||||
/**
|
||||
*
|
||||
* @param {string} filePath
|
||||
* @returns
|
||||
*/
|
||||
export function getASTAndScope(filePath) {
|
||||
const mod = fs.readFileSync(filePath);
|
||||
|
||||
|
||||
const modString = mod.toString();
|
||||
|
||||
const nodeJSCJSModString = prependString + modString + appendString;
|
||||
|
||||
const parseOptions = { ecmaVersion: 7, range: true, sourceType: 'script', comment: true };
|
||||
|
||||
|
||||
const parsedModAST = parseScript(nodeJSCJSModString, parseOptions);
|
||||
|
||||
|
||||
// https://eslint.org/docs/latest/extend/scope-manager-interface#fields-1
|
||||
// handle cjs ourselves
|
||||
const scopeManager = eslintScope.analyze(parsedModAST, { ...parseOptions, nodejsScope: false });
|
||||
return { scopeManager, parsedModAST };
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {import('eslint').Scope.ScopeManager} scopeManager
|
||||
* @returns
|
||||
*/
|
||||
export function getSetOfIdentifierReferencesForRequireUses(scopeManager) {
|
||||
const requireImportsUsed = scopeManager.scopes[1].set.get('require');
|
||||
|
||||
return new Set(requireImportsUsed.references.map(e => e.identifier));
|
||||
}
|
||||
|
11
src/ast/tag.mjs
Normal file
11
src/ast/tag.mjs
Normal file
@@ -0,0 +1,11 @@
|
||||
export function tagASTNode(importVariableReference, _variableName, moduleImportedName) {
|
||||
importVariableReference.identifier.tag = "ref:variable:" + _variableName + "_module_" + moduleImportedName;
|
||||
}
|
||||
|
||||
export function untagASTNode(importVariableReference, _variableName, moduleImportedName) {
|
||||
delete importVariableReference.identifier.tag
|
||||
}
|
||||
|
||||
export function getTagKey(){
|
||||
return 'tag';
|
||||
}
|
104
src/ast/visitors.mjs
Normal file
104
src/ast/visitors.mjs
Normal file
@@ -0,0 +1,104 @@
|
||||
import { Visitor } from "esrecurse"
|
||||
|
||||
export class ExpressionArrayVisitor extends Visitor {
|
||||
/**
|
||||
*
|
||||
* @param {import('eslint').Scope.ScopeManager} scopeManager
|
||||
*/
|
||||
constructor(scopeManager) {
|
||||
super();
|
||||
/**
|
||||
* @type {import('estree').Literal["value"][]}
|
||||
*/
|
||||
this.arguments = [];
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {import('estree').CallExpression} node
|
||||
* @returns
|
||||
*/
|
||||
CallExpression(node) {
|
||||
for (const argumentNode of node.arguments) {
|
||||
this.visit(argumentNode);
|
||||
}
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @param {import('estree').Expression} node
|
||||
*/
|
||||
Expression(node) {
|
||||
return this.visit(node.arguments);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {SpreadExpression} node
|
||||
*/
|
||||
SpreadExpression(node) {
|
||||
throw Error("No Spreads!");
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {import("estree").Identifier} node
|
||||
*/
|
||||
Identifier(node) {
|
||||
// TODO - Grab it from outside
|
||||
console.error("Found identifier ", node);
|
||||
throw Error("constant or nothing");
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @param {import("estree").Literal} node
|
||||
*/
|
||||
Literal(node) {
|
||||
this.arguments.push(node.value);
|
||||
}
|
||||
ObjectExpression(node) {
|
||||
console.warning("Not finished");
|
||||
throw Error("TBD");
|
||||
this.arguments.push(new ObjectSimplifierVisitor().visit(node));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {import("estree").Node} node
|
||||
*/
|
||||
visit(node) {
|
||||
console.log("Visiting", node.type);
|
||||
super.visit(node);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
class ObjectSimplifierVisitor extends Visitor {
|
||||
expr = {};
|
||||
exprStack = [];
|
||||
// /**
|
||||
// *
|
||||
// * @param {import("estree").ObjectExpression} node
|
||||
// */
|
||||
// ObjectExpression(node){
|
||||
// }
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {import("estree").Property} node
|
||||
*/
|
||||
Property(node){
|
||||
this.#topOfStack()?.[ node.key.value];
|
||||
}
|
||||
|
||||
|
||||
visit(node) {
|
||||
console.log("Objvisit",node);
|
||||
return super.visit(node);
|
||||
}
|
||||
|
||||
#topOfStack(){
|
||||
return this.exprStack[this.exprStack.length-1];
|
||||
}
|
||||
}
|
96
src/calls.mjs
Normal file
96
src/calls.mjs
Normal file
@@ -0,0 +1,96 @@
|
||||
import { Syntax } from 'esprima';
|
||||
import esquery from 'esquery';
|
||||
import { getSetOfIdentifierReferencesForRequireUses } from './ast/analysis.mjs';
|
||||
import { LibraryCallsRecorder } from './libcalls.mjs';
|
||||
import { tagASTNode, getTagKey, untagASTNode } from './ast/tag.mjs';
|
||||
import { ExpressionArrayVisitor } from './ast/visitors.mjs';
|
||||
import assert from 'assert';
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {import('eslint').Scope.ScopeManager} scopeManager
|
||||
* @returns
|
||||
*/
|
||||
export function getRequireCallsAndConstantArgs(scopeManager) {
|
||||
const requireReferencesIdentifierSet = getSetOfIdentifierReferencesForRequireUses(scopeManager);
|
||||
const callRecorder = new LibraryCallsRecorder();
|
||||
// all the variables, what part of require module they're using
|
||||
for (const scope of scopeManager.scopes) {
|
||||
for (const [variableName, variable] of scope.set) {
|
||||
// FIXME raise error if more than one but has import
|
||||
const declareDefinesOfVariable = variable.defs.filter(e => e.node.type === Syntax.VariableDeclarator && e.node.init.type === Syntax.CallExpression);
|
||||
// uses this variable
|
||||
const declarationNodesUsingRequireList = declareDefinesOfVariable.filter(e => requireReferencesIdentifierSet.has(e.node.init.callee));
|
||||
if (declarationNodesUsingRequireList.length > 1) {
|
||||
console.error(`Import variable ${variableName} has been defined twice, skipping`);
|
||||
continue;
|
||||
} else if (declarationNodesUsingRequireList.length === 0) {
|
||||
console.log(`Skipping unused import variable ${variableName}`);
|
||||
continue;
|
||||
}
|
||||
const declarationNodeUsingRequire = declarationNodesUsingRequireList[0]; // we know its singleton from above
|
||||
|
||||
|
||||
|
||||
//
|
||||
const moduleImportedName = getModuleNameFromRequireAssignDeclaration(declarationNodeUsingRequire);
|
||||
const importPortion = getModuleImportPortionFromDefinition(declarationNodeUsingRequire, variableName);
|
||||
for (const importVariableReference of variable.references) {
|
||||
tagASTNode(importVariableReference, variableName, moduleImportedName);
|
||||
|
||||
const simpleCallExpressionsOfVariableInBlockList = esquery.query(importVariableReference.from.block, `CallExpression:has(>[tag="${importVariableReference.identifier[getTagKey()]}"])`);
|
||||
for (const simpleCallExpressionOfVariableInBlock of simpleCallExpressionsOfVariableInBlockList) {
|
||||
const argumentsGathered = gatherArgumentsFromTheCallExpression(importVariableReference, simpleCallExpressionOfVariableInBlock, importVariableReference.from.block, scopeManager);
|
||||
callRecorder.pushToMap(moduleImportedName, importPortion, argumentsGathered);
|
||||
|
||||
}
|
||||
untagASTNode(importVariableReference, variableName, moduleImportedName)
|
||||
}
|
||||
}
|
||||
}
|
||||
return callRecorder.calls;
|
||||
}/**
|
||||
*
|
||||
* @param {import('eslint').Scope.Definition} declaratorDefinition
|
||||
*/
|
||||
|
||||
export function getModuleImportPortionFromDefinition(declaratorDefinition, variableName) {
|
||||
const node = declaratorDefinition.node;
|
||||
// console.log(`Req type`, node.type);
|
||||
// FIXME - import portion calculations correctly
|
||||
if (node.id.type === 'ObjectPattern') {
|
||||
// console.log("Obj");
|
||||
// const nodeName = node.id.properties.filter(e=>e.key.value.name==='x')
|
||||
// TODO allow re-naming
|
||||
return "." + variableName;
|
||||
} else {
|
||||
return '.';
|
||||
}
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @param {import('eslint').Scope.Reference} importVariableReference
|
||||
* @param {import('estree').CallExpression} callExpressionNode
|
||||
* @param {ReturnType<esquery>[0]} contextOfUse
|
||||
* @param {import('eslint').Scope.ScopeManager} scopeManager
|
||||
*/
|
||||
|
||||
export function gatherArgumentsFromTheCallExpression(importVariableReference, callExpressionNode, contextOfUse, scopeManager) {
|
||||
const expressionArrayVisitor = new ExpressionArrayVisitor(scopeManager);
|
||||
expressionArrayVisitor.visit(callExpressionNode);
|
||||
const { arguments: constantArguments } = expressionArrayVisitor;
|
||||
return constantArguments;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @param {Scope.Definition} requireUsingReference
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getModuleNameFromRequireAssignDeclaration(requireUsingReference) {
|
||||
assert(requireUsingReference.node.init.arguments.length === 1);
|
||||
const moduleImported = requireUsingReference.node.init?.arguments?.[0]?.value ?? null;
|
||||
assert(moduleImported !== null, "Module has to exist");
|
||||
|
||||
return moduleImported;
|
||||
}
|
||||
|
80
src/index.mjs
Normal file
80
src/index.mjs
Normal file
@@ -0,0 +1,80 @@
|
||||
|
||||
import assert from 'node:assert';
|
||||
import { getASTAndScope } from './ast/analysis.mjs';
|
||||
|
||||
import { getRequireCallsAndConstantArgs } from './calls.mjs';
|
||||
import { analyze, instrumentString, instrumentDir } from 'jalangi2';
|
||||
import { readFileSync ,realpathSync} from 'node:fs';
|
||||
|
||||
import {getSliceAndInfoSync} from 'slice-js/dist/slice-code/test/helpers/utils.js';
|
||||
import { dirname,join } from 'node:path';
|
||||
/**
|
||||
* Call parameter generation
|
||||
*/
|
||||
function main() {
|
||||
const FILE_PATH = './test_src/index.cjs';
|
||||
const { scopeManager, _parsedModAST } = getASTAndScope(FILE_PATH);
|
||||
assert(scopeManager.scopes.length >= 2, "expected atleast global and module scope");
|
||||
assert(scopeManager.scopes[1].type === 'function', "expected the 'module' scope to have function scope");
|
||||
|
||||
const calls = getRequireCallsAndConstantArgs(scopeManager);
|
||||
|
||||
for (const [moduleName, callBoxes] of calls.entries()) {
|
||||
if (moduleName.startsWith('.')) {
|
||||
console.log('Importing', moduleName, callBoxes);
|
||||
} else {
|
||||
console.log(`Module "${moduleName}" - System module. FIXME skipping`);
|
||||
}
|
||||
}
|
||||
console.log(`Call List`, calls);
|
||||
|
||||
for (const [moduleName, callBox] of calls) {
|
||||
// console.log(callBox);
|
||||
if (!moduleName.startsWith('.')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const relatedModuleNamePath = join(realpathSync(dirname(FILE_PATH)) ,moduleName);
|
||||
const fileSource = readFileSync(relatedModuleNamePath).toString('utf-8');
|
||||
const {slicedCode} = getSliceAndInfoSync(fileSource, (moduleExports) => {
|
||||
return [...callBox.entries()].flatMap(([methodName, methodArgsList])=>{
|
||||
const methodNameNormed = methodName.substring(1);
|
||||
console.log("Calls for ",methodNameNormed,methodArgsList)
|
||||
return methodArgsList.map(methodArgsList=>moduleExports[methodNameNormed].apply(moduleExports[methodNameNormed],methodArgsList));
|
||||
})
|
||||
},relatedModuleNamePath);
|
||||
console.log(`Sliced code ${moduleName}\n`,slicedCode);
|
||||
}
|
||||
}
|
||||
|
||||
function jalangiInstrumentMain() {
|
||||
const FILE_PATH = './test_src/index.cjs';
|
||||
|
||||
|
||||
const fileString = readFileSync(FILE_PATH).toString();
|
||||
const y = instrumentString(fileString, {});
|
||||
console.log(y);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Analysis POC
|
||||
*/
|
||||
function jalangiAnalyzeMain() {
|
||||
const FILE_PATH = './test_src/index.cjs';
|
||||
|
||||
const y = analyze(FILE_PATH, ["./node_modules/jalangi2/src/js/sample_analyses/tutorial/LogAll.js", "./src_analysis/analysisCallbackTemplate.cjs"]);
|
||||
// const x = 5;
|
||||
y.then(yp => {
|
||||
console.log("Analysis complete", yp);
|
||||
}).catch(console.error).finally(kek => {
|
||||
console.log("Threw error", kek);
|
||||
})
|
||||
}
|
||||
|
||||
if (process.argv[1] === import.meta.filename) {
|
||||
console.log("[SafeImport] started")
|
||||
main();
|
||||
}
|
||||
|
||||
|
35
src/libcalls.mjs
Normal file
35
src/libcalls.mjs
Normal file
@@ -0,0 +1,35 @@
|
||||
|
||||
//@ts-check
|
||||
/**
|
||||
* @typedef {import('estree').Literal["value"]} GenericLiteralType
|
||||
*/
|
||||
|
||||
/**
|
||||
* Record library calls
|
||||
*/
|
||||
export class LibraryCallsRecorder{
|
||||
/**
|
||||
* @type {Map<string,Map<string,GenericLiteralType[][]>>}
|
||||
*/
|
||||
#calls = new Map();
|
||||
/**
|
||||
*
|
||||
* @param {string} moduleName
|
||||
* @param {string} libraryFunctionSegment
|
||||
* @param {any[]} argumentsCalled
|
||||
*/
|
||||
pushToMap(moduleName, libraryFunctionSegment, argumentsCalled){
|
||||
const modulePortion = this.#calls.get(moduleName)?? new Map();
|
||||
|
||||
const defArgs = modulePortion.get(libraryFunctionSegment) ?? [];
|
||||
defArgs.push(argumentsCalled);
|
||||
|
||||
modulePortion.set(libraryFunctionSegment,defArgs);
|
||||
this.#calls.set(moduleName, modulePortion);
|
||||
}
|
||||
|
||||
get calls(){
|
||||
return this.#calls;
|
||||
}
|
||||
|
||||
}
|
5
src/slice-js.d.ts
vendored
Normal file
5
src/slice-js.d.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
declare module 'slice-js' {
|
||||
declare async function slice(fileName: string, name: string, tester: (...args:any[])=>any);
|
||||
}
|
||||
|
||||
export = slice;
|
3
src/utils/constants.mjs
Normal file
3
src/utils/constants.mjs
Normal file
@@ -0,0 +1,3 @@
|
||||
|
||||
export const prependString = `(function(exports, require, module, __filename, __dirname) {\n`
|
||||
export const appendString = `\n});`
|
Reference in New Issue
Block a user