-
Notifications
You must be signed in to change notification settings - Fork 76
Writing Plugins
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).
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 <[email protected]>",
"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.
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.
All of the plugin exportable functions are called within the context of the agent, which contains the following properties:
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.
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!");
The version of the client, in semver format.
console.log(this.version); # => '1.2.3'
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.
})
The helper functions, as declared in lib/helpers
.
An instance of the commander lib, with the parsed command line arguments.
The agent hooks from lib/agent/hooks
. One of the most useful things in all of the Preyland.
The commands module from lib/agent/commands
.
The provider controller from lib/agent/providers
.
The transports defined in lib/agent/transports
(e.g. http, smtp, etc).
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!