Skip to content
Tomás Pollak edited this page Mar 6, 2015 · 6 revisions

A Prey plugin is essentially a Node.js (NPM) module: a folder containing, at its bare minimum, a Javascript file and a package.json with its definition. They are stored in the lib/agent/plugins directory of your Prey installation path (/usr/lib/prey or C:\Windows\Prey, depending on the OS).

A plugin's package.json

Prey uses the default package.json format used in NPM modules to define a plugin's name, description and version. The only Prey-specific key is an optional options object that includes a list of user-modifiable settings for the plugin.

Let's take a look at the reports-to-inbox plugin package definition:

{
  "name": "prey-plugin-reports-to-inbox",
  "description": "Delivers a copy of each generated report to a specified inbox, directly via SMTP (no password needed).",
  "version": "0.0.2",
  "options": {
    "recipient": {
      "message": "Enter email address for delivery. This is where the reports will be sent.",
      "regex": "^\\S+@\\S+\\.\\S+$"
    },
    "subject": {
      "message": "Enter email subject line. You can leave this as it is.",
      "default": "Prey Status Report",
      "allow_empty": false
    },
    "from": {
      "message": "Enter from address.",
      "default": "Prey <prey-anti-theft@somewhere.com>",
      "allow_empty": false  
    }
  }
}

If you see, this plugin accepts three settings to be modified by the user: recipient, subject, and from. All of them require values to be set, but the two latest have default values. Prey uses the reply library to request input from the user, and passes the entire options object when prompting for it -- in other words, the valid properties for any element in options are simply the ones that reply accepts.

The name, description and version are needed by NPM to allow finding in the NPM repository and installing/updating them via npm install [package-name]. Though currently Prey doesn't allow installing them via npm, we will allow that eventually, and that's why we're using the prey-plugin prefix for plugin names -- so users will be able to find them easily.

Entry point (index.js)

Let's take a look at the reports-to-inbox index.js file, at version 0.0.2. You'll see it's quite simple:

var agent,
    recipient;

function send_report(type, data) {
  agent.transports.smtp.send(data, { to: recipient })
}

exports.load = function(cb) {
  if (!this.config.get('recipient'))
    return cb(new Error('Recipient not found.'));

  agent = this;
  recipient = this.config.get('recipient');

  agent.hooks.on('report', send_report);
  cb(); // notify that plugin has loaded
}

exports.unload = function() {
  agent.hooks.remove('report', send_report);
}

There are a few things going on here. First, you'll see that the module has two exported functions: load and unload. These are the two minimum functions that a plugin can export, and they control the initialization and deinitialization flow that all plugins should hook into (we'll talk about the supported methods in a sec).

You'll also notice that the load() function is called with a callback function, whereas the unload() function isn't. That's because some plugins require making an asynchronous call in order to verify whether they can continue with the load process or not (e.g. authenticating with a given username/password combo).

So, Prey allows them to use a function to call back whenever they're ready, or whenever they can reply with an error. If no async call is needed, then simply don't declare a callback function in the method definition and Prey will continue with the init/deinit sequence without waiting for your callback.

Now, you're probably wondering what this is. this is the agent object that contains everything that a plugin needs to work its magic. Let's dive into that.

The agent object

All of the plugin exportable functions are called within the context of the agent, which contains the following properties:

config

A scoped settings manager to retrieve and store plugin-specific or global settings.

exports.load = function() {
  var config = this.config;

  // get a local setting for this plugin
  config.get('username'); // => 'ZeroCool'
  
  // get a global setting for the agent
  config.global.get('try_proxy'); // => null

  // set a value, but don't save it yet
  config.set('token', 'something');

  // save local settings
  config.save(function(err) { if (!err) console.log('Saved!') }); 

  // update a global setting. use with care!
  config.global.update('auto_connect', false);
}

For the full list of available global settings, take a look at the prey.conf.default file.

logger

A logger to print info, warning, error or debug messages to the log file.

var logger = this.logger;

logger.debug("Debugging.");
logger.info("Hello there.");
logger.notice("Check this out!");
logger.warn("Something's up!");
logger.error("Holy crap. Something's wrong.");
logger.critical("Global meltdown detected. Run for your lives!");

version

The version of the client, in semver format.

console.log(this.version); # => '1.2.3'

system

Provides access to the system functions, as declared in lib/system. These are mainly utility functions for getting info about the OS and the current session, as well as providing a way to run commands using impersonation, which is needed for most of the actions (lock, alert, etc).

var system = this.system;

var tempfile = system.tempfile_path('foobar.txt');
console.log(tempfile); // => 'C:\Windows\Temp\foobar.txt'

var hostname = system.get_device_name(); 
console..log(hostname); // => 'the_gibson'

system.get_os_info(function(err, info) {
  if (!err) console.log(info); // => { arch: 'x64', name: 'Ubuntu', version: '14.10' }
})

system.get_logged_user(function(err, logged_user) {
  if (!err) console.log(logged_user); // => 'tomas'
})

// the running user is the one under which the prey agent is being run
// in linux and mac, it should normally be 'prey', and in Windows, 'SYSTEM'
var user = system.get_running_user();
console.log(user); // => 'prey'

system.spawn_as_logged_user('tail', ['-f', '/var/log/messages'], function(err, child) {
  // the child object is the same one you get when doing a child_process.spawn() in Node.js
  // so you can do all the `child.stdout.on('data')` streaming as you please
})

system.exec_as_logged_user('gnome-screensaver-command', function(err, child) {
  // same as above. even though we're using exec() here, the call needs to be asynchronous
  // in spite that the system module performs a few async calls before firing the command.
})

helpers

The helper functions, as declared in lib/helpers.

program

An instance of the commander lib, with the parsed command line arguments.

hooks

The agent hooks from lib/agent/hooks. One of the most useful things in all of the Preyland.

commands

The commands module from lib/agent/commands.

providers

The provider controller from lib/agent/providers.

transports

The transports defined in lib/agent/transports (e.g. http, smtp, etc).

Putting it to a use

Ok, so why don't we build something? As we already know, the most basic plugin requires two functions: load() and unload(). Let's declare them and also store the agent/this object in a local, shared variable so we can reuse it when we want to.

var agent;

exports.load = function() {
  agent = this;
}

exports.unload = function() {
  agent = null;
}

Cool. Now let's actually do something. What about preventing a specific process from being launched by a specific user? Let's see.

var agent,
    timer,
    nasty_user,
    nasty_process;

function check_processes() {

  // get the list of running processes using the providers controller
  agent.providers.get('process_list', function(err, list) {
    if (err) 
      return agent.logger.error('Error getting processes: ' + err.message);
    
    // for each running process, check if its name matches the one that's flagged
    list.forEach(function(program) {
      if (program.user == nasty_user && program.name.match(nasty_process)) {
        process.kill(program.pid);
      }
    })
  })
}

exports.load = function() {
  agent = this;
  
  nasty_user    = agent.config.get('nasty_user');
  nasty_process = agent.config.get('nasty_process');

  // set up a timer to check the running processes, every 60 seconds
  timer = setInterval(check_processes, 60000);
}

exports.unload = function() {
  agent = null;

  // ensure the interval no longer runs.
  if (timer) clearInterval(timer);
}

Not bad, eh? Now let's try to make this baby a bit smarted. What about preventing a process from being launched only on certain conditions? Let's use Prey's missing/not missing commands as a semaphore to limit whether the check is performed.

And while we're at it, why don't we tell the user that he's breaking the rules?

var agent,
    timer,
    missing,
    action,
    nasty_process,
    alert_message;

function check_command(command, target) {
  if (target == 'missing' || target == 'stolen') {
    // when missing, the command is 'report', otherwise 'cancel'
    missing = command == 'report' ? true : false;
  }
}

function check_processes() {
  if (!missing) return; // if not missing, stop right here.

  // get the list of running processes using the providers controller
  agent.providers.get('process_list', function(err, list) {
    if (err) 
      return agent.logger.error('Error getting processes: ' + err.message);
    
    // for each running process, check if its name matches the one that's flagged
    list.forEach(function(program) {
      if (program.name.match(nasty_process)) {
        if (action != 'kill')
          show_alert();

        if (action != 'alert')
          kill_process(program.pid);
      }
    })
  })
}

function show_alert() {
  // show a fullscreen alert with the message from the plugin's settings 
  agent.commands.run('start', 'alert', { message: alert_message });
}

function kill_process(pid) {
  // we'll use the handy kill_as_logged_user command to terminate this guy
  // given that we're not running as the logged user, so we need to impersonate.
  
  agent.system.kill_as_logged_user(pid, function(err) {
    var result = !err ? 'Successfully killed flagged process.' : 'Unable to kill: ' + err.message;
    logger.warn(result);
  })
}

exports.load = function() {
  agent = this;
  
  action        = agent.config.get('action');
  nasty_process = agent.config.get('nasty_process');
  alert_message = agent.config.get('alert_message');

  // first, make sure we know whenever the device is marked or unmarked missing/stolen
  agent.hooks.on('command', check_command);

  // set up a timer to check the running processes, every 60 seconds
  timer = setInterval(check_processes, 60000);
}

exports.unload = function() {
  agent = null;

  // ensure the interval no longer runs.
  if (timer) clearInterval(timer);
}

Sweet.

If you see, we need the user to define three options for the plugin to work. One is the nasty_process, that is, the name of the command that will trigger the following action. The second one is the action to take whenever that occurs, that can be either 'alert', 'kill', or 'both'. The third one is, of course, the alert message itself, which should only be requested unless the 'kill' action is chosen.

So we already know what needs to go into our plugin definition to wrap it all up. Let's call this plugin 'nasty process'.

{
  "name": "prey-plugin-nasty-process",
  "description": "Shows an alert or terminates a specific process, whenever launched.",
  "version": "0.0.2",
  "options": {
    "nasty_process": {
      "message": "The name of the nasty process as seen via the ps command (e.g. nasty-process)"
    },
    "action": {
      "message": "Whether to show an alert, kill the process, or both. (alert/kill/both)",
      "default": "alert",
      "regex":  "^(alert|kill|both)$"
    },
    "alert_message": {
      "message": "Message to show, if action is either 'alert' or 'both'.",
      "allow_empty": false,
      "depends_on": {
        "action": "^(alert|both)$"
      }
    }
  }
}

Now, let's make sure these two files (index.js and package.json) are inserted into the lib/agent/plugins/nasty-process and then enable the plugin.

$ ls -al lib/agent/plugins/nasty-process
index.js package.json

$ sudo bin/prey config plugins enable nasty-process

The name of the nasty process as seen via the ps command (e.g. nasty-process)
 - nasty-program: foobar 
Whether to show an alert, kill the process, or both. (alert/kill/both)
 - action: both
Message to show, if action is either 'alert' or 'both'.
 - alert_message: Hey there! This program is too nasty to run on this computer. Sorry.

Succesfully enabled nasty-process plugin.

There you go mister. Ready to roll!