node.js module for executing os commands against a pool of stateful child processes such as bash or powershell via stdout and stderr streams
JavaScript
Latest commit c45f4bd Nov 10, 2016 @bitsofinfo updated travis info

README.md

stateful-process-command-proxy

Node.js module for executing os commands against a pool of stateful, long-lived child processes such as bash shells or powershell consoles

Build Status

NPM NPM

This node module can be used for proxying long-lived bash process, windows console etc. It works and has been tested on both linux, os-x and windows hosts running the latest version of node.

Origin

This project originated out of the need to execute various Powershell commands (at fairly high volume and frequency) against services within Office365/Azure bridged via a custom node.js implemented REST API; this was due to the lack of certain features in the REST GraphAPI for Azure/o365, that are available only in Powershell.

If you have done any work with Powershell and o365, then you know that there is considerable overhead in both establishing a remote session and importing and downloading various needed cmdlets. This is an expensive operation and there is a lot of value in being able to keep this remote session open for longer periods of time rather than repeating this entire process for every single command that needs to be executed and then tearing everything down.

Simply doing an child_process.exec per command to launch an external process, run the command, and then killing the process is not really an option under such scenarios, as it is expensive and very singular in nature; no state can be maintained if need be. We also tried using edge.js with powershell and this simply would not work with o365 exchange commands and heavy session cmdlet imports (the entire node.js process would crash). Using this module gives you full un-fettered access to the externally connected child_process, with no restrictions other than what uid/gid (permissions) the spawned process is running under (which you really have to consider from security standpoint!)

The diagram below should conceptually give you an idea of what this module does.

The local user that the node process runs as should have virtually zero rights! Also be sure to properly configure a restricted UID/GID when instatiating a new instance of this. See security notes below.

Alt text

Features

  • Works with any operating system that can run Node.js
  • Tested w/ Bash and Powershell, and should work with virtually any other shell or interactive spawnable process which can be communicated with over STDIN, STDOUT, STDERR streams.
  • Maintains a configurable pool of re-usable processes that are checked out/in when commands need to be executed
  • Command whitelisting and blacklisting
  • Definable list of "init" and "destroy" commands to be run as processes are created/destroyed
  • Definable configuration for "auto-invalidation" of active processes
  • Can be configured to maintain a "history" of commands run against each proxied process, useful for reporting or auditing purposes

Install & Tests

npm install stateful-process-command-proxy
npm install mocha
mocha test/all.js

History

v1.0.1 - 2016-11-10
    - Address #13 (force generic-pool 2.4.4)

v1.0.0 - 2016-06-08
    - Address #7 and #8 (regex w/ global flag reset, arguments in strict-mode)

v1.0-beta.8 - 2015-03-19
    - Address memory leaks

v1.0-beta.7 - 2015-02-05
    - Blacklist logging fix

v1.0-beta.6 - 2015-01-30
    - bug fixes, for auto-invalidation cmds being auto-whitelisted

v1.0-beta.5 - 2015-01-28
    - whitelisting fix

v1.0-beta.4 - 2015-01-28
    - New options for command whitelist regex matching
      Note new parameter order in ProcessProxy constructor!

    - Support for regex flags for all regex based configs

    - All regex pattern based configurations now must be objects
      in format {regex:'pattern' [,flags:'img etc']}

v1.0-beta.3 - 2015-01-26
    - New options for command blacklisting regex matching and interval
      based self auto-invalidation of ProcessProxy instances

v1.0-beta.2 - 2015-01-21
    - New return types for executeCommands - is now an array

v1.0-beta.1 - 2015-01-17
    - Initial version

Usage

To use StatefulProcessCommandProxy the constructor takes one parameter which is a configuration object who's properties are described below. Please refer to the example (following) and the unit-test for more details.

    name:           The name of this instance, arbitrary

    max:               maximum number of processes to maintain

    min:               minimum number of processes to maintain

    idleTimeoutMS:     idle in milliseconds by which a process will be destroyed

    processCommand: full path to the actual process to be spawned (i.e. /bin/bash)

    processArgs:    arguments to pass to the process command

    processRetainMaxCmdHistory: for each process spawned, the maximum number
                                of command history objects to retain in memory
                                (useful for debugging), default 0

    processInvalidateOnRegex: optional config of regex patterns who if match
                              their respective type, will flag the process as invalid
                                          {
                                         'any' :    [ {regex:'regex1',flags:'ig'}, ....],
                                         'stdout' : [ {regex:'regex1',flags:'m'}, ....],
                                         'stderr' : [ {regex:'regex1',flags:'ig'}, ....]
                                         }

   processCmdBlacklistRegex: optional config array regex patterns who if match the
                             command requested to be executed will be rejected
                             with an error. Blacklists run before whitelists

                                     [ {regex:'regex1',flags:'ig'},
                                       {regex:'regex2',flags:'ig'}...]

   processCmdWhitelistRegex: optional config array regex patterns defining commands
                             that are permitted to execute, if no match, the command
                             will be rejected. Whitelists run after blacklists

                                       [ {regex:'regex1',flags:'ig'},
                                         {regex:'regex2',flags:'ig'}...]

    processCwd:    optional current working directory for the processes to be spawned

    processEnvMap: optional hash/object of key-value pairs for environment variables
                   to set for the spawned processes

    processUid:    optional uid to launch the processes as

    processGid:    optional gid to launch the processes as

    logFunction:    optional function that should have the signature
                    (severity,origin,message), where log messages will
                    be sent to. If null, logs will just go to console

    initCommands:   optional array of actual commands to execute on each newly
                    spawned ProcessProxy in the pool before it is made available

    preDestroyCommands: optional array of actual commands to execute on a process
                        before it is killed/destroyed on shutdown or being invalid

    validateFunction:  optional function that should have the signature to accept
                       a ProcessProxy object, and should return true/false if the
                       process is valid or not, at a minimum this should call
                       ProcessProxy.isValid(). If the function is not provided
                       the default behavior is to only check ProcessProxy.isValid()

    autoInvalidationConfig optional configuration that will run the specified
                           commands in the background on the given interval,
                           and if the given regexes match/do-not-match for each command the
                           ProcessProxy will be flagged as invalid and return FALSE
                           on calls to isValid(). The commands will be run in
                           order sequentially via executeCommands()
        {
           checkIntervalMS: 30000; // check every 30s
           commands:
              [
               { command:'cmd1toRun',

                 // OPTIONAL: because you can configure multiple commands
                 // where the first ones doe some prep, then the last one's
                 // output needs to be evaluated hence 'regexes'  may not
                 // always be present, (but your LAST command must have a
                 // regexes config to eval prior work, otherwise whats the point

                 regexes: {
                        // at least one key must be specified
                        // 'any' means either stdout or stderr
                        // for each regex, the 'on' property dictates
                        // if the process will be flagged invalid based
                        // on the results of the regex evaluation
                       'any' :    [ {regex:'regex1', flags:'m', invalidOn:'match | noMatch'}, ....],
                       'stdout' : [ {regex:'regex1', flags:'ig', invalidOn:'match | noMatch'}, ....],
                       'stderr' : [ {regex:'regex1', flags:'i', invalidOn:'match | noMatch'}, ....]
                  }
              },...
            ]
       }

Its highly recommended you check out the unit-tests for some examples in addition to the below:

Example

Note this example is for a machine w/ bash in the typical location on *nix machines (i.e. linux or os-x). Windows (or other) can adjust the below as necessary to run their shell of choice, dos/powershell etc).

var Promise = require('promise');
var StatefulProcessCommandProxy = require("./");

var statefulProcessCommandProxy = new StatefulProcessCommandProxy(
    {
      name: "test",
      max: 2,
      min: 2,
      idleTimeoutMS: 10000,

      logFunction: function(severity,origin,msg) {
          console.log(severity.toUpperCase() + " " +origin+" "+ msg);
      },

      processCommand: '/bin/bash',
      processArgs:  ['-s'],
      processRetainMaxCmdHistory : 10,

      processInvalidateOnRegex :
          {
            'any':[{regex:'.*error.*',flags:'ig'}],
            'stdout':[{regex:'.*error.*',flags:'ig'}],
            'stderr':[{regex:'.*error.*',flags:'ig'}]
          },

      processCwd : './',
      processEnvMap : {"testEnvVar":"value1"},
      processUid : null,
      processGid : null,

      initCommands: [ 'testInitVar=test' ],

      validateFunction: function(processProxy) {
          return processProxy.isValid();
      },

      preDestroyCommands: [ 'echo This ProcessProxy is being destroyed!' ]
    });

// echo the value of our env variable set above in the constructor config
statefulProcessCommandProxy.executeCommand('echo testEnvVar')
  .then(function(cmdResult) {
      console.log("testEnvVar value: Stdout: " + cmdResult.stdout);
  }).catch(function(error) {
      console.log("Error: " + error);
  });

// echo the value of our init command that was configured above
statefulProcessCommandProxy.executeCommand('echo testInitVar')
  .then(function(cmdResult) {
      console.log("testInitVar value: Stdout: " + cmdResult.stdout);
  }).catch(function(error) {
      console.log("Error: " + error);
  });

// test that our invalidation regex above traps and destroys this process instance
statefulProcessCommandProxy.executeCommand('echo "this command has an error and will be '+
                ' destroyed after check-in because it matches our invalidation regex"')
  .then(function(cmdResult) {
      console.log("error test: Stdout: " + cmdResult.stdout);
  }).catch(function(error) {
      console.log("Error: " + error);
  });

// set a var in the shell
statefulProcessCommandProxy.executeCommand('MY_VARIABLE=test1;echo MY_VARIABLE WAS JUST SET')
  .then(function(cmdResult) {
      console.log("Stdout: " + cmdResult.stdout);
  }).catch(function(error) {
      console.log("Error: " + error);
  });

// echo it back
statefulProcessCommandProxy.executeCommand('echo $MY_VARIABLE')
  .then(function(cmdResult) {
      console.log("MY_VARIABLE value: Stdout: " + cmdResult.stdout);
  }).catch(function(error) {
      console.log("Error: " + error);
  });

// shutdown the statefulProcessCommandProxy
// this is important and your destroy hooks will
// be called at this time.
setTimeout(function() {
  statefulProcessCommandProxy.shutdown();
},10000);

Security

Obviously this module can expose you to some insecure situations depending on how you use it... you are providing a gateway to an external process via Node on your host os! (likely a shell in most use-cases). Here are some tips; ultimately its your responsibility to secure your system.

  • Read OWASPs article on command injection - https://www.owasp.org/index.php/Command_Injection
  • Ensure that the node process is running as a user with very limited rights
  • Make use of the uid/gid configuration appropriately to further limit the processes
  • Make use of the whitelisted and blacklisted command configuration feature to mitigate your exposure
  • Never expose calls to this module directly, instead you should write a wrapper layer around StatefulProcessCommandProxy that protects, analyzes and sanitizes external input that can materialize in a command statement. For an example of this kind of wrapper w/ sanitization of arguments see https://github.com/bitsofinfo/powershell-command-executor
  • All commands you pass to execute should be sanitized to protect from injection attacks. The type of sanitization you do is up to you and is obviously different depending on what shell/process type you are mediating access to via this module.

Related Tools

Have a look at these related projects which build on top of this module to provide some higher level functionality