The origin of the term "canary" (as a method of detection) was originally used by those that worked deep in mines and would take a canary (the bird) with them to detect gas or other reasons they needed to leave. If the bird started behaving oddly they knew something was amiss. This same concept is applied in the security world and is similarly called a "canary".
Similarly, the Canary
library allows you to define key/value combinations that can be used to detect when certain data is used and notify
you using a variety of methods including the default PHP error log, log handling via Monolog
and messages to Slack
channels.
For example, you may generate a special username that you want to use as a trigger. This username isn't actually a user in your system
but you do want to be notified if a login attempt is made using it. Canary
makes this simple by defining checks with an if
method and,
optionally, a handler using a then
method. For example, say we generated the username of canary1234@foo.com
and we want to detect when
it's used. You can define this in a Canary
expression like so:
<?php
$_POST = [
'username' => 'canary1234@foo.com',
'password' => 'sup3rs3cr3t'
];
\Psecio\Canary\Instance::build()->if('username', 'canary1234@foo.com')->execute();
// Or you can set multiple match values to look for with an array
$matches = [
'username' => 'canary1234@foo.com',
'password' => 'sup3rs3cr3t'
];
\Psecio\Canary\Instance::build()->if($matches)->execute();
?>
In this example we're looking at the current input and checking to see if there's a username
value of canary1234@foo.com
. In the case
of our current $_POST
values, there's a match. By default (if no then
handler is defined) the information about the match is output to
the error like (via the Psecio\Canary\Notify\ErrorLog
handler). The JSON encoded result looks like this:
{"type":"equals","key":"username","value":"canary1234@foo.com"}
NOTE: Canary automatically pulls in the
$_GET
and$_POST
superglobal values for evaluation so you don't need to manually pass then in.
Canary
also allows you to use a (static) class method to provide the if
portion of the evaluation with data. To use it, just pass in the class
and static method name as a string:
<?php
$classMethod = '\Foo\Bar::criteria';
\Psecio\Canary\Instance::build()->if($classMethod)->execute();
?>
The return from this method must be an array otherwise an exception will be thrown.
Currently Canary
supports the following notification methods:
Type | Class | Expected Input |
---|---|---|
Error log | \Psecio\Canary\Notify\ErrorLog |
None, uses default location |
Monolog | \Psecio\Canary\Notify\Monolog |
\Monolog\Logger |
Callback | \Psecio\Canary\Notify\Callback |
Callable function |
Slack | \Psecio\Canary\Notify\Slack |
\Maknz\Slack\Client |
PagerDuty | \Psecio\Canary\Notify\PagerDuty |
\PagerDuty\Event |
If you don't want your results to go to the error log, you can create your own handler via the then
method. Currently the only custom
handler supported is a callable method. So, say we wanted to output a message to the user of our special username and kill the script. We
might use something like this:
<?php
$_POST = ['username' => 'canary1234@foo.com'];
\Psecio\Canary\Instance::build()->if('username', 'canary1234@foo.com')
->then(function($criteria) {
die("You shouldn't have done that!");
})
->execute();
?>
In this handler, when it detects that the username value matches our criteria, the callback is executed and the die
call kills the script.
You can also provide your own data set if you don't want to auto-load the current $_GET
and $_POST
values. To pass the data in you can use the
data
value in the configuration and passing it in:
<?php
$config = ['data' => [
'username' => 'foobar@baz.com'
]];
\Psecio\Canary\Instance::build($config)->if('username', 'canary1234@foo.com')->execute();
?>
You can set it as the default logger for all if
checks via the notify
key in the build()
configuration options:
<?php
// create a log channel
$log = new Logger('name');
$log->pushHandler(new StreamHandler('/tmp/mylog.log', Logger::WARNING));
$config = [
'notify' => $log
];
\Psecio\Canary\Instance::build($config)->if('username', 'canary1234@foo.com')->execute();
?>
NOTE: If you provide a default handler via the
notify
configuration it will override all other custom notification methods.
The Canary
tool also allows you to use the Monolog logging library to define a bit more customization to the structure of the data and how it's output. Like before, we create the Canary
instance but for the input of the then
method we provide a Monolog\Logger
instance:
<?php
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
require_once 'vendor/autoload.php';
$_GET = ['username' => 'test'];
// create a log channel
$log = new Logger('name');
$log->pushHandler(new StreamHandler('/tmp/mylog.log', Logger::WARNING));
\Psecio\Canary\Instance::build()
->if('username', 'canary1234@foo.com')
->then($log)
->execute();
?>
You can also make use of the Maknz\Slack library to send messages to Slack when a canary is triggered:
<?php
$settings = [
'channel' => '#my-channel-name',
'link_names' => true
];
$slack = new Maknz\Slack\Client('https://hooks.slack.com/services/.....', $settings);
\Psecio\Canary\Instance::build($config)->if('username', 'canary1234@foo.com')->then($slack);
?>
You'll need to set up an incoming webhook and replace the URL value in the Client
create with the custom URL you're given. The default name for the notifications is Canary Agent
and the output includes the same JSON
information as the other notification methods.
Canary
also allows you to send notifications to your account on the PagerDuty service using the nmcquay/pagerduty library:
<?php
$pager = new \PagerDuty\Event();
$pager->setServiceKey('[.... your service ID ....]');
\Psecio\Canary\Instance::build($config)->if('username', 'canary1234@foo.com')->then($pager);
?>
You can find the service ID by going to your services page (https://[your domain].pagerduty.com/services
) and clicking on the service you want to use. The ID is under the "Integrations" tab as the "Integration Key".