From ab0bdd895006362bdcce454d8787a0e3b4f6399e Mon Sep 17 00:00:00 2001 From: igibek Date: Wed, 28 Oct 2020 10:19:06 -0400 Subject: [PATCH] mininode src --- Challenges.md | 81 ++ LICENSE | 26 + README.md | 26 + src/index.js | 390 +++++++++ src/lib/.settings.json | 16 + src/lib/Analyzer.js | 546 ++++++++++++ src/lib/Configurator.js | 28 + src/lib/Detector.js | 114 +++ src/lib/Generator.js | 132 +++ src/lib/Reducer.js | 203 +++++ src/lib/Scope.js | 33 + src/lib/Stat.js | 321 +++++++ src/lib/Traverser.js | 96 +++ src/lib/attack.json | 141 +++ src/lib/models/AppBuilder.js | 65 ++ src/lib/models/Identifier.js | 48 ++ src/lib/models/IdentifierTable.js | 131 +++ src/lib/models/ModuleBuilder.js | 73 ++ src/lib/modules.surfaces.json | 24 + src/lib/utils/ast-utils.js | 39 + src/lib/utils/binary-expression-utils.js | 115 +++ src/lib/utils/entry-point.js | 34 + src/lib/utils/file-dependencies.js | 22 + src/lib/utils/flatten-object-keys.js | 25 + src/lib/utils/helpers.js | 270 ++++++ src/lib/utils/index.js | 187 ++++ src/lib/utils/loader.js | 21 + src/lib/utils/member-expression-utils.js | 194 +++++ src/lib/utils/native-modules.js | 31 + src/lib/utils/package-dependencies.js | 86 ++ src/lib/utils/set-operations.js | 10 + src/package-lock.json | 814 ++++++++++++++++++ src/package.json | 30 + src/test-module/check.js | 36 + src/test-module/index.js | 152 ++++ src/test-module/node_modules/crossref/bar.js | 8 + src/test-module/node_modules/crossref/foo.js | 4 + .../node_modules/crossref/index.js | 5 + .../node_modules/dynamic-export/index.js | 20 + .../node_modules/dynamic-import/bar.js | 16 + .../node_modules/dynamic-import/baz.js | 3 + .../node_modules/dynamic-import/extra.js | 3 + .../node_modules/dynamic-import/foo.js | 2 + .../node_modules/dynamic-import/index.js | 11 + .../node_modules/dynamic-module-usage/bar.js | 5 + .../node_modules/dynamic-module-usage/foo.js | 3 + .../dynamic-module-usage/index.js | 16 + .../node_modules/dynamic-module/bar.js | 4 + .../node_modules/dynamic-module/foo.js | 8 + .../node_modules/dynamic-module/index.js | 11 + .../node_modules/function-export/index.js | 6 + .../node_modules/global-module/bar.js | 12 + .../node_modules/global-module/foo.js | 6 + .../node_modules/global-module/index.js | 6 + .../invisible-child-parent/child.js | 12 + .../invisible-child-parent/index.js | 4 + .../invisible-child-parent/parent.js | 15 + .../node_modules/monkey-patching/foo.js | 2 + .../node_modules/monkey-patching/index.js | 7 + src/test-module/node_modules/normal.js | 38 + .../node_modules/object-def-export/index.js | 6 + .../node_modules/proto-export/index.js | 26 + src/test-module/node_modules/re-export/foo.js | 3 + .../node_modules/re-export/index.js | 1 + .../node_modules/rename-export/index.js | 13 + .../node_modules/rename-require/bar.js | 4 + .../node_modules/rename-require/foo.js | 4 + .../node_modules/rename-require/index.js | 11 + src/test-module/package.json | 11 + src/test-module/test.js | 5 + src/test-module/webpack.config.js | 9 + 71 files changed, 4880 insertions(+) create mode 100644 Challenges.md create mode 100644 LICENSE create mode 100644 README.md create mode 100644 src/index.js create mode 100644 src/lib/.settings.json create mode 100644 src/lib/Analyzer.js create mode 100644 src/lib/Configurator.js create mode 100644 src/lib/Detector.js create mode 100644 src/lib/Generator.js create mode 100644 src/lib/Reducer.js create mode 100644 src/lib/Scope.js create mode 100644 src/lib/Stat.js create mode 100644 src/lib/Traverser.js create mode 100644 src/lib/attack.json create mode 100644 src/lib/models/AppBuilder.js create mode 100644 src/lib/models/Identifier.js create mode 100644 src/lib/models/IdentifierTable.js create mode 100644 src/lib/models/ModuleBuilder.js create mode 100644 src/lib/modules.surfaces.json create mode 100644 src/lib/utils/ast-utils.js create mode 100644 src/lib/utils/binary-expression-utils.js create mode 100644 src/lib/utils/entry-point.js create mode 100644 src/lib/utils/file-dependencies.js create mode 100644 src/lib/utils/flatten-object-keys.js create mode 100644 src/lib/utils/helpers.js create mode 100644 src/lib/utils/index.js create mode 100644 src/lib/utils/loader.js create mode 100644 src/lib/utils/member-expression-utils.js create mode 100644 src/lib/utils/native-modules.js create mode 100644 src/lib/utils/package-dependencies.js create mode 100644 src/lib/utils/set-operations.js create mode 100644 src/package-lock.json create mode 100644 src/package.json create mode 100644 src/test-module/check.js create mode 100644 src/test-module/index.js create mode 100644 src/test-module/node_modules/crossref/bar.js create mode 100644 src/test-module/node_modules/crossref/foo.js create mode 100644 src/test-module/node_modules/crossref/index.js create mode 100644 src/test-module/node_modules/dynamic-export/index.js create mode 100644 src/test-module/node_modules/dynamic-import/bar.js create mode 100644 src/test-module/node_modules/dynamic-import/baz.js create mode 100644 src/test-module/node_modules/dynamic-import/extra.js create mode 100644 src/test-module/node_modules/dynamic-import/foo.js create mode 100644 src/test-module/node_modules/dynamic-import/index.js create mode 100644 src/test-module/node_modules/dynamic-module-usage/bar.js create mode 100644 src/test-module/node_modules/dynamic-module-usage/foo.js create mode 100644 src/test-module/node_modules/dynamic-module-usage/index.js create mode 100644 src/test-module/node_modules/dynamic-module/bar.js create mode 100644 src/test-module/node_modules/dynamic-module/foo.js create mode 100644 src/test-module/node_modules/dynamic-module/index.js create mode 100644 src/test-module/node_modules/function-export/index.js create mode 100644 src/test-module/node_modules/global-module/bar.js create mode 100644 src/test-module/node_modules/global-module/foo.js create mode 100644 src/test-module/node_modules/global-module/index.js create mode 100644 src/test-module/node_modules/invisible-child-parent/child.js create mode 100644 src/test-module/node_modules/invisible-child-parent/index.js create mode 100644 src/test-module/node_modules/invisible-child-parent/parent.js create mode 100644 src/test-module/node_modules/monkey-patching/foo.js create mode 100644 src/test-module/node_modules/monkey-patching/index.js create mode 100644 src/test-module/node_modules/normal.js create mode 100644 src/test-module/node_modules/object-def-export/index.js create mode 100644 src/test-module/node_modules/proto-export/index.js create mode 100644 src/test-module/node_modules/re-export/foo.js create mode 100644 src/test-module/node_modules/re-export/index.js create mode 100644 src/test-module/node_modules/rename-export/index.js create mode 100644 src/test-module/node_modules/rename-require/bar.js create mode 100644 src/test-module/node_modules/rename-require/foo.js create mode 100644 src/test-module/node_modules/rename-require/index.js create mode 100644 src/test-module/package.json create mode 100644 src/test-module/test.js create mode 100644 src/test-module/webpack.config.js diff --git a/Challenges.md b/Challenges.md new file mode 100644 index 0000000..3886775 --- /dev/null +++ b/Challenges.md @@ -0,0 +1,81 @@ +### 1. Binary dependency detection +The binary file can't be used as a CommonJS module because it has a shebang (`#! /usr/bin/env node`) at the top, which is an invalid syntax for JavaScript. That is why they are not considered when building a dependency tree of the application. The application fails when it tries to run test binary such as `mocha` because Mininode removed required dependencies. +- Issue: #65 +- Commit: _still working on automatic_ + +**Solution**: The idea behind the solution is to install devDependencies as global packages. In this way `mocha` or other executable JS files required for testing will not fail because Mininode will not reduce it. Additionally, because of executable JS files cannot be used as a required module we can remove them from node_modules folder _"safely"_. However, if executable JS files are called indirectly from modules using `child_processes`, our solution will break the application. + +### 2. Dynamic manipulation of the required module +Dynamic manipulation happens when the required module is passed to some *dynamic* function. The dynamic function is a function which is not defined inside the requested module. +```JavaScript +var utils = require('utils'); +foo(utils); // we don't know what will happen inside foo function. +``` + +### 3. Invisible child-parent exporting +Example: debug module. +### 4. Monkey-patching / Extending the required module +Example: +```JavaScript +// in malware.js +var express = require(express''); +express.get = function() { + // rewrite original get to any functionality +} +``` +### 5. Dynamically importing modules +When require is passed a variable. +```JavaScript +var a = null; +if (b === 0) + a = 'bar' +else + a = 'foo' +const c = require(a) +``` + +### 6. Overwriting/renaming the require function + +### 7. Cross-reference dependencies +```JavaScript +// in index.js +var foo = require('foo'), bar = require('bar'); +foo.x() +bar.a() +// in foo.js +var bar = require('bar'); +exports.x = function(){} +exports.y = function(){} +// in bar.js +var foo = require('foo') +exports.a = function() {} +exports.b = function() { + foo.y() +} +``` +### 8. Re-assigning exports (module.exports) to another variable. +Example: +```JavaScript +var es = exports, flatmap = require('flatmap-stream'); +es.flatmap = flatmap; +``` +### 9. Dynamically exporting functionality from module +There may be diffirent ways to dynamically export the functionality. +Example: +```JavaScript +var member = 'foo'; +exports[member]; +``` +### 10. Requiring module globally +Example: +```JavaScript +// inside a.js +let foo = require('foo'); +bar = require('bar'); // globally requiring +globalFoo = foo; // global variable +// inside b.js +bar.a(); // should detect this. +globalFoo.b() // should detect this +``` + +### 11. Exporting using Object.defineProperty \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a4b3b10 --- /dev/null +++ b/LICENSE @@ -0,0 +1,26 @@ +Copyright (c) 2019, North Carolina State University +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +3. Neither the name of North Carolina State University nor the names of its +contributors may be used to endorse or promote products derived from this +software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..f71c34c --- /dev/null +++ b/README.md @@ -0,0 +1,26 @@ +# Mininode +Mininode is a CLI tool to reduce the attack surface of the Node.js applications by using static analysis. + + + +### Options +List of command line options that can be passed to mininode. + +- `--dry-run`: just generates mininode.json without modifying the initial application. +- `--skip-stat`: skips calculating the statistics +- `--seeds`: seed files from where mininode will start building dependency graph. You can provide many seed files by separating them with colon. +- `--mode`: reduction mode. The value can be either `soft` or `hard`. In `soft` mode mininode will perform only coarse-grained reduction. While in `hard` mode mininode will perform fine-grained reduction. In general coarse-grained reduction is more reliable, because mininode will not try to reduce unused functions inside the module. Default value: `soft`. +- `--destination`: the path where mininode will save the reduced Node.js application. The default value: `mininode`. +- `--silent`: console output is disabled. This will improve the performance of the mininode. +- `--verbose`: outputs additional information to the console. The default value: `false` +- `--log`: mininode will generate log file inside, which contains dependency graph of the application in json format. The default value: `true`. +- `--log-output`: the name of the log file generated by mininode. The default value: `mininode.json`. +- `--compress-log`: compresses the final log file. By default it will dump everything into log file. In production it is advised to pass the `--compress-log` flag to save space. +- `--skip-reduction`: if passed mininode will not reduce the JavaScript files. The default value: `false`. +- `--skip-remove`: if passed mininode will not remove unused JavaScript files. The default value: `false`. + +### Contributing +[![js-semistandard-style](https://cdn.rawgit.com/flet/semistandard/master/badge.svg)](https://github.com/Flet/semistandard) + +We are following semistandard. + diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..a27b423 --- /dev/null +++ b/src/index.js @@ -0,0 +1,390 @@ +const fs = require('fs'); +const es = require('esprima'); +const escodegen = require('escodegen'); +const path = require('path'); +const argv = require('yargs').argv; +const chalk = require('chalk'); +const sloc = require('sloc'); + +const AppBuilder = require('./lib/models/AppBuilder'); +const ModuleBuilder = require('./lib/models/ModuleBuilder'); +const generator = require('./lib/Generator'); +const Analyzer = require('./lib/Analyzer'); +const Reducer = require('./lib/Reducer'); +const Detector = require('./lib/Detector'); +const config = require('./lib/Configurator'); +const stat = require('./lib/Stat'); +const utils = require('./lib/utils'); +const packageDependencies = require('./lib/utils/package-dependencies'); + + +let location = process.argv[2]; +let utf = 'utf-8'; +location = path.resolve(location); +if (location.indexOf('package.json') !== -1) { + location = path.dirname(location); +} + +let _app = new AppBuilder(); +let entries = []; +// let devs = packageDependencies.toList(location, true); +// console.log(devs, devs.length); +// process.exit(0); +(async function () { + console.time('overall'); + try { + console.info(chalk.bold.blue(`====================\nSTARTED ${location}\n====================`)); + console.info(chalk.bold.green('>> INITIALIZING...')); + await init(); + console.info(chalk.bold('>> DONE INITIALIZING')); + + console.info(chalk.bold.green('>> TRAVERSING...')); + await traverse(location); + console.info(chalk.bold('>> DONE TRAVERSING')); + + console.log(chalk.bold.green('>> DETECTOR...')); + await Detector(_app); + console.log(chalk.bold(">> DONE DETECTOR")) + + if (_app.usedComplicatedDynamicImport) { + throw new Error('DYNAMIC_IMPORT_DETECTED'); + } + + if (config.mode === 'hard') { + console.log(chalk.bold.green('>> BUILDING DEPENDENCY GRAPH...')); + await buildDependency(); + console.log(chalk.bold('>> DONE BUILDING DEPENDENCY GRAPH')); + // await attackSurface(); + + console.log(chalk.bold.green('>> FINAL REDUCING...')); + /** + * 1. iterate modules' memusage + * 2. map results to app.globals by name + * 3. add to individual modules used globals. + */ + for (let modul of _app.modules) { + for (let mem in modul.memusages) { + _app.globals.forEach(i => { + if (i.name === mem) { + for(let m of modul.memusages[mem]) { + i.members.push(m); + } + } + }) + } + } + + for (let modul of _app.modules) { + for (let glb of _app.globals) { + if (glb.path === modul.path) { + for (let m of glb.members) { + modul.used.push(m); + } + } + } + } + + let usedExternalModules = _app.modules.filter(m => (m.isUsed && !m.skipReduce)); + for (let modul of usedExternalModules) { + if (config.verbose) console.log(chalk.bold('> REDUCING:'), modul.path); + await reduceModule(modul); + if (config.verbose) console.log('-- done'); + } + console.log(chalk.bold('>> DONE FINAL REDUCING')); + + } + + if (!config.skipRemove) { + console.log(chalk.bold.green('>> REMOVING UNUSED MODULES...')); + + if (!_app.usedComplicatedDynamicImport) { + let modulesToRemove = _app.modules.filter(m => !m.isUsed); + for (let modul of modulesToRemove) { + await utils.removeFile(modul, config.dryRun); + } + } else if (config.mode === 'hard'){ + // run reduction with dimports + for (const dimport of _app.dimports) { + if (config.verbose) console.log('> DYNAMIC IMPORT REDUCTION', _app.dimports); + let reducableModules = _app.modules.filter(m => (!m.skipReduce) && dimport.members.every(v => m.members.includes(v))); + for (let modul of reducableModules) { + await reduceModule(modul, dimport.members); + } + } + } + + console.log(chalk.bold('>> DONE REMOVING UNUSED MODULES')); + } + + console.log(chalk.bold.green('>> GENERATING...')); + let usedModules = _app.modules.filter(m => !m.isRemoved && !m.skipReduce); + for (var modul of usedModules) { + if (!modul.parseError) { + generator.generate(modul, config.dryRun); + } + } + console.log(chalk.bold('>> DONE GENERATING')); + + console.log(chalk.bold.green('>> CALCULATING STATS...')); + utils.calculateAppStatictic(_app); + console.log(chalk.bold('>> DONE CALCULATING')); + + if (config.log) { + console.log(chalk.bold.green('>> CREATING MININODE.JSON...')); + let log_path = path.join(location, config.logOutput); + fs.writeFileSync(log_path, JSON.stringify(_app, null, 2), {encoding: 'utf-8'}); + console.log(chalk.bold('>> DONE CREATING MININODE.JSON')); + } + + } catch (err) { + throw err; + } + console.timeEnd('overall'); +})().catch(e => { + console.error(e); + process.exit(1); +}); + + +/** + * Initializes the initial state of the application. + */ +async function init () { + if (!config.dryRun) { + generator(location, config.destination); + location = path.resolve(config.destination); + } + + let content = fs.readFileSync(path.join(location, 'package.json'), {encoding: utf}); + let packageJson = JSON.parse(content); + + _app.appname = packageJson.name; + _app.version = packageJson.version; + _app.path = location; + _app.main = utils.entryPoint(location, packageJson.main); + + if (!_app.main) { + throw new Error("NO_ENTRY_POINT"); + } + entries.push(path.join(_app.path, _app.main)); + + if (packageJson.dependencies) { + _app.declaredDependencyCount = Object.keys(packageJson.dependencies).length; + let packageLock = path.join(location, 'package-lock.json'); + if (fs.existsSync(packageLock)) { + content = fs.readFileSync(path.join(location, 'package-lock.json'), {encoding: utf}); + packageJson = JSON.parse(content); + packageDependencies.installedPackages(packageJson, _app.dependencies); + _app.installedUniqueDependencyCount = Object.keys(_app.dependencies).length; + for(let i in _app.dependencies) { + _app.installedTotalDependencyCount += _app.dependencies[i].length; + } + } + } + // entries = utils.entryPoints(_app); + + // if (entries.length === 0) { + // console.error(chalk.bold.red('NO ENTRY POINTS SPECIFIED')); + // throw new Error('NO ENTRY POINTS SPECIFIED'); + // } +} + +/** + * Builds the dependency graph of the application + */ +async function buildDependency () { + + for(let entry of entries) { + let entryModule = _app.modules.find(m => { return m.path === entry; }); + if (!entryModule) { + console.error(chalk.red(`NO_ENTRY_POINT:`, entry)); + process.exit(1); + } + entryModule.skipReduce = true; + entryModule.isUsed = true; + console.log(chalk.bold.yellow('> STARTING:'), entry) + await readModule(entryModule); + } +} + +/** + * @param {ModuleBuilder} modul + */ +async function readModule (modul) { + + try { + /** + * Reducer part: + * it will skip reduction if: + * - module was marked with "SkipReduce" flag + * - or --skip-reduce flag was setup when script started + */ + if (!config.skipReduction && !modul.skipReduce) { + if(!config.silent) console.log(chalk.bold('> REDUCING:'), modul.path); + await reduceModule(modul); + if(!config.silent) console.log('-- done'); + } + + /** + * Analyzer part: + */ + if(!config.silent) console.log(chalk.bold('> ANALYZING:'), modul.path); + let result = await Analyzer.analyze(modul); + if(!config.silent) console.log('-- done'); + if (config.verbose) console.log(result); + + for (let i of modul.descendents) { + let descendent = _app.modules.find(m => { return m.path === i; }); + if (descendent) { + if (!descendent.ancestors.includes(modul.path)) descendent.ancestors.push(modul.path); + Array.prototype.push.apply(descendent.used, modul.used); + descendent.isUsed = true; + if (modul.skipReduce) descendent.skipReduce = true; + await readModule(descendent); + } + } + + /** + * Invokes readModule for module's child + * only if child's usage has changed + */ + let changed = false; + for (let cindex in modul.children) { + // let child = _app.modules.find(m => { return m.path === cindex; }); + let child = modul.children[cindex].ref; + if (!child) continue; + // if (modul.dynamicUsage) child.skipReduce = true; // why I wrote this line? + if (modul.dynamicChildren.includes(cindex)) { + child.skipReduce = true; + } + + child.isUsed = true; + for (let e of modul.children[cindex].used) { + if (!child.used.includes(e)) { + child.used.push(e); + changed = true; + } + } + if (changed) { // calling readModule only if child was updated + if (argv.verbose) console.log(chalk.cyan('READMODULE:'), child.path, child.used); + await readModule(child); + changed = false; + } + } + } catch (err) { + throw err; + } +} + +/** + * Traverses given directory and create ModuleBuilder object for each .js file + * @returns {Boolean} + * @param {String} directory + */ +async function traverse (directory) { + try { + var folderContent = fs.readdirSync(directory); + for (let item of folderContent) { + item = path.join(directory, item); + let stat = fs.statSync(item); + if (stat.isDirectory()) { + await traverse(item); + } else if (stat.isFile()) { + let extension = path.extname(item).toLowerCase(); + if (extension === '.js' || extension === '') { + var _module = new ModuleBuilder(); + _module.app = _app; + _module.name = path.basename(item); + _module.path = item; + + if (directory.indexOf('/node_modules', location.length - 1) === -1) { + _module.isOwned = true; + // should we add do not reduce here? + } else { + let arr = _module.path.split('/'); + let lIndex = arr.lastIndexOf('node_modules'); + _module.packagename = arr[lIndex + 1]; + // scoped packages packagename + if (_module.packagename.startsWith('@')) { + _module.packagename += '/' + arr[lIndex+2]; + } + } + _module.initialSrc = fs.readFileSync(`${_module.path}`, utf); + await moduleStat(_module); + + // let exprt = utils.loader(_module.path); + // if (exprt) _module.members = utils.flattenObjectKeys(exprt); + + _app.modules.push(_module); + } + } + } + } catch (err) { + throw new Error(err); + } + return true; +} + +/** + * Generate a general statistic for the module + * @param {ModuleBuilder} modul + */ +async function moduleStat (modul) { + try { + if (modul.initialSrc.startsWith('#!')) { + modul.hashbang = modul.initialSrc.split('\n', 1)[0]; // saves the hashbang value + modul.initialSrc = modul.initialSrc.replace(/^#!(.*\n)/, ''); // removes hashbang value + } + // modul.initialSrc = modul.initialSrc.replace(/\.\.\./g, '_mininode_'); // breaks the test + + + let ast = es.parseScript(modul.initialSrc, {range: true, tokens: true, comment: true}); + modul.ast = ast; + + //calculating the SLOC + let gen = escodegen.generate(modul.ast); + modul.initialSloc = sloc(gen, 'js').source; + modul.finalSloc = modul.initialSloc; + } catch (ex) { + if (config.verbose) console.warn(chalk.bold.yellow('WARN:'), modul.path, ex.message); + modul.parseError = true; + } finally { + modul.initialSrc = null; + } + try { + if(!config.skipStat && !modul.parseError) { + await stat(modul); + } + } catch (error) { + throw new Error(`STAT FAILED: ${modul.path} ${error.message}`); + } + + + return modul; +} + +/** + * Executes Reducer.reduce function on a provided module + * @param {ModuleBuilder} modul + */ +async function reduceModule(modul, extra = null) { + let additional = []; + + if (extra && extra.length > 0) { + Array.prototype.push.apply(additional, extra); + } + + for (let i of modul.descendents) { + let descendent = _app.modules.find(m => { return m.path === i; }); + if (descendent) Array.prototype.push.apply(additional, descendent.selfUsed); + } + + for (let i of modul.ancestors) { + let ancestor = _app.modules.find(m => { return m.path === i; }); + Array.prototype.push.apply(additional, ancestor.selfUsed); + } + + + await Reducer.reduce(modul, additional); +} + diff --git a/src/lib/.settings.json b/src/lib/.settings.json new file mode 100644 index 0000000..6b24cd1 --- /dev/null +++ b/src/lib/.settings.json @@ -0,0 +1,16 @@ +{ + "dryRun": false, + "skipReduction": false, + "default_attack_surface": true, + "extra_attack_surface": null, + "destination": "mininode", + "log": true, + "logOutput": "mininode.json", + "compressLog": false, + "seeds": [], + "skipStat": false, + "verbose": false, + "skipRemove": false, + "silent": false, + "mode": "soft" +} \ No newline at end of file diff --git a/src/lib/Analyzer.js b/src/lib/Analyzer.js new file mode 100644 index 0000000..f3f8400 --- /dev/null +++ b/src/lib/Analyzer.js @@ -0,0 +1,546 @@ +/** + * @author Igibek Koishybayev + * @abstract Analyzes the AST of the module to detect + * - used/unused members of the required modules + */ +const estraverse = require('estraverse'); +const syntax = require('esprima').Syntax; +const chalk = require('chalk'); +const config = require('./Configurator'); +const helper = require('./utils/helpers'); +const path = require('path'); +const resolveFrom = require('resolve-from'); +let ModuleBuilder = require('./models/ModuleBuilder'); +let utils = require('./utils'); +let resolve = null; + +let variables = []; // all variables +let assignments = [], vars = [], leaks = []; +let trackids = []; // required modules variable identifiers +let callbacks = []; +let currentScope = -1; + +async function init () { + variables = []; + assignments = []; + vars = []; + leaks = []; + trackids = []; + callbacks = []; + currentScope = -1; +} +/** + * @param {ModuleBuilder} modul + * @returns {AnalyzeResult} + */ +module.exports.analyze = async function analyze (modul) { + await init(); + resolve = resolveFrom.silent.bind(null, path.dirname(modul.path)); + await traverse(modul); + if (config.verbose) console.log(chalk.bold.cyan('SCOPE VARIABLES:'), vars); + if (config.verbose) console.log(chalk.bold.cyan('SCOPE ASSIGNMENTS:'), assignments); + await checkForLeaks() + if (config.verbose) console.log(chalk.bold.cyan('LEAKED GLOBALS:'), leaks); + variables.forEach((item, index) => { + if (item.isModule && item.value) { + if (utils.hasKey(modul.children, item.value)) { + for (var member of item.members) { + if (!modul.children[item.value].used.includes(member)) { + modul.children[item.value].used.push(member); + } + } + } else { + modul.children[item.value] = {}; + modul.children[item.value].used = item.members.slice(); + } + + // marking dynamically used children + if(item.isDynamic && !modul.dynamicChildren.includes(item.value)) { + modul.dynamicChildren.push(item.value); + } + + if (leaks.includes(item.name)) { + if (config.verbose) console.log(chalk.bold.red('GLOBAL TO APP:', item.name)); + modul.app.globals.push({name: item.name, path: item.value, members: item.members.slice()}); + } + } else if (item.isModule && item.value === null) { + console.log(chalk.bold.cyan('DYNAMIC IMPORT:'), item.name, modul.path); + let arr = modul.app.dimports.concat({name: item.name, members: item.members.filter(i => i !== '.'), by: modul.path}); + modul.app.dimports = [...new Set(arr)]; + } + }); +}; + +/** + * + * @param {ModuleBuilder} modul + */ +async function traverse (modul) { + estraverse.traverse(modul.ast, { + enter: function (node, parent) { + node['xParent'] = parent; + if (helper.createsNewScope(node)) { + currentScope += 1; + if (vars[currentScope] === undefined) { + vars.push([[]]); + assignments.push([[]]); + } else { + vars[currentScope].push([]); + assignments[currentScope].push([]); + } + } + if (node['xUsed'] === false) { + // todo: implement skipping mechanisms + // I need to skip unused function declarations + // I need to skip unused exports initialized with function declarations + if (node.type === syntax.FunctionDeclaration || node.type === syntax.FunctionExpression){// || node.type === syntax.VariableDeclarator) { + this.skip(); + } else if (node.type === syntax.MemberExpression && parent.type === syntax.AssignmentExpression) { + if (parent.xParent.type !== syntax.AssignmentExpression && parent.right.type === syntax.FunctionExpression) { + this.skip(); + } + } + } + + switch (node.type) { + case syntax.Identifier: + if (parent && (parent.type !== syntax.VariableDeclarator || parent.type !== syntax.AssignmentExpression || parent.type !== syntax.FunctionDeclaration)) + { + let variable = getLatestVariable(variables, node.name, true); + if (variable && !variable.members.includes('.')) variable.members.push('.'); + } + break; + case syntax.VariableDeclarator: + if (node.id.type === syntax.Identifier) { + vars[currentScope][vars[currentScope].length - 1].push(node.id.name); + } + VariableDeclarator(node); + break; + case syntax.MemberExpression: + if (node.object.type === syntax.Identifier) { // makes sure that this will be called only for top memberexpression + let isComputed = helper.isComputed(node); + if ((modul.exporters.includes(node.object.name)) || (node.object.name === 'exports') || (node.object.name === 'module' && node.property.name === 'exports')) { + let memexp = helper.getMemberExpressionString(node); + let full = memexp.object + '.' + memexp.property; + let prop = helper.getExportedProperty(full); + if (!prop) prop = memexp.property; + let assignment = helper.closestsNot(node, syntax.MemberExpression); + if (assignment.type === syntax.AssignmentExpression) { + // todo: detect if it is assigned or used by comparing left and right + if (assignment.left.type === syntax.MemberExpression) { // + let left = helper.getMemberExpressionString(assignment.left); + left = left.object + '.' + left.property; + if (left === full) { + if (!modul.members.includes(prop)) modul.members.push(prop); + // detecting descendents + if (assignment.right.type === syntax.Identifier) { + if (trackids.includes(assignment.right.name)) { + let variable = getLatestVariable(variables, assignment.right.name); + if (variable) modul.descendents.push(variable.value); + } + } + // todo: add detecting descendents in forms of memberexpression. Ex: var des = require('foo'); module.exports = des.another; + } + } + if (assignment.right.type === syntax.MemberExpression) { + let right = helper.getMemberExpressionString(assignment.right); + if (right) { + right = right.object + '.' + right.property; + if (right === full) { + if (!modul.selfUsed.includes(prop)) { + modul.selfUsed.push(prop); + let propStart = getPropertyStart(prop); + if (propStart && !modul.selfUsed.includes(propStart)) modul.selfUsed.push(propStart); + } + } + } + } + } else { + if (!modul.selfUsed.includes(prop)) { + modul.selfUsed.push(prop); + let propStart = getPropertyStart(prop); + if (propStart && !modul.selfUsed.includes(propStart)) modul.selfUsed.push(propStart); + } + } + } else { + let item = callbacks.filter((item, index) => { + return item.name === node.object.name && item.scope <= currentScope; + }).sort(sortByScope).pop(); + let variable = null; + let propertyName = helper.getPropertyName(node); + if (item) { + variable = getLatestVariable(variables, item.value); + if (variable) { + variable.members.push(propertyName); + let propStart = getPropertyStart(propertyName); + if (propStart && !variable.members.includes(propStart)) variable.members.push(propStart); + } + } else if (trackids.includes(node.object.name)) { + variable = getLatestVariable(variables, node.object.name); + if (variable) { + variable.members.push(propertyName); + let propStart = getPropertyStart(propertyName); + if (propStart && !variable.members.includes(propStart)) variable.members.push(propStart); + } + } else if(!isDeclared(node.object.name) && node.object.name !== 'constructor') { + let objname = node.object.name; + if (utils.hasKey(modul.memusages, objname)) { + if (!modul.memusages[objname].includes(propertyName)) { + modul.memusages[objname].push(propertyName); + } + } else { + modul.memusages[objname] = [propertyName]; + } + } + if (variable && isComputed) variable.isDynamic = true; + } + } + break; + case syntax.CallExpression: + if ((modul.requires.includes(node.callee.name) || node.callee.name === 'require') && node.arguments.length === 1) { + let importPath = null; + if (node.arguments[0].type === syntax.Literal) { + importPath = resolve(node.arguments[0].value); + } else if (node.arguments[0].type === syntax.Identifier) { + if (utils.hasKey(node, 'xModule')) { + console.log(node['xModule']) + importPath = node['xModule'][0]; + } + } + + let assignment = helper.closests(node, syntax.AssignmentExpression); + if (assignment) { + if (assignment.left.type === syntax.MemberExpression) { + let memexp = helper.getMemberExpressionString(assignment.left); + // chaining detection + if (memexp && (memexp.object === 'module' || memexp.object === 'exports' || modul.exporters.includes(memexp.object))) { + // if (memexp.property === 'exports' || memexp.property.startsWith('exports.')) { + if(!config.silent) console.log(chalk.bold.magenta(`-- CHAINING`), memexp, importPath); + if (!modul.descendents.includes(importPath)) { + modul.descendents.push(importPath); + } + if (parent && parent.type === syntax.MemberExpression) { + let propertyName = helper.getPropertyName(parent); + if (propertyName) { + let variable = new VariableBuilder(propertyName, importPath, true); // why name is propertyName? + variable.members.push(propertyName); + variables.push(variable); + } + } + // } + } else if (memexp) { + let variable = new VariableBuilder(memexp.object + '.' + memexp.property, importPath, true); + let propertyName = helper.getPropertyName(parent); // detects require('something').property + if (propertyName) variable.members.push(propertyName); + variable.members.push('.'); + trackids.push(variable.name); + variables.push(variable); + } + } else if (assignment.left.type === syntax.Identifier) { + let variable = new VariableBuilder(assignment.left.name, importPath, true); + let propertyName = helper.getPropertyName(parent); // detects require('something').property + if (propertyName) variable.members.push(propertyName); + trackids.push(variable.name); + variables.push(variable); + } + } + + let vardeclarator = helper.closests(node, syntax.VariableDeclarator); + if (vardeclarator) { + if (vardeclarator.id.type === syntax.Identifier) { + let varid = vardeclarator.id.name; + let oprop = null; + if (vardeclarator.init.type === syntax.ObjectExpression) { + oprop = helper.closests(node, syntax.Property); + if (oprop) { + if (oprop.key && oprop.key.type === syntax.Identifier) varid = oprop.key.name; + else if (oprop.key && oprop.key.type === syntax.Literal) varid = oprop.key.value; + } + } + let variable = new VariableBuilder(varid, importPath, true); + let propertyName = helper.getPropertyName(parent); // detects require('something').property + if (propertyName) variable.members.push(propertyName); + trackids.push(variable.name); + variables.push(variable); + if (vardeclarator.xUsed || oprop) variable.members.push('.'); + } else if (vardeclarator.id.type === syntax.ObjectPattern) { + let objectPattern = vardeclarator.id; + for (let p of objectPattern.properties) { + if (p.key.type === syntax.Identifier) { + let variable = new VariableBuilder(p.key.name, importPath, true); + console.log(variable); + variable.members.push(p.key.name); + let propertyName = helper.getPropertyName(parent); // detects require('something').property + if (propertyName) variable.members.push(propertyName); + trackids.push(variable.name); + variables.push(variable); + } + } + } + } + + if (parent && parent.type === syntax.CallExpression && parent.callee !== node) { + // function ( require() ) + let variable = new VariableBuilder(importPath, importPath, true); + variable.isDynamic = true; + variable.members.push('.'); + variables.push(variable); + } else if (parent && parent.type === syntax.ExpressionStatement) { + // require() + let variable = new VariableBuilder(importPath, importPath, true); + variable.isDynamic = true; + variable.members.push('.'); + variables.push(variable); + } else if (parent && parent.type === syntax.CallExpression && parent.callee === node) { + // require("foo")() + let variable = new VariableBuilder(importPath, importPath, true); + variable.members.push('.'); + variables.push(variable); + } else if (parent && parent.type === syntax.MemberExpression && parent.object === node) { + // require("foo").something().hello + let meta = helper.getMemberExpressionMeta(parent); + if (meta) { + let variable = new VariableBuilder(importPath, importPath, true); + if (meta.computed) variable.isDynamic = true; + variable.members.push(meta.property); + variables.push(variable); + } + } + } + else if (node.arguments.length > 0) { + // Detecting if variable was passed as argument to a function + // If so, it will mark it as dynamically used variable + for (let argument of node.arguments) { + if (argument.type === syntax.Identifier) { + let variable = getLatestVariable(variables, argument.name); + if (variable && variable.isModule) { + variable.isDynamic = true; + } else if (argument.name === 'exports') { + modul.skipReduce = true; + modul.isDynamicallyUsed = true; + } + } + } + } + break; + case syntax.FunctionExpression: + if (parent.type === syntax.CallExpression) { + let calleeName = null; + if (parent.callee.type === syntax.Identifier) { + calleeName = parent.callee.name; + } else if (parent.callee.type === syntax.MemberExpression) { + calleeName = helper.getObjectName(parent.callee); + } + if (calleeName && trackids.includes(calleeName)) { + for (let arg of node.params) { + if (arg.type === syntax.Identifier) { + callbacks.push({name: arg.name, value: calleeName, scope: currentScope}); + } + } + } + } + break; + } + }, + + leave: function (node, parent) { + if (helper.createsNewScope(node)) { + currentScope -= 1; + } + switch (node.type) { + case syntax.FunctionExpression: + callbacks = callbacks.filter((item, index) => { + return item.scope !== currentScope + 1; + }); + break; + case syntax.AssignmentExpression: + if (node.left.type === syntax.Identifier) { + assignments[currentScope][assignments[currentScope].length - 1].push(node.left.name); + let left = new VariableBuilder(node.left.name); + if (node.right.type === syntax.Identifier) { + if (trackids.includes(node.right.name)) { + trackids.push(node.left.name); + let right = getLatestVariable(variables, node.right.name); + if (right) { + left.value = right.value; + left.isModule = true; + } + } + } else if (node.right.type === syntax.CallExpression) { + let cname = node.right.callee.name; + if (trackids.includes(cname)) { + trackids.push(node.left.name); + let right = getLatestVariable(variables, cname, true); + if (right) { + left.value = right.value; + left.isModule = true; + } + } + } + variables.push(left); + } else if (node.left.type === syntax.MemberExpression) { + // let memexp = helper.getMemberExpressionString(node.left); + // if(memexp.object === "module") { + // } else if (memexp.object === "exports") { + // modul.members.push(memexp.property); + // } + } + break; + } + } + }); +} +/** + * @param {Array} children + * @param {Boolean} donNotReduce + * @constructor + */ +function AnalyzeResult (children = new Object(), doNotReduce = false) { + this.children = children; + this.doNotReduce = doNotReduce; +} + +function VariableDeclarator (node) { + let variable = null; + if (node.id.type === syntax.Identifier) { + variable = new VariableBuilder(node.id.name); + if (node.init) { + if (node.init.type === syntax.CallExpression) { + let callee = node.init.callee; + if (callee.type === syntax.Identifier) { + if (trackids.includes(callee.name)) { + trackids.push(node.id.name); + let item = getLatestVariable(variables, callee.name, true); + if (item) { + variable.value = item.value; + variable.isModule = true; + } + } + } + } else if (node.init.type === syntax.Identifier) { + if (trackids.includes(node.init.name)) { + trackids.push(node.id.name); + let item = getLatestVariable(variables, node.init.name, true); + if (item) { + variable.value = item.value; + variable.isModule = true; + } + } + } else if (node.init.type === syntax.MemberExpression) { + // todo: VariableDeclare with MemberExpression init + let memexp = helper.getMemberExpressionString(node.init); + if (memexp) { + if (trackids.includes(memexp.object)) { + trackids.push(node.id.name); + let item = getLatestVariable(variables, memexp.object, true); + if (item) { + variable.value = item.value; + variable.isModule = true; + } + } + } + } + } + } + if (variable) variables.push(variable); +} + +/** + * =================== + * UTILITIES + * =================== + */ + +/** + * + * @param {Array} arr + * @param {String} name + * @returns {VariableBuilder} + */ +function getLatestVariable (arr, name, isModule = true) { + if (arr.length) { + let element = null; + if (isModule) { + element = arr.filter((item, index) => { + return item.name === name && item.scope <= currentScope && item.isModule; + }).sort(sortByScope).pop(); + } else if (isModule === false) { + element = arr.filter((item, index) => { + return item.name === name && item.scope <= currentScope && item.isModule === false; + }).sort(sortByScope).pop(); + } else { + element = arr.filter((item, index) => { + return item.name === name && item.scope <= currentScope; + }).sort(sortByScope).pop(); + } + + return element; + } + return null; +} + +function sortByScope (a, b) { + return a.scope > b.scope ? 1 : a.scope < b.scope ? -1 : 0; +} + +function VariableBuilder (name, value = null, isModule = false) { + this.name = name; + this.value = value; + this.isModule = isModule; + this.scope = currentScope; + this.members = []; +} +/** + * @returns {String} + * @param {String} property + */ +function getPropertyStart(property){ + property = property.toString(); + if (!property) return null; + if (!property.includes('.')) return property; + return property.split('.', 1)[0]; +} + +async function checkForLeaks() { + for(let i in vars) { + for (let j in vars[i]) { + for (let v of vars[i][j]) { + for (let k = i; k < assignments.length; k++) { + if (k === i) { + for (let n in assignments[k][j]) { + if (assignments[k][j][n] === v) assignments[k][j].splice(n,1); + } + } else { + for (let m in assignments[k]) { + for (let n in assignments[k][m]) { + if (assignments[k][m][n] === v) assignments[k][m].splice(n,1); + } + } + } + } + } + } + } + for (let i in assignments) { + for (let j in assignments[i]) { + for (let k of assignments[i][j]) { + leaks.push(k); + } + } + } +} + +/** + * Checks if the name is declared in the currentScope + * @param {String} name + */ +function isDeclared(name) { + for(let i = 0; i < currentScope; i++) { + for (let j in vars[i]) { + if (vars[i][j].includes(name)) return true; + } + } + let last = vars[currentScope].length-1; + if (vars[currentScope][last].includes(name)) return true; + return false; +} \ No newline at end of file diff --git a/src/lib/Configurator.js b/src/lib/Configurator.js new file mode 100644 index 0000000..45aa4d2 --- /dev/null +++ b/src/lib/Configurator.js @@ -0,0 +1,28 @@ +/** + * @author Igibek Koishybayev + * @abstract Responsible for parsing the given arguments and generating the configuration object + */ +const settings = require('./.settings.json'); +const argv = require('yargs').argv; + +settings.dryRun = argv.dryRun; +settings.skipReduction = argv.skipReduction; +settings.skipStat = argv.skipStat; +settings.verbose = argv.verbose; +settings.skipRemove = argv.skipRemove; +settings.silent = argv.silent; +settings.compressLog = argv.compressLog; +settings.logOutput = argv.logOutput ? argv.logOutput : settings.logOutput; + +if (argv.seeds) { + let seeds = argv.seeds.split(','); + console.log('> seeds:', seeds); + Array.prototype.push.apply(settings.seeds, seeds); + settings.seeds = [...new Set(settings.seeds)]; +} +if (argv.destination) settings.destination = argv.destination; +if (argv.defaultAttackSurface) settings.default_attack_surface = argv.defaultAttackSurface; +if (argv.mode) { + settings.mode = argv.mode; +} +module.exports = settings; diff --git a/src/lib/Detector.js b/src/lib/Detector.js new file mode 100644 index 0000000..995726e --- /dev/null +++ b/src/lib/Detector.js @@ -0,0 +1,114 @@ +/** + * @author Igibek Koishybayev + * @abstract Detects all used modules by traversing the require value. If require's argument is not literal, + * marks the application as usedDynamicImport + */ +const path = require('path'); +const resolveFrom = require('resolve-from'); +const esprima = require('esprima'); +const syntax = esprima.Syntax; +const utils = require('./utils'); +const flatten = require('./utils/flatten-object-keys'); +const estraverse = require('estraverse'); +const AppBuilder = require('./models/AppBuilder'); +const ModuleBuilder = require('./models/ModuleBuilder'); + + +/** @type {AppBuilder} */ +let _app = null; +let visited = []; + +/** + * @param {ModuleBuilder} modul + */ +async function run(modul) { + + if (modul.parseError === true) { + console.error('parserror', modul.path); + return; + } + let rsv = resolveFrom.silent.bind(null, path.dirname(modul.path)); + + estraverse.traverse(modul.ast, { + enter: function(node, parent) { + switch(node.type) { + case syntax.CallExpression: + if (node.callee.name === 'require' || modul.requires.includes(node.callee.name)) { + if (node.arguments.length >= 1) { + let arg = node.arguments[0]; + if (arg.type === syntax.Literal) { + let uri = rsv(arg.value); + if (uri && !utils.hasKey(modul.children, uri)) { + modul.children[uri] = {ref: null, used: []}; + if (utils.hasKey(node, 'xModule')) { + node['xModule'].push(uri); + } else { + node['xModule'] = [uri]; + } + } + } else if (arg.type === syntax.Identifier) { + _app.usedDynamicImport = true; + // identifiers are calculated during stat phase + if (modul.identifiers.hasIdentifier(arg.name) && !modul.identifiers.isComplex(arg.name)) { + let values = modul.identifiers.getValues(arg.name); + for (let v of values) { + let uri = rsv(v); + if (uri && !utils.hasKey(modul.children, uri)) { + modul.children[uri] = {ref: null, used: []}; + if (utils.hasKey(node, 'xModule')) { + node['xModule'].push(uri); + } else { + node['xModule'] = [uri]; + } + } + } + } else { + _app.usedComplicatedDynamicImport = true; + } + } else { + _app.usedDynamicImport = true; + _app.usedComplicatedDynamicImport = true; + } + } + } + break; + } + } + }); +} + +/** + * + * @param {ModuleBuilder} modul + */ +async function readModule(modul) { + if (!visited.includes(modul.path)) { + modul.isUsed = true; + + await run(modul); + visited.push(modul.path); + for (const key in modul.children) { + if (!modul.children[key].ref) { + let child = _app.modules.find(m => m.path === key); + if (child) { + modul.children[key].ref = child; + await readModule(modul.children[key].ref); + } + } + } + } +} + +/** + * + * @param {AppBuilder} app + */ +async function main (app) { + _app = app; + let entryPath = path.join(app.path, app.main); + let entry = app.modules.find(m => m.path === entryPath); + if (!entry) throw new Error('Detector.js: NO_ENTRY_POINT'); + await readModule(entry); +} + +module.exports = main; \ No newline at end of file diff --git a/src/lib/Generator.js b/src/lib/Generator.js new file mode 100644 index 0000000..a9f4a34 --- /dev/null +++ b/src/lib/Generator.js @@ -0,0 +1,132 @@ +/** + * @author Igibek Koishybayev + * @abstract Generates a code from given AST + */ +const helper = require('./utils/helpers'); +const estraverse = require('estraverse'); +const escodegen = require('escodegen'); +const es = require('esprima'); +const fse = require('fs-extra'); +const chalk = require('chalk'); +const ModuleBuilder = require('./models/ModuleBuilder'); +const sloc = require('sloc'); + +module.exports = function (source, destination) { + try { + console.log(chalk.bold('> COPYING:'), source, '->', destination); + fse.copySync(source, destination); + } catch (err) { + console.log(chalk.red(err)); + } +}; + +/** + * + * @param {ModuleBuilder} modul + */ +module.exports.generate = async function (modul, dryRun = false) { + let currentScope = 0; + let removedExports = 0, removedFunctions = 0, removedVariables = 0; + try { + estraverse.replace(modul.ast, { + enter: function (node, parent) { + if (helper.createsNewScope(node)) { + currentScope += 1; + } + switch (node.type) { + case es.Syntax.FunctionDeclaration: + if (node.xUsed === false) { + removedFunctions += 1; + this.remove(); + } + break; + case es.Syntax.VariableDeclarator: + if (parent && parent.xParent && (parent.xParent.type === es.Syntax.ForInStatement || parent.xParent.type === es.Syntax.ForOfStatement)){ + this.skip(); + } else { + if (node.xUsed === false) { + // modul.removedVariables += 1; + // this.remove(); + } + } + break; + case es.Syntax.Property: + if (node.xUsed === false) { + // modul.removedVariables += 1; + // this.remove(); + } + break; + } + }, + leave: function (node, parent) { + if (helper.createsNewScope(node)) { + currentScope -= 1; + // todo remove scoped + } + switch (node.type) { + case es.Syntax.ObjectPattern: + if (node.properties.length < 1) { + // this.remove(); + } + break; + case es.Syntax.VariableDeclarator: + if (!node.id) { + // this.remove(); + } + break; + case es.Syntax.VariableDeclaration: + if (node.declarations.length < 1) { + this.remove(); + } + break; + case es.Syntax.ExpressionStatement: + if (!node.expression) { + this.remove(); + } else if (node.expression.xUsed === false) { + removedExports += 1; + this.remove(); + } + // else if (node.expression.xUsed === true) { + // let assignment = helper.closests(node, es.Syntax.AssignmentExpression); + // if (assignment && assignment.xUsed === false) { + // assignment.hasUsed = true; + // console.log('>>> HAS USED', modul.name, assignment.left); + // } + // } + break; + case es.Syntax.AssignmentExpression: + if (node.right.type === es.Syntax.AssignmentExpression) { + if (node.xUsed === false) { + removedExports += 1; + return node.right; + } else { + if (node.right.xUsed === false) { + node.right = node.right.right; + removedExports += 1; + return node; + } + } + } + break; + } + } + }); + + // need to generate potential final source code to calculate reduced LOC + modul.ast = escodegen.attachComments(modul.ast, modul.ast.comments, modul.ast.tokens); + let gen = escodegen.generate(modul.ast, {comment: true}); + modul.finalSloc = sloc(gen, 'js').source; + modul.removedExports = removedExports; + modul.removedFunctions = removedFunctions; + modul.removedVariables = removedVariables; + + if(!dryRun) { + if (modul.hashbang) { + gen = modul.hashbang + '\n' + gen; + } + await fse.writeFile(modul.path, gen); + } + } catch (error) { + console.error(chalk.bgRed.bold('ERROR:'), 'generation', modul.path, error); + } +}; diff --git a/src/lib/Reducer.js b/src/lib/Reducer.js new file mode 100644 index 0000000..14e654c --- /dev/null +++ b/src/lib/Reducer.js @@ -0,0 +1,203 @@ +/** + * @author Igibek Koishybayev + * @abstract Marks AST nodes as used/unused depending on: + * - if exports is used/unused + * - variable is used/unused + * - function is used/unsed + */ +const estraverse = require('estraverse'); +const es = require('esprima'); +const chalk = require('chalk'); +const path = require('path'); +const helper = require('./utils/helpers'); +const config = require('./Configurator'); +const ModuleBuilder = require('./models/ModuleBuilder'); +const Scope = require('./Scope'); + +var variables = []; +var functions = []; +var members = {}; +var currentScope = 0; +let idstr = []; +let scope = new Scope(); +async function init (dirname) { + variables = []; // {scope:1, id:"", node:{}} + functions = []; // {scope:1, id:"", node:{}} + idstr = []; // {scope:1, id:""} + members = {}; + currentScope = 0; + chain = null; +} + +/** + * + * @param {ModuleBuilder} modul module that needs to be reduced + * @param {Array} expr exports that were used by dependents of the module + * @returns {String} if the module is chained returns path of the chain, else returns NULL + */ +module.exports.reduce = async function (modul, extra = []) { + await init(path.dirname(modul.path)); + await traverse(modul, extra); +}; +/** + * + * @param {ModuleBuilder} modul + * @param {Array} extra + */ +async function traverse (modul, extra) { + estraverse.traverse(modul.ast, { + enter: function (node, parent) { + if (helper.createsNewScope(node)) { + currentScope += 1; + scope.create(); + } + + if (node.type === es.Syntax.FunctionDeclaration && node.xUsed === false) { + this.skip(); + } + + node['xParent'] = parent; + + let idIndex = -1; + + switch (node.type) { + case es.Syntax.Identifier: + // todo: problem resides right here! + if (parent + && (parent.type !== es.Syntax.VariableDeclarator || (parent.init && parent.init.type === es.Syntax.Identifier && parent.init.name === node.name)) + && parent.type !== es.Syntax.FunctionDeclaration + && parent.type !== es.Syntax.ObjectPattern) { + idstr.push({scope: currentScope, id: node.name}); + // todo: more sophisticated detection of used scoped variable + let varElement = variables.filter(e => e.scope <= currentScope && e.id === node.name).pop(); + if (varElement) { + varElement.node['xUsed'] = true; + } + + let funcElement = functions.filter(e => e.scope <= currentScope && e.id === node.name).pop(); + if (funcElement) { + funcElement.node['xUsed'] = true; + } + } + break; + case es.Syntax.VariableDeclarator: + if (node.id.type === es.Syntax.Identifier) { + node['xScope'] = currentScope; + // todo: change to Array.some + idIndex = idstr.findIndex(e => (e.scope >= currentScope && e.id === node.id.name)); + if (idIndex > -1) { + node['xUsed'] = true; + } else { + node['xUsed'] = false; + } + let variable = { + scope: currentScope, + id: node.id.name, + node: node + }; + variables.push(variable); + scope.push(node.id.name); + } else if (node.id.type === es.Syntax.ObjectPattern) { + for (let prop of node.id.properties) { + prop['xScope'] = currentScope; + idIndex = idstr.findIndex(e => (e.scope >= currentScope && e.id === prop.key.name)); + if (idIndex > -1) { + prop['xUsed'] = true; + } else { + prop['xUsed'] = false; + } + let variable = { + scope: currentScope, + id: prop.key.name, + node: prop + }; + variables.push(variable); + } + } + break; + case es.Syntax.FunctionDeclaration: + node['xScope'] = currentScope - 1; + if (!node.xUsed) { + idIndex = idstr.findIndex(e => (e.id === node.id.name && e.scope >= currentScope - 1)); + if (idIndex > -1) { + node['xUsed'] = true; + } else { + node['xUsed'] = false; + } + } + + let declaration = { + id: node.id.name, + scope: currentScope - 1, + node: node + }; + + functions.push(declaration); + break; + case es.Syntax.CallExpression: + let funcElement = functions.filter(e => (e.id === node.callee.name && e.scope <= currentScope)).pop(); + if (funcElement) { + funcElement.node['xUsed'] = true; + } + break; + case es.Syntax.MemberExpression: + if (parent && parent.type === es.Syntax.AssignmentExpression && parent.left === node) { + let meta = helper.getMemberExpressionMeta(node); + if (meta && !meta.computed && (meta.exported || modul.exporters.includes(meta.object))) { + let full = meta.object + '.' + meta.property; + let prop = meta.exported ? helper.getExportedProperty(full) : meta.property; + if (prop) { + if (modul.used.includes(prop) || modul.selfUsed.includes(prop) || extra.includes(prop) || full === 'module.exports') { + if(config.verbose) console.log('-- used:', chalk.blue(full)); + parent['xUsed'] = true; + if (full === 'module.exports' && parent.right && parent.right.type === es.Syntax.ObjectExpression) { + for (let property of parent.right.properties) { + let key = property.key; + let keyName = key.type === es.Syntax.Identifier ? key.name : key.type === es.Syntax.Literal ? key.value : ''; + if (modul.used.includes(keyName) || modul.selfUsed.includes(keyName) || extra.includes(keyName)) { + property.xUsed = true; + } else { + property.xUsed = false; + } + } + } + } else { + if(config.verbose) console.log('-- not used:', chalk.yellow(full)); + parent['xUsed'] = false; + // todo: mark parent.right hand side as unused if it is functionexpress + // if (parent.right.type === es.Syntax.FunctionExpression) { + // // parent.right.xUsed = false; + // } + } + } + } + } + else if (parent && parent.type === es.Syntax.CallExpression && node.object.name === 'Object' && node.property.name === 'defineProperty') { + let arg1 = parent.arguments[0]; + let arg2 = parent.arguments[1]; + if (arg2.type === es.Syntax.Literal) { + if ((arg1.type === es.Syntax.Identifier && arg1.name === 'exports') + || (arg1.type === es.Syntax.MemberExpression && arg1.object.name === 'module' && arg1.property.name === 'exports')) + { + let prop = arg2.value; + if (modul.used.includes(prop) || modul.selfUsed.includes(prop) || extra.includes(prop)) { + parent.xUsed = true; + } else { + parent.xUsed = false; + } + } + } + + } + break; + } + }, + leave: function (node, parent) { + if (helper.createsNewScope(node)) { + // todo: remove idstr elements that is out of scope + currentScope -= 1; + scope.exit(); + } + } + }); +} \ No newline at end of file diff --git a/src/lib/Scope.js b/src/lib/Scope.js new file mode 100644 index 0000000..f34472f --- /dev/null +++ b/src/lib/Scope.js @@ -0,0 +1,33 @@ +const syntax = require('esprima').Syntax; +class Scope { + constructor() { + this.vars = []; + this.current = -1; + } + + create() { + this.current++; + if (this.vars[this.current]) { + this.vars[this.current].push([]); + } else { + this.vars.push([[]]); + } + } + + exit() { + this.current--; + } + + push(name) { + this.vars[this.current][this.vars[this.current].length - 1].push(name); + } + + isNew(node) { + if (!node) throw new Error('SCOPE_NODE_IS_NULL'); + return node.type === syntax.Program || + node.type === syntax.FunctionDeclaration || + node.type === syntax.FunctionExpression; + } +} + +module.exports = Scope; \ No newline at end of file diff --git a/src/lib/Stat.js b/src/lib/Stat.js new file mode 100644 index 0000000..afebe93 --- /dev/null +++ b/src/lib/Stat.js @@ -0,0 +1,321 @@ +const estraverse = require('estraverse'); +const syntax = require('esprima').Syntax; +const helper = require('./utils/helpers'); +const utils = require('./utils'); +const Scope = require('./Scope'); +const attack = require('./attack.json'); +const ModuleBuilder = require('./models/ModuleBuilder'); + +// FIXME: maximum call stack exceeded +/** + * @param {ModuleBuilder} modul + */ +async function stat(modul) { + await initialPass(modul); + await secondPass(modul); +} + +/** + * + * @param {ModuleBuilder} modul + */ +async function secondPass(modul) { + let scope = new Scope(); + estraverse.traverse(modul.ast, { + enter: function(node, parent){ + if (scope.isNew(node)) scope.create(); + if (node.type === syntax.VariableDeclarator) { + if (node.init && node.id.type === syntax.Identifier) { + let id = node.id.name; + if (modul.identifiers.hasIdentifier(id) && modul.identifiers.isModule(id)) { + if (node.init.type === syntax.Literal) { + modul.identifiers.addValue(id, node.init.value); + } else if(node.init.type === syntax.Identifier) { + modul.identifiers.addLink(id, node.init.name); + } else if (node.init.type === syntax.BinaryExpression) { + if (utils.BinaryExpression.isDynamic(node.init)) { + let values = utils.BinaryExpression.getValues(node.init, modul.identifiers); + if (!values) modul.identifiers.setComplex(id, true); + else { modul.identifiers.addValue(id, values); } + } else { + let binaryExpressionValue = utils.BinaryExpression.getValue(node.init); + modul.identifiers.addValue(id, binaryExpressionValue); + } + } else { + modul.identifiers.setComplex(id, true); + } + } + + } + } else if (node.type === syntax.AssignmentExpression) { + if (node.left.type === syntax.Identifier) { + let id = node.left.name; + if (modul.identifiers.hasIdentifier(id) && modul.identifiers.isModule(id)) { + if (node.right.type === syntax.Literal) { + modul.identifiers.addValue(id, node.right.value); + } else if (node.right.type === syntax.Identifier) { + modul.identifiers.addLink(id, node.right.name); + } else if (node.right.type === syntax.BinaryExpression) { + if (utils.BinaryExpression.isDynamic(node.right)) { + let values = utils.BinaryExpression.getValues(node.right, modul.identifiers); + if (!values) modul.identifiers.setComplex(id, true); + else { modul.identifiers.addValue(id, values);} + } else { + let binaryExpressionValue = utils.BinaryExpression.getValue(node.right); + modul.identifiers.addValue(id, binaryExpressionValue); + } + } else { + modul.identifiers.setComplex(id, true); + } + } + } + + } + }, + leave: function(node, parent) { + if (scope.isNew(node)) scope.exit(); + } + }) +} + +/** + * initialPass calculates basic statistics and initializes modul's fields + * @param {ModuleBuilder} modul + */ +async function initialPass(modul) { + let scope = new Scope(); + let tracker = []; + let modules = []; + let self = []; + estraverse.traverse(modul.ast, { + enter: function (node, parent) { + node['xParent'] = parent; + if(scope.isNew(node)) { + scope.create(); + } + switch (node.type) { + case syntax.CallExpression: + let callee = node.callee.name; + if (callee === 'eval') { + modul.eval += 1; + if (node.arguments.length > 0) { + let arg = node.arguments[0]; + if (arg.type !== syntax.Literal) { + modul.evalWithVariable += 1; + } + } + } else if (callee === 'Function') { + modul.functionNew += 1; + modul.functions += 1; + } else if (callee === 'require' && node.arguments.length > 0) { + let arg = node.arguments[0]; + if (arg.type !== syntax.Literal) { + modul.dynamicRequire += 1; + // TODO: detect what is value + if (arg.type === syntax.BinaryExpression) { + let isDynamicBinaryExpression = utils.BinaryExpression.isDynamic(arg); + if (isDynamicBinaryExpression) modul.complexDynamicRequire += 1; + } else if (arg.type === syntax.Identifier && (!modul.identifiers.hasIdentifier(arg.name) + || modul.identifiers.hasIdentifier(arg.name) && modul.identifiers.isComplex(arg.name))) { + modul.complexDynamicRequire += 1; + } + } else { + modul.staticRequire += 1; + if (utils.hasKey(attack, arg.value)) { + helper.VariableAssignmentName(parent, (name) => { + if (name) { + if (Array.isArray(name)) { + console.log(name); + } + tracker.push(name); + let vector = {name: name, value: arg.value, members: []}; + modul.attackVectors.push(vector); + if (parent.type === syntax.MemberExpression) { + vector.members.push(parent.property.name); + } + } + }); + } + let vardeclarator = helper.closests(node, syntax.VariableDeclarator); + if (vardeclarator) { + modules.push(vardeclarator.id.name); + } + let assignment = helper.closests(node, syntax.AssignmentExpression); + if (assignment && assignment.left.type === syntax.Identifier) { + modules.push(assignment.left.name); + } + } + } else if (callee === 'match' && node.arguments.length === 1) { + modul.stringMatch += 1; + } else if (callee === 'replace' && node.arguments.length === 2) { + modul.stringReplace += 1; + } else if (callee === 'search' && node.arguments.length === 1) { + modul.stringSearch += 1; + } else if (modul.requires.includes(callee) && node.arguments.length === 1) { + modul.dynamicRequire += 1; + } + break; + case syntax.MemberExpression: + if (node.object.type === syntax.Identifier) { + if (node.object.name === 'Object' && node.property.name === 'defineProperty') { + if (parent.type === syntax.CallExpression) { + let firstArg = parent.arguments[0]; + let secondArg = parent.arguments[1]; + if (firstArg && secondArg) { + if (firstArg.type === syntax.Identifier && firstArg.name === 'exports') { + if (secondArg.type !== syntax.Literal) modul.dynamicExport += 1; + else modul.staticExport += 1; + } else if (firstArg.type === syntax.MemberExpression && helper.getObjectName(firstArg) === 'module') { + if (secondArg.type !== syntax.Literal) modul.dynamicExport += 1; + else modul.staticExport += 1; + } + } + } + } else if (node.object.name === 'JSON' && node.property.name === 'parse') { + if (parent.type === syntax.CallExpression) { + modul.jsonParse += 1; + } + } else if (node.object.name === 'exports' || (node.object.name === 'module' && node.property.name === 'exports')) { + if (helper.isComputed(node)){ + modul.dynamicExport += 1; + } else { + modul.staticExport += 1; + } + let assignment = helper.closests(node, syntax.AssignmentExpression); + if (assignment && assignment.left.type === syntax.Identifier) { + self.push(assignment.right.name); + } + let vardeclarator = helper.closests(node, syntax.VariableDeclarator); + if (vardeclarator) { + if (vardeclarator.id.type === syntax.Identifier) { + self.push(vardeclarator.id.name); + } else if (vardeclarator.id.type === syntax.ObjectPattern) { + // TODO: what is going to be here? + } + } + } + if (tracker.includes(node.object.name)) { + modul.attackVectors.forEach(item => { + if (item.name === node.object.name) { + item.members.push(node.property.name); + } + }); + } + if (modules.includes(node.object.name)) { + let assignment = helper.closests(node, syntax.AssignmentExpression); + if (assignment && assignment.left.type === syntax.MemberExpression) { + if (helper.getObjectName(assignment.left) === node.object.name) { + modul.monkeyPatching += 1; + } + } + if (helper.isComputed(node)) { + modul.dynamicUsage += 1; + } + } + if (self.includes(node.object.name)) { + if (helper.isComputed(node)) { + modul.dynamicUsage += 1; + } + } + } + break; + case syntax.AssignmentExpression: + if (node.left.type === syntax.Identifier) { + if (node.right.type === syntax.Identifier) { + switch (node.right.name) { + case 'eval': + modul.obfuscated += 1; + break; + case 'require': + modul.requires.push(node.left.name); + break; + case 'exports': + modul.exporters.push(node.left.name); + break; + } + } else if (node.right.type === syntax.MemberExpression) { + let meta = helper.getMemberExpressionMeta(node.right); + if (meta && meta.object === 'module' && meta.property === 'exports') { + modul.exporters.push(node.left.name); + } + } + // TODO: calculate the possible values of identifier + // storing the modul.identifiers for simple dynamic resolution. + let id = node.left.name; + + modul.identifiers.addIdentifier(id); + if (node.right.type === syntax.Identifier) { + modul.identifiers.addLink(id, node.right.name); + } else if (node.right.type === syntax.BinaryExpression && utils.BinaryExpression.isDynamic(node.right)) { + let idens = utils.BinaryExpression.getIdentifiers(node.right); + if (idens) { + // console.log(idens, 'idens'); + for (const i of idens) { + modul.identifiers.addDependency(id, i); + } + } + } + // console.log(id, 'added in initial pass'); + } + break; + case syntax.VariableDeclarator: + if (node.init && node.id.type === syntax.Identifier) { + modul.variables += 1; + if (node.init.type === syntax.Identifier) { + if (node.init.name === 'require') { + modul.requires.push(node.id.name); + } else if (node.init.name === 'exports') { + modul.exporters.push(node.id.name); + } + } else if (node.init.type === syntax.MemberExpression) { + let meta = helper.getMemberExpressionMeta(node.init); + if (meta && meta.exported && meta.object === 'module' && meta.property === 'exports') { + modul.exporters.push(node.id.name); + } + } + // TODO: calculate the possible values of identifier + // storing the modul.identifiers for simple dynamic resolution. + let id = node.id.name; + modul.identifiers.addIdentifier(id); + if(node.init.type === syntax.Identifier) { + modul.identifiers.addLink(id, node.init.name); + } else if (node.init.type === syntax.BinaryExpression && utils.BinaryExpression.isDynamic(node.init)) { + let idens = utils.BinaryExpression.getIdentifiers(node.init); + if (idens) { + // console.log(idens, 'idens'); + for (const i of idens) { + modul.identifiers.addDependency(id, i); + } + } + } + } + break; + case syntax.Literal: + if (node.regex) { + modul.regex += 1; + if (helper.isDangerousRegex(node.name)) { + modul.regexDos += 1; + } + } + break; + case syntax.FunctionDeclaration: + modul.functions += 1; + break; + } + }, + leave: function (node, parent) { + if(scope.isNew(node)) scope.exit(); + if (node.type === syntax.CallExpression) { + if ((node.callee.name === 'require' || modul.requires.includes(node.callee.name)) && node.arguments.length > 0) { + let first = node.arguments[0]; + if (first.type === syntax.Identifier && modul.identifiers.hasIdentifier(first.name)) { + // marking the identifier as a module + modul.identifiers.setIsModule(first.name, true); + } + } + } + } + }); +} + +module.exports = stat; \ No newline at end of file diff --git a/src/lib/Traverser.js b/src/lib/Traverser.js new file mode 100644 index 0000000..9aaeeab --- /dev/null +++ b/src/lib/Traverser.js @@ -0,0 +1,96 @@ +const fs = require('fs'); +const path = require('path'); +const esprima = require('esprima'); +const escodegen = require('escodegen'); + +const AppBuilder = require('./models/AppBuilder'); +const ModuleBuilder = require('./models/ModuleBuilder'); +const config = require('./Configurator'); + +/** @type {AppBuilder} */ +let _app = null; +let location = null; + +/** + * @returns {Boolean} + * @param {String} directory + */ +async function traverse(directory) { + try { + var folderContent = fs.readdirSync(directory); + for (let item of folderContent) { + item = path.join(directory, item); + let stat = fs.statSync(item); + if (stat.isDirectory()) { + await traverse(item); + } else if (stat.isFile() && item.endsWith('.js')) { + let _module = new ModuleBuilder(); + _module.app = _app; + _module.name = path.basename(item); + _module.path = item; + + if (directory.indexOf('/node_modules', location.length - 1) === -1) { + _module.isOwned = true; + // should we add do not reduce here? + } else { + let arr = _module.path.split('/'); + let lIndex = arr.lastIndexOf('node_modules'); + _module.packagename = arr[lIndex + 1]; + } + + _module.initialSrc = fs.readFileSync(`${_module.path}`, 'utf8'); + // _module.initialSloc = sloc(_module.initialSrc, 'js').source; + await parse(_module); + _module.initialSrc = null; + _app.modules.push(_module); + } else if (stat.isFile() && item === 'package.json') { + let content = fs.readFileSync(`${item}`, utf); + let packageJson = JSON.parse(content); + for (let index in packageJson.dependencies) { + let obj = {}; + obj.parent = packageJson.name; + obj.name = index; + obj.version = packageJson.dependencies[index]; + _app.dependencies.push(obj); + } + } + } + } catch (err) { + throw new Error(err); + } + return true; +} +/** + * + * @param {ModuleBuilder} modul + */ +async function parse(modul) { + try { + if (modul.initialSrc.startsWith('#!')) { + modul.hashbang = modul.initialSrc.split('\n', 1)[0]; // saves the hashbang value + modul.initialSrc = modul.initialSrc.replace(/^#!(.*\n)/, ''); // removes hashbang value + } + let ast = es.parseScript(modul.initialSrc, {range: true, tokens: true, comment: true}); + modul.ast = ast; + + //calculating the SLOC + let gen = escodegen.generate(modul.ast); + modul.initialSloc = sloc(gen, 'js').source; + } catch (ex) { + if(config.verbose) console.warn(chalk.bold.yellow('WARN:'), modul.path, ex.message); + modul.parseError = true; + } + return modul; +} + +/** + * @returns {AppBuilder} + * @param {String} directory + */ +async function main(app, directory) { + _app = app; + location = directory; + await traverse(directory); +} + +module.exports = main; \ No newline at end of file diff --git a/src/lib/attack.json b/src/lib/attack.json new file mode 100644 index 0000000..aa637d6 --- /dev/null +++ b/src/lib/attack.json @@ -0,0 +1,141 @@ +{ + "fs": { + "appendFile":1, + "appendFileSync": 1, + "chmod": 1, + "chmodSync": 1, + "chown": 1, + "chownSync": 1, + "copyFile": 1, + "copyFileSync": 1, + "createReadStream": 1, + "createWriteStream": 1, + "fchmod": 1, + "fchmodSync": 1, + "fchown": 1, + "fchownSync": 1, + "fdatasync": 1, + "fdatasyncSync": 1, + "fstat": 1, + "fstatSync": 1, + "fsync": 1, + "fsyncSync": 1, + "ftruncate": 1, + "ftruncateSync": 1, + "futimes": 1, + "futimesSync": 1, + "lchmod": 1, + "lchmodSync": 1, + "lchown": 1, + "lchownSync": 1, + "link": 1, + "linkSync": 1, + "lstat": 1, + "lstatSync": 1, + "mkdir": 1, + "mkdirSync": 1, + "mkdtemp": 1, + "mkdtempSync": 1, + "open": 1, + "openSync": 1, + "read": 1, + "readdir": 1, + "readdirSync": 1, + "readFile": 1, + "readFileSync": 1, + "readlink": 1, + "readlinkSync": 1, + "readSync": 1, + "rename": 1, + "renameSync": 1, + "rmdir": 1, + "rmdirSync": 1, + "stat": 1, + "statSync": 1, + "symlink": 1, + "symlinkSync": 1, + "truncate": 1, + "truncateSync": 1, + "unlink": 1, + "unlinkSync": 1, + "utimes": 1, + "utimesSync": 1, + "watch": 1, + "write": 1, + "writeFile": 1, + "writeFileSync": 1, + "writeSync": 1 + + }, + "os": { + "arch": 1, + "cpus": 1, + "endianness": 1, + "freeman": 1, + "homedir": 1, + "hostname": 1, + "loadavg": 1, + "networkInterfaces": 1, + "platform": 1, + "releases": 1, + "tmpdir": 1, + "totalmem": 1, + "type": 1, + "uptime": 1 + }, + "child_process": { + "exec":1, + "execFile":1, + "fork":1, + "spawn":1, + "subprocess.stdin":1, + "subprocess.stdout":1, + "subprocess.stdio":1 + }, + "process": { + "arch": 1, + "argv": 1, + "channel": 1, + "chdir": 1, + "config": 1, + "connected": 1, + "cpuUsage": 1, + "cwd": 1, + "dlopen": 1, + "env": 1, + "execArgv": 1, + "execPath": 1, + "getegid": 1, + "geteuid": 1, + "getgid": 1, + "getgroups": 1, + "getuid": 1, + "initgroups": 1, + "kill": 1, + "mainModule": 1, + "memoryUsage": 1, + "pid": 1, + "platform": 1, + "ppid": 1, + "release": 1, + "send": 1, + "setegid": 1, + "seteuid": 1, + "setgid": 1, + "setgroups": 1, + "setuid": 1, + "stderr": 1, + "stdin": 1, + "stdout": 1, + "umask": 1, + "uptime": 1 + }, + "readline": { + "question":1, + "prompt":1, + "write":1 + }, + "repl": { + "start":1 + } +} \ No newline at end of file diff --git a/src/lib/models/AppBuilder.js b/src/lib/models/AppBuilder.js new file mode 100644 index 0000000..6f5ede4 --- /dev/null +++ b/src/lib/models/AppBuilder.js @@ -0,0 +1,65 @@ +const ModuleBuilder = require('./ModuleBuilder'); +module.exports = function () { + this.appname = ''; + this.version = ''; + this.path = ''; + this.main = ''; + + this.attackSurface = 0; + this.externalAttackSurface = 0; + this.usedDynamicImport = false; + this.usedComplicatedDynamicImport = false; + // measurement stats + this.originalFiles = 0; + this.externalFiles = 0; + this.usedExternalFiles = 0; + + this.originalFilesLOC = 0; + this.externalFilesLOC = 0; + this.usedExternalFilesLOC = 0; + this.usedExternalReducedFilesLOC = 0; + + this.declaredDependencyCount = 0; + this.installedUniqueDependencyCount = 0; + this.installedTotalDependencyCount = 0; + + this.totalStaticExports = 0; + this.totalDynamicExports = 0; + + this.totalStaticRequire = 0; + this.totalDynamicRequire = 0; + this.totalComplexDynamicRequire = 0; + this.totalDynamicUsage = 0; + + this.totalFunctions = 0; + this.totalVariables = 0; + + this.totalEval = 0; + this.totalEvalWithVar = 0; + this.totalFunctionNew = 0; + this.totalMonkeyPatching = 0; + + // ReDOS attack related fields + this.totalRegex = 0; + this.totalRegexDos = 0; + this.totalStringReplace = 0; + this.totalStringMatch = 0; + this.totalStringSearch = 0; + this.totalJsonParse = 0; + + // reduction stats + this.totalRemovedVariables = 0; + this.totalRemovedFunctions = 0; + this.totalRemovedExports = 0; + this.totalRemovedFiles = 0; + this.totalRemovedLOC = 0; + + this.globals = []; // all global variables that Application has. Ex: {name: 'foo', path: 'lib/foo.js', members: []} + this.dimports = []; // all modules that dynamically imported. Ex: {name: 'foo', members: [], by: ''}. DIMPORTS has higher precedence than GLOBALS + /** @type {ModuleBuilder} */ + this.modules = [];// all modules that Application has, i.e. all js files inside every package. Ex: express/lib/express.js, mocha/index.js + + this.dependencies = {};// all packages Application has. Ex: express, mocha, lodash. {parent: "", name:"", version:""} + + this.builtins = {}; // all native modules that were used inside the application. +}; diff --git a/src/lib/models/Identifier.js b/src/lib/models/Identifier.js new file mode 100644 index 0000000..0b547f6 --- /dev/null +++ b/src/lib/models/Identifier.js @@ -0,0 +1,48 @@ +class Identifier { + constructor() { + this.complex = false; + this.isModule = false; + this.values = new Set(); + this.links = []; + this.dependencies = new Set(); + } + + addValue(value) { + // console.log('stuck in add value'); + let type = typeof value; + if (type === 'string') { + this.values.add(value); + } + } + + addLink(link) { + if (typeof link !== 'string') throw new Error('IDENTIFIER_LINK_MUST_BE_STRING'); + + if (!this.links.includes(link)) + this.links.push(link); + } + + addDependencies(dependency) { + if (typeof dependency !== 'string') throw new Error('IDENTIFIER_DEPENDENCY_MUST_BE_STRING'); + this.dependencies.add(dependency); + } + + setComplex(val) { + if (typeof val === 'boolean') { + this.complex = val; + } + } + + setIsModule(val) { + if (typeof val === 'boolean') { + this.isModule = val; + } + } + + changeToArray() { + this.values = [...this.values]; + this.dependencies = [...this.dependencies]; + } +} + +module.exports = Identifier; \ No newline at end of file diff --git a/src/lib/models/IdentifierTable.js b/src/lib/models/IdentifierTable.js new file mode 100644 index 0000000..30d1420 --- /dev/null +++ b/src/lib/models/IdentifierTable.js @@ -0,0 +1,131 @@ +const Identifier = require('./Identifier'); +class IdentifierTable { + constructor() { + this.table = {}; + } + + getValues(key) { + if (this.hasIdentifier(key)) { + let queue = this.getLinks(key), result = new Set(), visited = []; + let next = null; + queue.push(key); + while (queue.length > 0) { + next = queue.pop(); + visited.push(next); + if (this.hasIdentifier(next)) { + for (const value of this.table[next].values) { + result.add(value); + } + } + // console.log(queue.length, 'queue'); + } + return result; + } + return []; + } + + getLinks(key) { + if(this.hasIdentifier(key)) { + let queue = [], visited = [], result = []; + let next = null; + queue.push(key); + while(queue.length > 0) { + next = queue.pop(); + visited.push(next); + if (this.hasIdentifier(next)) { + for (const link of this.table[next].links) { + if (!visited.includes(link) && !queue.includes(link)) { + queue.push(link); + } + if (!result.includes(link)) result.push(link); + } + } + } + return result; + } + return []; + } + + getDependencies(key) { + if(this.hasIdentifier(key)) { + return this.table[key].dependencies; + } + return new Set(); + } + + + addValue(key, value) { + if (this.hasIdentifier(key)) { + if (value instanceof Set) { + for (const v of value) { + this.table[key].addValue(v); + } + } else { + this.table[key].addValue(value); + } + } + } + + addLink(key, link) { + if(this.hasIdentifier(key) && link !== key && !this.getLinks(key).includes(link)) { + this.table[key].addLink(link); + } + } + + addDependency(key, dependency) { + if(this.hasIdentifier(key) && dependency !== key) { + this.table[key].dependencies.add(dependency); + } + } + + addIdentifier(key) { + if (typeof key !== 'string') throw new Error('IDENTIFIER_KEY_MUST_BE_STRING'); + if (!this.hasIdentifier(key)) { + this.table[key] = new Identifier(); + } + } + + setComplex(key, value) { + if(this.hasIdentifier(key)) { + this.table[key].setComplex(value); + } + } + + setIsModule(key, value) { + if(this.hasIdentifier(key)) { + this.table[key].setIsModule(value); + for (const link of this.getLinks(key)) { + if (this.hasIdentifier(link)) { + this.table[link].setIsModule(value); + } + } + for (const dependency of this.getDependencies(key)) { + if (this.hasIdentifier(dependency)) { + this.table[dependency].setIsModule(value); + } + } + } + } + + hasIdentifier(key) { + return Object.prototype.hasOwnProperty.call(this.table, key); + } + + isComplex(key) { + return this.table[key].complex; + } + + isModule(key) { + return this.table[key].isModule; + } + + clean() { + for (const key in this.table) { + if (this.hasIdentifier(key) && !this.table[key].isModule) { + delete this.table[key]; + } + } + } +} + +module.exports = IdentifierTable; \ No newline at end of file diff --git a/src/lib/models/ModuleBuilder.js b/src/lib/models/ModuleBuilder.js new file mode 100644 index 0000000..dcb7667 --- /dev/null +++ b/src/lib/models/ModuleBuilder.js @@ -0,0 +1,73 @@ +const AppBuilder = require('./AppBuilder'); +const IdentifierTable = require('./IdentifierTable'); + +module.exports = function () { + this.name = ''; + this.path = ''; + this.packagename = ''; // retrieve from package.json or module path? + this.packageversion = ''; // retrieve from package.json + this.attackSurface = 0; + this.attackVectors = []; // object: {value:"fs", members:[]} + + this.initialSrc = ''; + this.initialSloc = 0; + this.ast = null; + + // reduction stats + this.finalSrc = ''; + this.finalSloc = 0; + this.removedFunctions = 0; + this.removedVariables = 0; + this.removedExports = 0; + + this.isUsed = false; + this.isDynamicallyUsed = false; + this.isOwned = false; + this.isRemoved = false; + this.skipReduce = false; + + // Module statistics + this.eval = 0; + this.evalWithVariable = 0; + this.functionNew = 0; + this.definesGlobal = 0; + + this.staticRequire = 0; + this.dynamicRequire = 0; + this.complexDynamicRequire = 0; + this.dynamicUsage = 0; + + this.staticExport = 0; + this.dynamicExport = 0; + this.dynamicExportUsage = 0; + + this.variables = 0; + this.functions = 0; + + this.regex = 0; + this.regexDos = 0; + this.stringReplace = 0; + this.stringMatch = 0; + this.stringSearch = 0; + this.jsonParse = 0; + this.monkeyPatching = 0; + /** + * METADATA + */ + /** @type {AppBuilder} */ + this.app = null; + this.parseError = false; + this.hashbang = null; + + this.used = []; // members that were used by another modules. + this.selfUsed = []; // members that were used by module itself. For ex: exports.save() + this.members = []; // exported members to the outside + this.children = {}; // object. { "path/to/child.js": { ref: module, used: [] } } + this.dynamicChildren = []; + this.ancestors = []; // stores all parents of the module + this.descendents = []; // stores all of the module + this.requires = []; // stores the name of require functions. Example: var r = require; + this.exporters = []; // stores the name of exporters. Example: var es = exports; + this.memusages = {}; // member expression usages that are not declared. For example: console.log(), foo.a() (if foo is global) + this.identifiers = new IdentifierTable() // {"identifier": { complex: true|false, values: [literals only], links: [literals only]} } +}; diff --git a/src/lib/modules.surfaces.json b/src/lib/modules.surfaces.json new file mode 100644 index 0000000..af15f4b --- /dev/null +++ b/src/lib/modules.surfaces.json @@ -0,0 +1,24 @@ +{ + "packages": [ + { + "name":"fs", + "version":"", + "entries":["appendFile", "appendFileSync", "copyFile", "copyFileSync", "chown", "chownSync", "chmod", "chmodSync", "createWriteStream", "fchown", "fchmod","fchownSync","fchmodSync", "ftruncate", "ftruncateSync", "mkdir", "mkdirSync", "lchown", "lchownSync", "rmdir", "rmdirSync", "symlink", "symlinkSync", "truncate", "truncateSync", "unlink", "unlinkSync", "write", "writeSync", "writeFile", "writeFileSync"], + "exits":["readFile", "readFileSync", "readdir", "readdirSync", "createReadStream", "read", "readSync", "readlink", "readlinkSync", "rename", "renameSync", "stat", "statSync" ] + }, + { + "name":"child_process", + "version":"", + "entries":[], + "exits":[] + } + ], + "scores":{ + "entries":3, + "exits":3, + "eval":10, + "obfus":100, + "dynamic":20, + "env":1 + } +} \ No newline at end of file diff --git a/src/lib/utils/ast-utils.js b/src/lib/utils/ast-utils.js new file mode 100644 index 0000000..4677b6a --- /dev/null +++ b/src/lib/utils/ast-utils.js @@ -0,0 +1,39 @@ +const syntax = require("esprima").Syntax; + +function closests (node, type) { + if (!node || node.type === syntax.Program) { + return null; + } + if (node.type === type) { + return node; + } + return closests(node.xParent, type); +} + +function closestsNot(node, type) { + if (node.type !== type) { + return node; + } + return closestsNot(node.xParent, type); +} + +function all(ast, type) { + +} + +/** + * Detects if AST node creates new scope or not. + * @returns {Boolean} + * @param {*} node + */ +function createsNewScope (node) { + if (!node) console.log(chalk.red(node)); + return node.type === es.Syntax.Program || + node.type === es.Syntax.FunctionDeclaration || + node.type === es.Syntax.FunctionExpression; + // || node.type === syntax.ArrowFunctionExpression +} + +module.exports.closests = closests; +module.exports.closestsNot = closestsNot; +module.exports.createsNewScope = createsNewScope; \ No newline at end of file diff --git a/src/lib/utils/binary-expression-utils.js b/src/lib/utils/binary-expression-utils.js new file mode 100644 index 0000000..7c543a1 --- /dev/null +++ b/src/lib/utils/binary-expression-utils.js @@ -0,0 +1,115 @@ +const syntax = require('esprima').Syntax; +const IdentifierTable = require('../models/IdentifierTable'); +/** + * + * @param {syntax.BinaryExpression} node + */ +function isDynamicBinaryExpression(node) { + if (node.type === syntax.BinaryExpression) { + if (node.left.type !== syntax.BinaryExpression) { + return node.left.type !== syntax.Literal || node.right.type !== syntax.Literal; + } + let copy = node; + while (copy.type === syntax.BinaryExpression) { + if (copy.right.type !== syntax.Literal) { + return true; + } else if (copy.right.type === syntax.Literal && copy.left.type === syntax.Literal) { + return false; + } + copy = copy.left; + } + } + return false; +} + +/** + * Returns the value of NOT dynamic binary expression. Will fail if the binary expression is dynamic + * @param {syntax.BinaryExpression} node + */ +function getValue(node) { + let right = node.right.value; + let left = node.left.type === syntax.Literal ? node.left.value : getValue(node.left); + return left + right; +} + +/** + * + * @param {syntax.BinaryExpression} node + * @param {IdentifierTable} identifiers + */ +function getValues(node, identifiers) { + let result = new Set(), rights = new Set(), lefts = new Set(); + if ((node.left.type !== syntax.Literal && node.left.type !== syntax.Identifier && node.left.type !== syntax.BinaryExpression) + || (node.right.type !== syntax.Literal && node.right.type !== syntax.Identifier)) { + return null; + } + + if (node.right.type === syntax.Literal) { + if (typeof node.right.value === 'string' || typeof node.right.value === 'number') + rights.add(node.right.value); + } else if (node.right.type === syntax.Identifier) { + let values = identifiers.getValues(node.right.name) + for (const val of values) { + rights.add(val); + } + } + + if (node.left.type === syntax.Literal) { + if (typeof node.left.value === 'string' || typeof node.right.value === 'number') + lefts.add(node.left.value); + } else if(node.left.type === syntax.Identifier) { + let values = identifiers.getValues(node.left.name) + for (const val of values) { + lefts.add(val); + } + } else if(node.left.type === syntax.BinaryExpression) { + + let deep = getValues(node.left, identifiers); + if (!deep) { + return null; + } + + for (const val of deep) { + lefts.add(val) + } + } + + for (const left of lefts) { + for (const right of rights) { + let val = left + right; + result.add(val); + } + } + return result; +} + +function getIdentifiers(node) { + let result = new Set(); + let rtype = node.right.type, ltype = node.left.type; + if (rtype !== syntax.Literal && rtype !== syntax.Identifier) return null; + if (ltype !== syntax.Literal && ltype !== syntax.Identifier && ltype !== syntax.BinaryExpression) return null; + + let copy = node; + + while (copy.type === syntax.BinaryExpression) { + if (copy.right.type === syntax.Identifier) { + result.add(copy.right.name); + } + if (copy.left.type === syntax.Identifier) { + result.add(copy.left.name); + } else if (copy.left.type !== syntax.Literal && copy.left.type !== syntax.BinaryExpression) { + return null; + } + copy = copy.left; + } + + return result; +} + +module.exports.isDynamic = isDynamicBinaryExpression; + +module.exports.getValue = getValue; + +module.exports.getValues = getValues; + +module.exports.getIdentifiers = getIdentifiers; \ No newline at end of file diff --git a/src/lib/utils/entry-point.js b/src/lib/utils/entry-point.js new file mode 100644 index 0000000..3c0dd25 --- /dev/null +++ b/src/lib/utils/entry-point.js @@ -0,0 +1,34 @@ +const jfs = require('jsonfile'); +const fs = require('fs'); +const path = require('path'); + +module.exports = function entryPoint(location, seed = null) { + let pckg, main; + main = seed; + if (seed === null) { + pckg = jfs.readFileSync(path.join(location,'package.json')); + main = pckg.main; + } + + if (!main) main = 'index.js'; + let url = path.join(location, main); + if (fs.existsSync(url)) { + if (fs.lstatSync(url).isDirectory()) { + main = path.join(main, 'index.js'); + url = path.join(location, main); + if (fs.existsSync(url)) { + return main; + } + } else { + return main; + } + } else if (!main.endsWith('.js')) { + main += '.js'; + url = path.join(location, main); + if (fs.existsSync(url)) { + return main; + } + } + + return null; +} \ No newline at end of file diff --git a/src/lib/utils/file-dependencies.js b/src/lib/utils/file-dependencies.js new file mode 100644 index 0000000..318e825 --- /dev/null +++ b/src/lib/utils/file-dependencies.js @@ -0,0 +1,22 @@ +const fs = require('fs'); +const resolve = require('resolve-from'); +const esprima = require('esprima'); +const syntax = esprima.Syntax; +const estraverse = require('estraverse'); + +function allRequires(base, ast) { + let resolveFrom = resolve.bind(base, null); + let result = []; + estraverse.traverse(ast, { + enter: function(node, parent) { + // todo: add renamed requires + if (node.type === syntax.CallExpression && node.callee.name === 'require') { + + } + }, + leave: function(node, parent) { + + } + }) + return result; +} \ No newline at end of file diff --git a/src/lib/utils/flatten-object-keys.js b/src/lib/utils/flatten-object-keys.js new file mode 100644 index 0000000..a75277b --- /dev/null +++ b/src/lib/utils/flatten-object-keys.js @@ -0,0 +1,25 @@ + +const isObject = (obj) => { + return typeof obj === 'object'; +} + +const isEmptyObject = (obj) => { + return !Object.keys(obj).length; +} + + +function flattenObjectKeys(obj, arr, unique = false) { + if (!obj || !isObject(obj) || !isEmptyObject(obj) || Array.isArray(obj)) return []; + if(!arr) arr = []; + Object.keys(obj).forEach((key) => { + arr.push(key); + flattenObjectKeys(obj[key], arr, true); + }); + + if (unique) { + return [...new Set(arr)]; + } + return arr; +} + +module.exports = flattenObjectKeys; \ No newline at end of file diff --git a/src/lib/utils/helpers.js b/src/lib/utils/helpers.js new file mode 100644 index 0000000..90ef898 --- /dev/null +++ b/src/lib/utils/helpers.js @@ -0,0 +1,270 @@ +const es = require('esprima'); +const saferegex = require('safe-regex'); + +/** + * @returns {String} closests variable_declarator/assignment_expression identifier + * @param {*} node + * @param {Function} cb + */ +function VariableAssignmentName (node, cb) { + // console.log(chalk.blue("Parent:"), node); + if (!node) return cb(null); + if (node.type === es.Syntax.Program) { + return cb(null); + } else if (node.type === es.Syntax.VariableDeclarator) { + if (node.id.type === es.Syntax.Identifier) { + return cb(node.id.name); + } + if (node.id.type === es.Syntax.ObjectPattern) { + // todo: detect let {a, b} = require('something') + let arr = []; + for (const prop of node.id.properties) { + if (prop.type === es.Syntax.Property) { + arr.push(prop.key.name); + } + } + return arr; + } + } else if (node.type === es.Syntax.AssignmentExpression) { + if (node.left.type === es.Syntax.Identifier) { + return cb(node.left.name); + } else if (node.left.type === es.Syntax.MemberExpression) { + // todo: detect object.property = require('something') + let mexpr = getMemberExpressionString(node.left); + if (mexpr) return mexpr.object + '.' + mexpr.property; + else return null; + } + } + if (node.xParent) VariableAssignmentName(node.xParent, cb); + else return cb(null); +} + +/** + * Detects if AST node creates new scope or not. + * @returns {Boolean} + * @param {*} node + */ +function createsNewScope (node) { + if (!node) console.log(chalk.red(node)); + return node.type === es.Syntax.Program || + node.type === es.Syntax.FunctionDeclaration || + node.type === es.Syntax.FunctionExpression; + // || node.type === syntax.ArrowFunctionExpression +} + +/** + * Finds object name of the member expression. + * @returns {String} + * @param {MemberExpression} node + */ +function getObjectName (node) { + if (node.type === es.Syntax.CallExpression) { + return getObjectName(node.callee); + } else if (node.type === es.Syntax.Identifier) { + return node.name; + } + // should I add Literal??? + // else if (node.type === es.Syntax.Literal) { + // return node.value; + // } + if (node.type !== es.Syntax.MemberExpression) { + return null; + } else if (node.object.type === es.Syntax.Identifier) { + return node.object.name; + } else if (node.object.type === es.Syntax.ThisExpression) { + return 'this'; + } + return getObjectName(node.object); +} + +/** + * Contruct property name of the member expression. + * If member expression has nested properties returned string will contain + * all properties joined by . + * @returns {String} + * @param {MemberExpression} node + */ +function getPropertyName (node) { + if (node) { + if (node.type === es.Syntax.MemberExpression) { + let propertyName = ''; + if (node.property.type === es.Syntax.Identifier) { + propertyName = node.property.name; + } else if (node.property.type === es.Syntax.Literal) { + propertyName = node.property.value; + } + let parentProperty = getPropertyName(node['xParent']); + if (parentProperty) { + return propertyName + '.' + parentProperty; + } + return propertyName; + } else if (node.type === es.Syntax.CallExpression) { + return getPropertyName(node['xParent']); + } + } + + return null; +} + +function getMemberExpressionString (node) { + if (node.type !== es.Syntax.MemberExpression) return null; + if (node.object.type === es.Syntax.Identifier || node.object.type === es.Syntax.ThisExpression) { + let objectName = getObjectName(node); + let propertyName = getPropertyName(node); + return {object: objectName, property: propertyName}; + } else { + return getMemberExpressionString(node.object); + } +} + +function closests (node, type) { + if (!node || node.type === es.Syntax.Program) { + return null; + } + if (node.type === type) { + return node; + } + return closests(node.xParent, type); +} + +function closestsNot(node, type) { + if (node.type !== type) { + return node; + } + return closestsNot(node.xParent, type); +} + +/** + * @returns {String} + * @param {String} str + */ +function getExportedProperty (str) { + if (str.startsWith('exports.')) { + return str.replace('exports.', ''); + } else if (str.startsWith('module.exports.')) { + return str.replace('module.exports.', ''); + } else if (str === 'module.exports') { // how to detect exports + return str; + } + return null; +} +/** + * @returns {Boolean} Returns true if MemberExpression itself or its object is computed, false otherwise + * @param {Node} node + */ +function isComputedMemberExpressionChild(node) { + if(node.type === es.Syntax.CallExpression) { + return isComputedMemberExpressionChild(node.callee); + } else if (node.type === es.Syntax.MemberExpression) { + if (node.computed && node.property.type !== es.Syntax.Literal) { + return true; + } + if (node.object) { + return isComputedMemberExpressionChild(node.object); + } + } + return false; +} +/** + * @returns {Boolean} Returns true if MemberExpression itself or its xParent is computed, false otherwise + * @param {Node} node + */ +function isComputedMemberExpressionParent(node) { + if (node.type === es.Syntax.CallExpression && node.xParent) { + return isComputedMemberExpressionParent(node.xParent); + } else if(node.type === es.Syntax.MemberExpression) { + if (node.computed && node.property.type !== es.Syntax.Literal) { + return true; + } else if (node.xParent) { + return isComputedMemberExpressionParent(node.xParent); + } + } + return false; +} + +/** + * @returns {Boolean} Returns true if any part of nested MemberExpression is computed, or false otherwise. + * @param {Node} node + */ +function isComputed(node) { + return isComputedMemberExpressionChild(node) || isComputedMemberExpressionParent(node); +} + +/** + * @returns {Boolean} returns true if regex introduces backtracking + * @param {String} regex + */ +function isDangerousRegex(regex) { + return !saferegex(regex); +} +/** + * @returns {Boolean} returns true if MemberExpression is exporting, and false otherwise. + * @param {*} node + */ +function isExport(node) { + let object = getObjectName(node); + if (object === 'exports' || object === 'module') { + let assignment = closests(node, es.Syntax.AssignmentExpression); + if (assignment && assignment.left === node) { + return true; + } + } + return false; +} +/** + * + * @param {*} node + */ +function getMemberExpressionMeta(node) { + if (node.type === es.Syntax.CallExpression) { + if (node.callee.type === es.Syntax.Identifier) { + let property = getPropertyName(node); + let computed = isComputed(node); + + return { + object: node.callee.name, + property: property, + computed: computed, + exported: false + } + } + + return getMemberExpressionMeta(node.callee); + + } else if (node.type === es.Syntax.MemberExpression) { + if (node.object.type === es.Syntax.Identifier) { + let property = getPropertyName(node); + let computed = isComputed(node); + let exported = false; + + if (node.object.name === 'exports' || (node.object.name === 'module' && node.property.type === es.Syntax.Identifier && node.property.name === 'exports')) { + let assignment = closests(node, es.Syntax.AssignmentExpression); + if (assignment && assignment.left.type === es.Syntax.MemberExpression) { + exported = getObjectName(assignment.left) === node.object.name; + } + } + + return { + object: node.object.name, + property: property, + computed: computed, + exported: exported + }; + } + return getMemberExpressionMeta(node.object); + } + return null; +} + +module.exports.VariableAssignmentName = VariableAssignmentName; +module.exports.createsNewScope = createsNewScope; +module.exports.getObjectName = getObjectName; +module.exports.getPropertyName = getPropertyName; +module.exports.getMemberExpressionString = getMemberExpressionString; +module.exports.closests = closests; +module.exports.closestsNot = closestsNot; +module.exports.getExportedProperty = getExportedProperty; +module.exports.isComputed = isComputed; +module.exports.isDangerousRegex = isDangerousRegex; +module.exports.isExport = isExport; +module.exports.getMemberExpressionMeta = getMemberExpressionMeta; \ No newline at end of file diff --git a/src/lib/utils/index.js b/src/lib/utils/index.js new file mode 100644 index 0000000..3165d72 --- /dev/null +++ b/src/lib/utils/index.js @@ -0,0 +1,187 @@ +const chalk = require('chalk'); +const fs = require('fs'); +const config = require('../Configurator'); +const path = require('path'); +const glob = require('glob'); +const AppBuilder = require('../models/AppBuilder'); +const ModuleBuilder = require('../models/ModuleBuilder'); +const nativeModules = require('./native-modules.js'); + + +/** + * Precalculates application stat + * @param {AppBuilder} app + */ +function calculateAppStatistic(app) { + for (const modul of app.modules) { + + app.totalStaticExports += modul.staticExport; + app.totalDynamicExports += modul.dynamicExport; + + app.totalStaticRequire += modul.staticRequire; + app.totalDynamicRequire += modul.dynamicRequire; + app.totalComplexDynamicRequire += modul.complexDynamicRequire; + app.totalDynamicUsage += modul.dynamicUsage; + + app.totalFunctions += modul.functions; + app.totalVariables += modul.variables; + + app.totalEval += modul.eval; + app.totalEvalWithVar += modul.evalWithVariable; + app.totalFunctionNew += modul.functionNew; + + app.totalRegex += modul.regex; + app.totalRegexDos += modul.regexDos; + app.totalStringReplace += modul.stringReplace; + app.totalStringMatch += modul.stringMatch; + app.totalStringSearch += modul.stringSearch; + app.totalJsonParse += modul.jsonParse; + app.totalMonkeyPatching += modul.monkeyPatching; + if (modul.isOwned) { + app.originalFilesLOC += modul.initialSloc; + app.originalFiles += 1; + } else { + app.externalFiles += 1; + app.externalFilesLOC += modul.initialSloc; + if (modul.isUsed) { + app.usedExternalFiles += 1; + app.usedExternalFilesLOC += modul.initialSloc; + app.usedExternalReducedFilesLOC += modul.finalSloc; + } + } + + if (modul.isRemoved) { + app.totalRemovedFiles += 1; + app.totalRemovedExports += (modul.staticExport + modul.dynamicExport); + app.totalRemovedLOC += modul.initialSloc; + // todo: calculate removed functions + app.totalRemovedFunctions += modul.functions; + // todo: calculate removed variables + app.totalRemovedVariables += modul.variables; + } else { + app.totalRemovedLOC += (modul.initialSloc - modul.finalSloc); + app.totalRemovedVariables += modul.removedVariables; + app.totalRemovedFunctions += modul.removedFunctions; + app.totalRemovedExports += modul.removedExports; + } + + // constructs the builtins field by traversing modules children before minimizing the module + for (let mod of Object.keys(modul.children)) { + if (nativeModules.includes(mod)) { + if (mod in app.builtins) { + Array.prototype.push.apply(app.builtins[mod], modul.children[mod].used); + } else { + app.builtins[mod] = modul.children[mod].used; + } + + } + } + minimizeModule(modul); + } + + // removes duplicate entries from array. + for (let mod of Object.keys(app.builtins)) { + app.builtins[mod] = [...new Set(app.builtins[mod])]; + } +} + +/** + * Minimizes module object by removing extra properties + * @param {ModuleBuilder} modul + */ +function minimizeModule(modul) { + delete modul.app; + delete modul.ast; + delete modul.initialSrc; + + if (config.compressLog) { + delete modul.memusages; + delete modul.identifiers; + delete modul.children; + for (let key of Object.keys(modul)) { + if (!modul[key]) delete modul[key]; + if (Array.isArray(modul[key]) && modul[key].length === 0) delete modul[key]; + } + } else { + for (const key in modul.children) { + delete modul.children[key].ref; + } + for (const key of Object.keys(modul.identifiers.table)) { + modul.identifiers.table[key].changeToArray(); + } + } +} + +/** + * Marks module as removed and removes corresponding js file + * @param {ModuleBuilder} modul + */ +async function removeFile(modul, dryRun = false) { + + try { + // remove only if it was analyzed. + if (!modul.parseError) { + // TODO: .bin, do we need this? + if (!(modul.path.includes('/.bin/') || modul.path.includes('/bin/'))) { + modul.isRemoved = true; + if (!dryRun) fs.unlinkSync(modul.path); + if(config.verbose) console.log('> removed: ', modul.path); + } + } + + } catch (error) { + throw new Error('CAN_NOT_REMOVE: ' + modul.path + '\n' + error.message); + } +} +/** + * @returns {Array} + * @param {AppBuilder} app + */ +function entryPoints(app) { + let entries = []; + if (config.seeds.length > 0) { + for (let seed of config.seeds) { + let files = glob.sync(seed, {cwd:app.path, absolute:true, nonull: false}); + for (let file of files) { + if (fs.lstatSync(file).isDirectory()) { + let jses = fs.readdirSync(file).filter(f => f.endsWith('.js')).map(e => path.join(file, e)); + for (let js of jses) { + entries.push(js); + } + } else if (file.endsWith('.js')) { + entries.push(file); + } + } + } + } + if (app.main) { + let uri = path.join(app.path, app.main); + if (fs.existsSync(uri)) { + if (fs.lstatSync(uri).isDirectory()) { + app.main = path.join(app.main, 'index.js'); + } else if (!app.main.endsWith('.js')) { + app.main = app.main + '.js'; + } + uri = path.join(app.path, app.main); + if (fs.existsSync(uri)) entries.push(uri); + else app.main = '-- not exists --'; + } + } else { + app.main = '-- undefined --'; + } + + return entries; +} + +function hasKey(object, key) { + return Object.prototype.hasOwnProperty.call(object, key); +} + +module.exports.flattenObjectKeys = require('./flatten-object-keys'); +module.exports.loader = require('./loader'); +module.exports.calculateAppStatictic = calculateAppStatistic; +module.exports.removeFile = removeFile; +module.exports.entryPoints = entryPoints; +module.exports.hasKey = hasKey; +module.exports.BinaryExpression = require('./binary-expression-utils'); +module.exports.entryPoint = require('./entry-point'); \ No newline at end of file diff --git a/src/lib/utils/loader.js b/src/lib/utils/loader.js new file mode 100644 index 0000000..43ca52f --- /dev/null +++ b/src/lib/utils/loader.js @@ -0,0 +1,21 @@ +const vm = require('vm'); +const m = require('module'); + +/** @param {String} path */ +function load(path, useVM = true) { + if (!useVM) { + let mod = null; + try { + mod = require(path); + } catch (ex) { + console.log('failed loading', path); + } + return mod; + } else { + let code = m.wrap(`module.exports = require('${path}')`); + vm.runInThisContext(code)(exports, require, module, __filename, __dirname) + return module.exports; + } +} + +module.exports = load; \ No newline at end of file diff --git a/src/lib/utils/member-expression-utils.js b/src/lib/utils/member-expression-utils.js new file mode 100644 index 0000000..49d21fa --- /dev/null +++ b/src/lib/utils/member-expression-utils.js @@ -0,0 +1,194 @@ + +const astutils = require('./ast-utils'); +const es = require('esprima'); + +/** + * Finds object name of the member expression. + * @returns {String} + * @param {MemberExpression} node + */ +function getObjectName (node) { + if (node.type === es.Syntax.CallExpression) { + return getObjectName(node.callee); + } else if (node.type === es.Syntax.Identifier) { + return node.name; + } + // should I add Literal??? + // else if (node.type === es.Syntax.Literal) { + // return node.value; + // } + if (node.type !== es.Syntax.MemberExpression) { + return null; + } else if (node.object.type === es.Syntax.Identifier) { + return node.object.name; + } else if (node.object.type === es.Syntax.ThisExpression) { + return 'this'; + } + return getObjectName(node.object); +} + +/** + * Contruct property name of the member expression. + * If member expression has nested properties returned string will contain + * all properties joined by . + * @returns {String} + * @param {MemberExpression} node + */ +function getPropertyName (node) { + if (node) { + if (node.type === es.Syntax.MemberExpression) { + let propertyName = ''; + if (node.property.type === es.Syntax.Identifier) { + propertyName = node.property.name; + } else if (node.property.type === es.Syntax.Literal) { + propertyName = node.property.value; + } + let parentProperty = getPropertyName(node['xParent']); + if (parentProperty) { + return propertyName + '.' + parentProperty; + } + return propertyName; + } else if (node.type === es.Syntax.CallExpression) { + return getPropertyName(node['xParent']); + } + } + + return null; +} + +function getMemberExpressionString (node) { + if (node.type !== es.Syntax.MemberExpression) return null; + if (node.object.type === es.Syntax.Identifier || node.object.type === es.Syntax.ThisExpression) { + let objectName = getObjectName(node); + let propertyName = getPropertyName(node); + return {object: objectName, property: propertyName}; + } else { + return getMemberExpressionString(node.object); + } +} + +/** + * @returns {String} + * @param {String} str + */ +function getExportedProperty (str) { + if (str.startsWith('exports.')) { + return str.replace('exports.', ''); + } else if (str.startsWith('module.exports.')) { + return str.replace('module.exports.', ''); + } else if (str === 'module.exports') { // how to detect exports + return str; + } + return null; +} +/** + * @returns {Boolean} Returns true if MemberExpression itself or its object is computed, false otherwise + * @param {Node} node + */ +function isComputedMemberExpressionChild(node) { + if(node.type === es.Syntax.CallExpression) { + return isComputedMemberExpressionChild(node.callee); + } else if (node.type === es.Syntax.MemberExpression) { + if (node.computed && node.property.type !== es.Syntax.Literal) { + return true; + } + if (node.object) { + return isComputedMemberExpressionChild(node.object); + } + } + return false; +} +/** + * @returns {Boolean} Returns true if MemberExpression itself or its xParent is computed, false otherwise + * @param {Node} node + */ +function isComputedMemberExpressionParent(node) { + if (node.type === es.Syntax.CallExpression && node.xParent) { + return isComputedMemberExpressionParent(node.xParent); + } else if(node.type === es.Syntax.MemberExpression) { + if (node.computed && node.property.type !== es.Syntax.Literal) { + return true; + } + if (node.xParent) { + return isComputedMemberExpressionParent(node.xParent); + } + } + return false; +} + +/** + * @returns {Boolean} Returns true if any part of nested MemberExpression is computed, or false otherwise. + * @param {Node} node + */ +function isComputed(node) { + return isComputedMemberExpressionChild(node) || isComputedMemberExpressionParent(node); +} + +/** + * @returns {Boolean} returns true if MemberExpression is exporting, and false otherwise. + * @param {*} node + */ +function isExport(node) { + let object = getObjectName(node); + if (object === 'exports' || object === 'module') { + let assignment = astutils.closests(node, es.Syntax.AssignmentExpression); + if (assignment && assignment.left.type === es.Syntax.MemberExpression) { + if (object === getObjectName(assignment.left)) { + return true; + } + } + } + return false; +} +/** + * + * @param {*} node + */ +function getMemberExpressionMeta(node) { + if (node.type === es.Syntax.CallExpression) { + if (node.callee.type === es.Syntax.Identifier) { + let property = getPropertyName(node); + let computed = isComputed(node); + + return { + object: node.callee.name, + property: property, + computed: computed, + exported: false + } + } + + return getMemberExpressionMeta(node.callee); + + } else if (node.type === es.Syntax.MemberExpression) { + if (node.object.type === es.Syntax.Identifier) { + let property = getPropertyName(node); + let computed = isComputed(node); + let exported = false; + + if (node.object.name === 'exports' || (node.object.name === 'module' && node.property.type === es.Syntax.Identifier && node.property.name === 'exports')) { + let assignment = astutils.closests(node, es.Syntax.AssignmentExpression); + if (assignment && assignment.left.type === es.Syntax.MemberExpression) { + exported = getObjectName(assignment.left) === node.object.name; + } + } + + return { + object: node.object.name, + property: property, + computed: computed, + exported: exported + }; + } + return getMemberExpressionMeta(node.object); + } + return null; +} + +exports.getMemberExpressionMeta = getMemberExpressionMeta; +exports.isComputed = isComputed; +exports.isExport = isExport; +exports.getExportedProperty = getExportedProperty; +exports.getMemberExpressionString = getMemberExpressionString; +exports.getObjectName = getObjectName; +exports.getPropertyName = getPropertyName; diff --git a/src/lib/utils/native-modules.js b/src/lib/utils/native-modules.js new file mode 100644 index 0000000..fd2ba04 --- /dev/null +++ b/src/lib/utils/native-modules.js @@ -0,0 +1,31 @@ +// TODO: complete the list of native modules +module.exports = [ + 'assert', + 'buffer', + 'child_process', + 'cluster', + 'crypto', + 'dgram', + 'dns', + 'domain', + 'events', + 'fs', + 'http', + 'https', + 'net', + 'os', + 'path', + 'punnycode', + 'querystring', + 'readline', + 'stream', + 'string_decoder', + 'timers', + 'tls', + 'tty', + 'url', + 'util', + 'v8', + 'vm', + 'zlib' +] \ No newline at end of file diff --git a/src/lib/utils/package-dependencies.js b/src/lib/utils/package-dependencies.js new file mode 100644 index 0000000..24536bb --- /dev/null +++ b/src/lib/utils/package-dependencies.js @@ -0,0 +1,86 @@ +const path = require('path') +const fs = require('fs'); +const utils = require('.'); +let glocation; +let table = {}; + +function main(location, dev = false) { + glocation = location; + table = {}; + if (!fs.existsSync(`${location}/package.json`)) { + throw new Error(`NO_PACKAGE.JSON`); + } + + let content = fs.readFileSync(`${location}/package.json`, {encoding: 'utf8'}); + let pckg = JSON.parse(content); + let seed = null; + if (dev) { + seed = pckg.devDependencies; + } else { + seed = pckg.dependencies; + } + if (!seed) { + return false; + } + + let result = {} + for (let key in seed) { + result[key] = dependencyBuilder(location, key); + } + + return result; +} + +function toList(location, dev = false) { + let dependencies = main(location, dev); + let result = utils.flattenObjectKeys(dependencies); + return [...new Set(result)]; +} +function dependencyBuilder(loc, name) { + if (glocation.length > loc.length) return null; + let l = `${loc}/node_modules/${name}`; + while(!fs.existsSync(l)) { + loc = path.dirname(loc); + if (loc.length < glocation.length) return null; + l = `${loc}/node_modules/${name}`; + } + + if (fs.existsSync(`${l}/package.json`)) { + let result = {}; + let content = fs.readFileSync(`${l}/package.json`, {encoding: 'utf8'}); + let package = JSON.parse(content); + if (!package.dependencies || Object.keys(package.dependencies).length === 0) return null; + for (let key in package.dependencies) { + if (table[l+key]) { + result[key] = JSON.parse(JSON.stringify(table[l+key])); + } else { + let depen = dependencyBuilder(l, key); + result[key] = depen; + table[l+key] = depen; + } + } + return result; + } else { + return null; + } +} + +function installedPackages(package, obj, includeDev = false) { + if (!package) return null; + + for(let p in package.dependencies) { + if (!package.dependencies[p].dev || includeDev) { + if(!Object.prototype.hasOwnProperty.call(obj, p)) { + obj[p] = [package.dependencies[p].version]; + } else if (!obj[p].includes(package.dependencies[p].version)){ + obj[p].push(package.dependencies[p].version); + } + installedPackages(package.dependencies[p], obj); + } + + } +} + +module.exports = main; +module.exports.toList = toList; +module.exports.installedPackages = installedPackages; \ No newline at end of file diff --git a/src/lib/utils/set-operations.js b/src/lib/utils/set-operations.js new file mode 100644 index 0000000..007939f --- /dev/null +++ b/src/lib/utils/set-operations.js @@ -0,0 +1,10 @@ +/** + * + * @param {Array} set + * @param {Array} subset + */ +function isSubset(set, subset) { + return subset.every( val => set.includes(val)); +} + +module.exports.isSubset = isSubset; \ No newline at end of file diff --git a/src/package-lock.json b/src/package-lock.json new file mode 100644 index 0000000..ab36e81 --- /dev/null +++ b/src/package-lock.json @@ -0,0 +1,814 @@ +{ + "name": "mininode", + "version": "0.0.4", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "requires": { + "color-convert": "^1.9.0" + } + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + }, + "brace-expansion": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.8.tgz", + "integrity": "sha1-wHshHHyVLsH479Uad+8NHTmQopI=", + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "camelcase": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", + "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=" + }, + "chalk": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", + "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "cli-table": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/cli-table/-/cli-table-0.3.1.tgz", + "integrity": "sha1-9TsFJmqLGguTSz0IIebi3FkUriM=", + "requires": { + "colors": "1.0.3" + }, + "dependencies": { + "colors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz", + "integrity": "sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs=" + } + } + }, + "cli-table3": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.5.1.tgz", + "integrity": "sha512-7Qg2Jrep1S/+Q3EceiZtQcDPWxhAvBw+ERf1162v4sikJrvojMHFqXt8QIVha8UlH9rgU0BeWPytZ9/TzYqlUw==", + "requires": { + "colors": "^1.1.2", + "object-assign": "^4.1.0", + "string-width": "^2.1.1" + } + }, + "cliui": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-4.0.0.tgz", + "integrity": "sha512-nY3W5Gu2racvdDk//ELReY+dHjb9PlIcVDFXP72nVIhq2Gy3LuVXYwJoPVudwQnv1shtohpgkdCKT2YaKY0CKw==", + "requires": { + "string-width": "^2.1.1", + "strip-ansi": "^4.0.0", + "wrap-ansi": "^2.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, + "code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" + }, + "color-convert": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.2.tgz", + "integrity": "sha512-3NUJZdhMhcdPn8vJ9v2UQJoH0qqoGUkYTgFEPZaPjEtwmmKUfNV46zZmgB2M5M4DCEQHMaCfWHCxiBflLm04Tg==", + "requires": { + "color-name": "1.1.1" + } + }, + "color-name": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.1.tgz", + "integrity": "sha1-SxQVMEz1ACjqgWQ2Q72C6gWANok=" + }, + "colors": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.3.2.tgz", + "integrity": "sha512-rhP0JSBGYvpcNQj4s5AdShMeE5ahMop96cTeDl/v9qQQm2fYClE2QXZRi8wLzc+GmXSxdIqqbOIAhyObEXDbfQ==" + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "cross-spawn": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", + "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", + "requires": { + "lru-cache": "^4.0.1", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" + }, + "deep-is": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=" + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" + }, + "escodegen": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.10.0.tgz", + "integrity": "sha512-fjUOf8johsv23WuIKdNQU4P9t9jhQ4Qzx6pC2uW890OloK3Zs1ZAoCNpg/2larNF501jLl3UNy0kIRcF6VI22g==", + "requires": { + "esprima": "^3.1.3", + "estraverse": "^4.2.0", + "esutils": "^2.0.2", + "optionator": "^0.8.1", + "source-map": "~0.6.1" + }, + "dependencies": { + "esprima": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-3.1.3.tgz", + "integrity": "sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=" + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "optional": true + } + } + }, + "esprima": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.0.tgz", + "integrity": "sha512-oftTcaMu/EGrEIu904mWteKIv8vMuOgGYo7EhVJJN00R/EED9DCua/xxHRdYnKtcECzVg7xOWhflvJMnqcFZjw==" + }, + "estraverse": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", + "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=" + }, + "esutils": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", + "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=" + }, + "execa": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz", + "integrity": "sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=", + "requires": { + "cross-spawn": "^5.0.1", + "get-stream": "^3.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + } + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=" + }, + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "requires": { + "locate-path": "^2.0.0" + } + }, + "fs-extra": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-6.0.1.tgz", + "integrity": "sha512-GnyIkKhhzXZUWFCaJzvyDLEEgDkPfb4/TPvJCJVuS8MWZgoSsErf++QpiAlDnKFcqhRlm+tIOcencCjyJE6ZCA==", + "requires": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + }, + "get-caller-file": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.2.tgz", + "integrity": "sha1-9wLmMSfn4jHBYKgMFVSstw1QR+U=" + }, + "get-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=" + }, + "glob": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", + "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "graceful-fs": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", + "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=" + }, + "graceful-readlink": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz", + "integrity": "sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=" + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "invert-kv": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", + "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=" + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" + }, + "is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=" + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" + }, + "jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", + "requires": { + "graceful-fs": "^4.1.6" + } + }, + "lcid": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", + "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=", + "requires": { + "invert-kv": "^1.0.0" + } + }, + "levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "requires": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + } + }, + "locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "requires": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + }, + "dependencies": { + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=" + } + } + }, + "lodash": { + "version": "4.17.14", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.14.tgz", + "integrity": "sha512-mmKYbW3GLuJeX+iGP+Y7Gp1AiGHGbXHCOh/jZmrawMmsE7MS4znI3RL2FsjbqOyMayHInjOeykW7PEajUk1/xw==" + }, + "lru-cache": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.1.tgz", + "integrity": "sha512-q4spe4KTfsAS1SUHLO0wz8Qiyf1+vMIAgpRYioFYDMNqKfHQbg+AVDH3i4fvpl71/P1L0dBl+fQi+P37UYf0ew==", + "requires": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + } + }, + "mem": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/mem/-/mem-1.1.0.tgz", + "integrity": "sha1-Xt1StIXKHZAP5kiVUFOZoN+kX3Y=", + "requires": { + "mimic-fn": "^1.0.0" + } + }, + "mimic-fn": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", + "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==" + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", + "requires": { + "path-key": "^2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1" + } + }, + "optionator": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", + "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", + "requires": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.4", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "wordwrap": "~1.0.0" + }, + "dependencies": { + "wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=" + } + } + }, + "os-locale": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-2.1.0.tgz", + "integrity": "sha512-3sslG3zJbEYcaC4YVAvDorjGxc7tv6KVATnLPZONiljsUncvihe9BQoVCEs0RZ1kmf4Hk9OBqlZfJZWI4GanKA==", + "requires": { + "execa": "^0.7.0", + "lcid": "^1.0.0", + "mem": "^1.1.0" + } + }, + "p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=" + }, + "p-limit": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.2.0.tgz", + "integrity": "sha512-Y/OtIaXtUPr4/YpMv1pCL5L5ed0rumAaAeBSj12F+bSlMdys7i8oQF/GUJmfpTS/QoaRrS/k6pma29haJpsMng==", + "requires": { + "p-try": "^1.0.0" + } + }, + "p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "requires": { + "p-limit": "^1.1.0" + } + }, + "p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=" + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + }, + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=" + }, + "prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=" + }, + "process-nextick-args": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", + "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=" + }, + "pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=" + }, + "readable-stream": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz", + "integrity": "sha512-m+qzzcn7KUxEmd1gMbchF+Y2eIUbieUaxkWtptyHywrX0rE8QEYqPC07Vuy4Wm32/xE16NcdBctb8S0Xe/5IeQ==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~1.0.6", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.0.3", + "util-deprecate": "~1.0.1" + } + }, + "readdirp": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.1.0.tgz", + "integrity": "sha1-TtCtBg3zBzMAxIRANz9y0cxkLXg=", + "requires": { + "graceful-fs": "^4.1.2", + "minimatch": "^3.0.2", + "readable-stream": "^2.0.2", + "set-immediate-shim": "^1.0.1" + }, + "dependencies": { + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "requires": { + "brace-expansion": "^1.1.7" + } + } + } + }, + "regexp-tree": { + "version": "0.0.85", + "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.0.85.tgz", + "integrity": "sha512-KkuweOhL1M00EHljLhq2lARpLoazdKd0BpkfOZKcO55lWhRqeRCIPSPGf9osgMPj1l/0v37FaqDwa9Ks1W+92A==", + "requires": { + "cli-table3": "^0.5.0", + "colors": "^1.1.2", + "yargs": "^10.0.3" + }, + "dependencies": { + "yargs": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-10.1.2.tgz", + "integrity": "sha512-ivSoxqBGYOqQVruxD35+EyCFDYNEFL/Uo6FcOnz+9xZdZzK0Zzw4r4KhbrME1Oo2gOggwJod2MnsdamSG7H9ig==", + "requires": { + "cliui": "^4.0.0", + "decamelize": "^1.1.1", + "find-up": "^2.1.0", + "get-caller-file": "^1.0.1", + "os-locale": "^2.0.0", + "require-directory": "^2.1.1", + "require-main-filename": "^1.0.1", + "set-blocking": "^2.0.0", + "string-width": "^2.0.0", + "which-module": "^2.0.0", + "y18n": "^3.2.1", + "yargs-parser": "^8.1.0" + } + }, + "yargs-parser": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-8.1.0.tgz", + "integrity": "sha512-yP+6QqN8BmrgW2ggLtTbdrOyBNSI7zBa4IykmiV5R1wl1JWNxQvWhMfMdmzIYtKU7oP3OOInY/tl2ov3BDjnJQ==", + "requires": { + "camelcase": "^4.1.0" + } + } + } + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" + }, + "require-main-filename": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", + "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=" + }, + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==" + }, + "safe-buffer": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", + "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" + }, + "safe-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-2.0.0.tgz", + "integrity": "sha512-thCAfpaDb/DuCwidgS2h5BGyNx+vcN9F8fPLLhOrDndirBhOAwPkB4V28LMc+/Km1uHOg0APIIXdSg1Ck8BHjw==", + "requires": { + "regexp-tree": "~0.0.85" + } + }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" + }, + "set-immediate-shim": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz", + "integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=" + }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "requires": { + "shebang-regex": "^1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=" + }, + "signal-exit": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" + }, + "sloc": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/sloc/-/sloc-0.2.0.tgz", + "integrity": "sha1-tC09oaRCpIn0VMMsYo6OvwAHh1w=", + "requires": { + "async": "~2.1.4", + "cli-table": "^0.3.1", + "commander": "~2.9.0", + "readdirp": "^2.1.0" + }, + "dependencies": { + "async": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/async/-/async-2.1.5.tgz", + "integrity": "sha1-5YfGhYCZSsZ/xW/4bTrFa9voELw=", + "requires": { + "lodash": "^4.14.0" + } + }, + "commander": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.9.0.tgz", + "integrity": "sha1-nJkJQXbhIkDLItbFFGCYQA/g99Q=", + "requires": { + "graceful-readlink": ">= 1.0.0" + } + } + } + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, + "string_decoder": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", + "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "strip-eof": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=" + }, + "supports-color": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", + "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", + "requires": { + "has-flag": "^3.0.0" + } + }, + "type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "requires": { + "prelude-ls": "~1.1.2" + } + }, + "universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==" + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "which": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.0.tgz", + "integrity": "sha512-xcJpopdamTuY5duC/KnTTNBraPK54YwpenP4lzxU8H91GudWpFv38u0CKjclE1Wi2EH2EDz5LRcHcKbCIzqGyg==", + "requires": { + "isexe": "^2.0.0" + } + }, + "which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=" + }, + "wrap-ansi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", + "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", + "requires": { + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1" + }, + "dependencies": { + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + } + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "y18n": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz", + "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=" + }, + "yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=" + }, + "yargs": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-11.0.0.tgz", + "integrity": "sha512-Rjp+lMYQOWtgqojx1dEWorjCofi1YN7AoFvYV7b1gx/7dAAeuI4kN5SZiEvr0ZmsZTOpDRcCqrpI10L31tFkBw==", + "requires": { + "cliui": "^4.0.0", + "decamelize": "^1.1.1", + "find-up": "^2.1.0", + "get-caller-file": "^1.0.1", + "os-locale": "^2.0.0", + "require-directory": "^2.1.1", + "require-main-filename": "^1.0.1", + "set-blocking": "^2.0.0", + "string-width": "^2.0.0", + "which-module": "^2.0.0", + "y18n": "^3.2.1", + "yargs-parser": "^9.0.2" + } + }, + "yargs-parser": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-9.0.2.tgz", + "integrity": "sha1-nM9qQ0YP5O1Aqbto9I1DuKaMwHc=", + "requires": { + "camelcase": "^4.1.0" + } + } + } +} diff --git a/src/package.json b/src/package.json new file mode 100644 index 0000000..dc3c0e8 --- /dev/null +++ b/src/package.json @@ -0,0 +1,30 @@ +{ + "name": "mininode", + "version": "0.0.4", + "description": "Mininode is a tool to reduce JavaScript code by using sophisticated techniques", + "main": "index.js", + "scripts": { + "test": "npm run test:dry & npm run test:soft & npm run test:hard", + "test:dry": "node index.js test-module/ --silent --dry-run", + "test:soft": "node index.js test-module/ --destination=mininode.soft && cp test-module/check.js mininode.soft/ && node mininode.soft/ && node mininode.soft/check.js", + "test:hard": "node index.js test-module/ --mode=hard --destination=mininode.hard && cp test-module/check.js mininode.hard/ && node mininode.hard/ && node mininode.hard/check.js", + "webpack": "cd mininode/ && webpack", + "parcel": "cd mininode/ && parcel index.js -d parcel", + "clean": "rm -rf mininode/ mininode.soft/ mininode.hard/ test-module/mininode.json" + }, + "author": "Igibek Koishybayev", + "license": "ISC", + "dependencies": { + "chalk": "^2.4.1", + "escodegen": "^1.10.0", + "esprima": "^4.0.0", + "estraverse": "^4.2.0", + "fs-extra": "^6.0.1", + "glob": "^7.1.3", + "resolve-from": "^4.0.0", + "safe-regex": "^2.0.0", + "sloc": "^0.2.0", + "yargs": "^11.0.0" + }, + "devDependencies": {} +} diff --git a/src/test-module/check.js b/src/test-module/check.js new file mode 100644 index 0000000..fca456a --- /dev/null +++ b/src/test-module/check.js @@ -0,0 +1,36 @@ +/** + * @file This files checks how well MININODE reduced unused code parts. + * @author Igibek Koishybayev + */ +console.log('==================================='); +console.log('STARTED CHECKING REDUCTION EFFIENCY'); +console.log('==================================='); + +[ + 'crossref', + 'dynamic-export', + 'dynamic-import', + 'dynamic-module', + 'dynamic-module-usage', + 'function-export', + 'global-module', + 'invisible-child-parent', + 'monkey-patching', + 'object-def-export', + 'proto-export', + 're-export', + 'rename-export', + 'rename-require', + 'normal' +].forEach(name => { + let mod = require(name); + checkReductionStatus(name, mod); +}) + +function checkReductionStatus(name, obj) { + for (let key in obj) { + if (key.startsWith('_')) { + console.log('NOT REDUCED>', name, key); + } + } +} diff --git a/src/test-module/index.js b/src/test-module/index.js new file mode 100644 index 0000000..b177daf --- /dev/null +++ b/src/test-module/index.js @@ -0,0 +1,152 @@ +const path = require('path'); +// path.resolve('..'); + +// normal (CORRECT!!!) usage of CommonJS exports and imports +try { + const normal = require('normal'); + normal.x(); + normal.z(); + // checkReductionStatus('normal', normal); +} catch (error) { + console.log('normal.js failed', error); +} + + +// challenge #7: cross-reference +try { + const crossref = require('crossref'); + crossref.use; //todo: is not working. Because I am not skipping unused exports. + // checkReductionStatus('crossref', crossref); +} catch (error) { + console.log('crossref failed', error); +} + +// challenge #9: dynamic export +try { + const dynamicExport = require('dynamic-export'); + dynamicExport.a(); + dynamicExport.c(); + // checkReductionStatus('dynamic-export', dynamicExport); +} catch (error) { + console.error('dynamic-export failed', error); +} + + +// challenge #5: dynamic import +try { + const dynamicImport = require('dynamic-import'); + dynamicImport.a(); + // checkReductionStatus('dynamic-import', dynamicImport); +} catch (error) { + console.error('dynamic-import failed', error); +} + + +// challenge #2: dynamic module +try { + const dynamicModule = require('dynamic-module'); + dynamicModule.a(); //todo: need to delete foo._b + // checkReductionStatus('dynamic-usage', dynamicModule); +} catch (error) { + console.error('dynamic-module failed', error); +} + +// challenge #3: invisible-child-parent +try { + const invisibleChildParent = require('invisible-child-parent'); + invisibleChildParent.used(); +} catch (error) { + console.error('invisible-child-parent', error); +} + + +// challenge #4: monkey patching +try { + const monkeyPatching = require('monkey-patching'); + monkeyPatching.used; +} catch (error) { + console.error('monkey-patching failed', error); +} + + +// challenge # +try { + const protoExport = require('proto-export'); + protoExport.a(); + protoExport.b(); + protoExport.d(); + // checkReductionStatus('proto-export', protoExport); +} catch (error) { + console.error('proto-export failed', error); +} + + +// challenge # +try { + const reExport = require('re-export'); + reExport.a; + reExport.b; + // checkReductionStatus('re-export', reExport); +} catch (error) { + console.error('re-export', error); +} + + +// challenge #8: re-assigning export +try { + // todo: implement + const renameExport = require('rename-export'); + renameExport.a(); + renameExport.b(); + renameExport.c(); + // checkReductionStatus('rename-export', renameExport); +} catch (error) { + console.error('rename-export failed', error); +} + + +// challenge #6: overwriting/renaming require +try { + const renameRequire = require('rename-require'); + renameRequire.used(); +} catch (error) { + console.error('rename-require failed', error); +} + + +// challenge #: dynamic usage of module +// fix: it is reducing dynamically used module +try { + const dynamicModuleUsage = require('dynamic-module-usage'); + dynamicModuleUsage.a(); + // checkReductionStatus('dynamic-module-usage', dynamicModuleUsage); +} catch (error) { + console.error('dynamic-module-usage failed', error); +} + +// challenge #: object-def-export +try { + const objectDefExport = require('object-def-export'); + objectDefExport.a(); + objectDefExport.b(); + // checkReductionStatus('object-def-export', objectDefExport); +} catch (error) { + console.log('object-def-export failed', error); +} + + +try { + const globalModule = require('global-module'); + globalModule.use(); +} catch (error) { + console.log('global-module failed', error); +} + +try { + const functionExport = require('function-export'); + const fe = new functionExport(); + fe.a(); + fe.b(); +} catch (error) { + console.log('function-export failed', error); +} diff --git a/src/test-module/node_modules/crossref/bar.js b/src/test-module/node_modules/crossref/bar.js new file mode 100644 index 0000000..acb6068 --- /dev/null +++ b/src/test-module/node_modules/crossref/bar.js @@ -0,0 +1,8 @@ +var foo = require('./foo'); + +exports.a = () => {} +exports._a = foo._a(); + +exports._b = function() { + foo._b(); +} \ No newline at end of file diff --git a/src/test-module/node_modules/crossref/foo.js b/src/test-module/node_modules/crossref/foo.js new file mode 100644 index 0000000..5340d5f --- /dev/null +++ b/src/test-module/node_modules/crossref/foo.js @@ -0,0 +1,4 @@ +exports.a = ()=>{}; +exports._a = function() {} +exports.b = function() {} +exports._b = function() {} \ No newline at end of file diff --git a/src/test-module/node_modules/crossref/index.js b/src/test-module/node_modules/crossref/index.js new file mode 100644 index 0000000..17541dd --- /dev/null +++ b/src/test-module/node_modules/crossref/index.js @@ -0,0 +1,5 @@ +var foo = require('./foo'); +var bar = require('./bar'); +foo.a(); +bar.a(); +exports.use = true; \ No newline at end of file diff --git a/src/test-module/node_modules/dynamic-export/index.js b/src/test-module/node_modules/dynamic-export/index.js new file mode 100644 index 0000000..df779f6 --- /dev/null +++ b/src/test-module/node_modules/dynamic-export/index.js @@ -0,0 +1,20 @@ +/** + * during dynamic exports mininode should reduce static exports, but not dynamic + **/ +exports.a = function(){} +exports._b = function(){ + //reduce +} +exports['c'] = function() { + //keep +} + +var d = 'd'; + +exports[d] = function(){ + //keep +} + +exports['_e'] = function(){ + //reduce +} diff --git a/src/test-module/node_modules/dynamic-import/bar.js b/src/test-module/node_modules/dynamic-import/bar.js new file mode 100644 index 0000000..1531a26 --- /dev/null +++ b/src/test-module/node_modules/dynamic-import/bar.js @@ -0,0 +1,16 @@ + +var m = './ba'; +var iden = m; +iden = m + 'z'; +// else if (2) m = './ba' + 'z'; +// else m = './b'.concat('az'); +var baz = require(iden); +// var baz = require('./baz'); +baz.xyz(); + +exports.a = function() {} +exports._b = function() {} + + +iden = 'hello'; +iden = m + iden; diff --git a/src/test-module/node_modules/dynamic-import/baz.js b/src/test-module/node_modules/dynamic-import/baz.js new file mode 100644 index 0000000..e6c8d28 --- /dev/null +++ b/src/test-module/node_modules/dynamic-import/baz.js @@ -0,0 +1,3 @@ +exports.xyz = function() {} +exports._xyz = function() {} +exports._a = function() {} \ No newline at end of file diff --git a/src/test-module/node_modules/dynamic-import/extra.js b/src/test-module/node_modules/dynamic-import/extra.js new file mode 100644 index 0000000..320c33d --- /dev/null +++ b/src/test-module/node_modules/dynamic-import/extra.js @@ -0,0 +1,3 @@ +//empty file +exports.a = function(){} +exports.b = function(){} \ No newline at end of file diff --git a/src/test-module/node_modules/dynamic-import/foo.js b/src/test-module/node_modules/dynamic-import/foo.js new file mode 100644 index 0000000..ba4ead9 --- /dev/null +++ b/src/test-module/node_modules/dynamic-import/foo.js @@ -0,0 +1,2 @@ +exports.a = function() {} +exports._b = function() {} diff --git a/src/test-module/node_modules/dynamic-import/index.js b/src/test-module/node_modules/dynamic-import/index.js new file mode 100644 index 0000000..e7ec53f --- /dev/null +++ b/src/test-module/node_modules/dynamic-import/index.js @@ -0,0 +1,11 @@ +var foo = require('./foo'); +foo.a(); + +var bar = require('./bar'); +bar.a(); + +exports.a = function(){} + + + + diff --git a/src/test-module/node_modules/dynamic-module-usage/bar.js b/src/test-module/node_modules/dynamic-module-usage/bar.js new file mode 100644 index 0000000..f361463 --- /dev/null +++ b/src/test-module/node_modules/dynamic-module-usage/bar.js @@ -0,0 +1,5 @@ +exports.a = function () {} +exports.b = function () {} +exports._c = function () { + // must delete if crossref working correctly +} \ No newline at end of file diff --git a/src/test-module/node_modules/dynamic-module-usage/foo.js b/src/test-module/node_modules/dynamic-module-usage/foo.js new file mode 100644 index 0000000..6284fa8 --- /dev/null +++ b/src/test-module/node_modules/dynamic-module-usage/foo.js @@ -0,0 +1,3 @@ +exports.a = function () {} +exports.b = function () {} +exports._will_not_delete_because_of_dynamic_usage = function () {} \ No newline at end of file diff --git a/src/test-module/node_modules/dynamic-module-usage/index.js b/src/test-module/node_modules/dynamic-module-usage/index.js new file mode 100644 index 0000000..6bd9cdf --- /dev/null +++ b/src/test-module/node_modules/dynamic-module-usage/index.js @@ -0,0 +1,16 @@ +var bar = require('./bar'); + +exports.a = function() { + var foo = require('./foo'); + var member = 'a'; + foo[member]; + + + bar['a']; + bar.b(); + +} + +exports._b = function() { + bar._c(); +} \ No newline at end of file diff --git a/src/test-module/node_modules/dynamic-module/bar.js b/src/test-module/node_modules/dynamic-module/bar.js new file mode 100644 index 0000000..7db9964 --- /dev/null +++ b/src/test-module/node_modules/dynamic-module/bar.js @@ -0,0 +1,4 @@ +exports.a = function() {} +exports._b = function () { + // reduce +} \ No newline at end of file diff --git a/src/test-module/node_modules/dynamic-module/foo.js b/src/test-module/node_modules/dynamic-module/foo.js new file mode 100644 index 0000000..7a6acce --- /dev/null +++ b/src/test-module/node_modules/dynamic-module/foo.js @@ -0,0 +1,8 @@ +exports.a = function(bar) { + bar.a; +} + + +exports._b = function () { + // reduce +} \ No newline at end of file diff --git a/src/test-module/node_modules/dynamic-module/index.js b/src/test-module/node_modules/dynamic-module/index.js new file mode 100644 index 0000000..329350e --- /dev/null +++ b/src/test-module/node_modules/dynamic-module/index.js @@ -0,0 +1,11 @@ +var foo = require('./foo'); +var bar = require('./bar'); + + +exports.a = function() { + foo.a(bar); +} + +exports._b = function() { + foo._b(); +} \ No newline at end of file diff --git a/src/test-module/node_modules/function-export/index.js b/src/test-module/node_modules/function-export/index.js new file mode 100644 index 0000000..67418d4 --- /dev/null +++ b/src/test-module/node_modules/function-export/index.js @@ -0,0 +1,6 @@ +module.exports = function () { + this.a = function(){} + this.b = function(){} + this._a = function(){} + this._b = function(){} +} \ No newline at end of file diff --git a/src/test-module/node_modules/global-module/bar.js b/src/test-module/node_modules/global-module/bar.js new file mode 100644 index 0000000..5398597 --- /dev/null +++ b/src/test-module/node_modules/global-module/bar.js @@ -0,0 +1,12 @@ +foo = require('./foo'); +foo.a(); +exports.a = function(){ + a = 1; + var c; +} +exports._a = function(){ + b = 2; + c = 3; +} + +var a, b; \ No newline at end of file diff --git a/src/test-module/node_modules/global-module/foo.js b/src/test-module/node_modules/global-module/foo.js new file mode 100644 index 0000000..ca44e72 --- /dev/null +++ b/src/test-module/node_modules/global-module/foo.js @@ -0,0 +1,6 @@ +exports.a = function(){} +exports.b = function(){ + console.log('global b function'); +} +exports._a = function(){} +exports._b = function(){} \ No newline at end of file diff --git a/src/test-module/node_modules/global-module/index.js b/src/test-module/node_modules/global-module/index.js new file mode 100644 index 0000000..fd87c0c --- /dev/null +++ b/src/test-module/node_modules/global-module/index.js @@ -0,0 +1,6 @@ +const bar = require('./bar'); + +exports.use = function() { + bar.a(); + foo.b(); +} \ No newline at end of file diff --git a/src/test-module/node_modules/invisible-child-parent/child.js b/src/test-module/node_modules/invisible-child-parent/child.js new file mode 100644 index 0000000..c8cfc20 --- /dev/null +++ b/src/test-module/node_modules/invisible-child-parent/child.js @@ -0,0 +1,12 @@ +exports.childA = function() { + console.log('calling child-A'); + parentFunctionCall(); +} + +exports._childB = function() { + console.log('calling child-B'); +} + +function parentFunctionCall() { + exports.parentA(); +} diff --git a/src/test-module/node_modules/invisible-child-parent/index.js b/src/test-module/node_modules/invisible-child-parent/index.js new file mode 100644 index 0000000..587a7cb --- /dev/null +++ b/src/test-module/node_modules/invisible-child-parent/index.js @@ -0,0 +1,4 @@ +var parent = require('./parent'); +exports.used = function() { + parent.a(); +} \ No newline at end of file diff --git a/src/test-module/node_modules/invisible-child-parent/parent.js b/src/test-module/node_modules/invisible-child-parent/parent.js new file mode 100644 index 0000000..c38c9f0 --- /dev/null +++ b/src/test-module/node_modules/invisible-child-parent/parent.js @@ -0,0 +1,15 @@ +exports = module.exports = require('./child'); + +exports.a = function() { + exports.childA(); +} + +exports.parentA = function() { + console.log('calling parent-A'); +} + + + +exports._a = function() {} +exports._b = function() {} + diff --git a/src/test-module/node_modules/monkey-patching/foo.js b/src/test-module/node_modules/monkey-patching/foo.js new file mode 100644 index 0000000..c6149ef --- /dev/null +++ b/src/test-module/node_modules/monkey-patching/foo.js @@ -0,0 +1,2 @@ +exports.a = function () {} +exports._a = function () {} \ No newline at end of file diff --git a/src/test-module/node_modules/monkey-patching/index.js b/src/test-module/node_modules/monkey-patching/index.js new file mode 100644 index 0000000..382f22b --- /dev/null +++ b/src/test-module/node_modules/monkey-patching/index.js @@ -0,0 +1,7 @@ +let foo = require('./foo'); + +foo.a = function() { + console.log('monkey patched foo.a'); +} + +exports.used = true; diff --git a/src/test-module/node_modules/normal.js b/src/test-module/node_modules/normal.js new file mode 100644 index 0000000..1a443df --- /dev/null +++ b/src/test-module/node_modules/normal.js @@ -0,0 +1,38 @@ +const fs = require('fs') + +fs.existsSync("notexitsfile"); + +exports.x = function() { + // save the comment + console.log('> inside:', module.id); + exports._x1 = 1; +} + +// remove the comment +exports._y = function() { + console.log('> inside:', module.id); +} + +/** + * save the comment + */ +module.exports.z = function() { + console.log('> inside:', module.id, 'called z'); + module.exports.z2 = 'hello'; +} + +module.exports._w = function() { + // remove the comment + console.log('> inside:', module.id); + exports.z(); +} + +let a = 1; + +let b, c; + +b = a; +c = 'hello'; +a = c; +b = c; +a = a + 5; \ No newline at end of file diff --git a/src/test-module/node_modules/object-def-export/index.js b/src/test-module/node_modules/object-def-export/index.js new file mode 100644 index 0000000..f117853 --- /dev/null +++ b/src/test-module/node_modules/object-def-export/index.js @@ -0,0 +1,6 @@ +module.exports = { + a: function() {}, + _a: function() {}, + b: function() {}, + _b: function() {} +} \ No newline at end of file diff --git a/src/test-module/node_modules/proto-export/index.js b/src/test-module/node_modules/proto-export/index.js new file mode 100644 index 0000000..76fbd1f --- /dev/null +++ b/src/test-module/node_modules/proto-export/index.js @@ -0,0 +1,26 @@ +exports.a = function() { + +} + +exports._a = function() { + +} + +Object.defineProperty(exports, 'b', { + value: function() { + console.log('> inside:', module.id, '-> b') + } +}) + +Object.defineProperty(exports, '_b', { + value: function() { + console.log('> inside:', module.id, '-> _b') + } +}) + +let d = 'd'; +Object.defineProperty(exports, d, { + value: function() { + console.log('> inside:', module.id, '-> d') + } +}) \ No newline at end of file diff --git a/src/test-module/node_modules/re-export/foo.js b/src/test-module/node_modules/re-export/foo.js new file mode 100644 index 0000000..d6a3fa9 --- /dev/null +++ b/src/test-module/node_modules/re-export/foo.js @@ -0,0 +1,3 @@ +exports.a = function () {} +exports.b = function () {} +exports._c = function () {} \ No newline at end of file diff --git a/src/test-module/node_modules/re-export/index.js b/src/test-module/node_modules/re-export/index.js new file mode 100644 index 0000000..45171e4 --- /dev/null +++ b/src/test-module/node_modules/re-export/index.js @@ -0,0 +1 @@ +module.exports = require('./foo'); \ No newline at end of file diff --git a/src/test-module/node_modules/rename-export/index.js b/src/test-module/node_modules/rename-export/index.js new file mode 100644 index 0000000..edd0176 --- /dev/null +++ b/src/test-module/node_modules/rename-export/index.js @@ -0,0 +1,13 @@ +var e = exports; +var another; +e.a = function(){} +e.b = function () {} +e._c = function () { + // reduce +} + +another = module.exports; +another.c = function(){} +another._d = function() { + // reduce +} \ No newline at end of file diff --git a/src/test-module/node_modules/rename-require/bar.js b/src/test-module/node_modules/rename-require/bar.js new file mode 100644 index 0000000..7d4b240 --- /dev/null +++ b/src/test-module/node_modules/rename-require/bar.js @@ -0,0 +1,4 @@ +exports.a = function(){}; +exports._a = function(){}; +exports.b = function(){}; +exports._b = function(){}; \ No newline at end of file diff --git a/src/test-module/node_modules/rename-require/foo.js b/src/test-module/node_modules/rename-require/foo.js new file mode 100644 index 0000000..7d4b240 --- /dev/null +++ b/src/test-module/node_modules/rename-require/foo.js @@ -0,0 +1,4 @@ +exports.a = function(){}; +exports._a = function(){}; +exports.b = function(){}; +exports._b = function(){}; \ No newline at end of file diff --git a/src/test-module/node_modules/rename-require/index.js b/src/test-module/node_modules/rename-require/index.js new file mode 100644 index 0000000..ddc369e --- /dev/null +++ b/src/test-module/node_modules/rename-require/index.js @@ -0,0 +1,11 @@ +var r = require; +var l = require; +var foo = r('./foo'); +var bar = l('./bar'); + +exports.used = function() { + foo.a(); + foo.b(); + bar.a(); + bar.b(); +} \ No newline at end of file diff --git a/src/test-module/package.json b/src/test-module/package.json new file mode 100644 index 0000000..6d7774e --- /dev/null +++ b/src/test-module/package.json @@ -0,0 +1,11 @@ +{ + "name": "mininode-test-package", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC" +} diff --git a/src/test-module/test.js b/src/test-module/test.js new file mode 100644 index 0000000..b0ab2bc --- /dev/null +++ b/src/test-module/test.js @@ -0,0 +1,5 @@ +const exec = require('child_process').execSync; + +exec('node index.js test/module/ --mode=hard --destination=mininode.hard', {stdio: [0, 1, 2]}); + +exec('node index.js test/module/ --destination=mininode.soft', {stdio: [0, 1, 2]}); diff --git a/src/test-module/webpack.config.js b/src/test-module/webpack.config.js new file mode 100644 index 0000000..6a427f5 --- /dev/null +++ b/src/test-module/webpack.config.js @@ -0,0 +1,9 @@ +const path = require('path'); + +module.exports = { + entry: './index.js', + output: { + path: path.resolve(__dirname, '.'), + filename: './webpack.js' + } +} \ No newline at end of file