Graylog and Cylance Protect Integration

TL;DR - we needed to ingest multiple sources of Cylance logs into Graylog, and this is how we did it.

Since we had never integrated with their API before, we were looking at a lot of unknowns. Searching GitHub uncovered a huge win. Lucky for us, someone had already done a lot of the heavy lifting writing Python wrappers to hit the Cylance API. We forked this repository, and integrated it with our own custom application.

This is a lightweight Flask application that runs locally on your system (in this example, it's running on an Ubuntu server), and polls as little or as often as you want. By default, it polls the Cylance API every minute for new threat and detection events. There are other endpoints available, but these are the two we decided to ingest. You can very easily modify the script to hit the other endpoints as needed.

Note: We have a pull request open that adds the detections API endpoint and a get_detections method to the CyPyAPI repository, which is in use in this application and in our fork of the repo. Many thanks to those folks for their work.

Let's get started :)

Cylance API access

In order to use the Cylance API, you need to find a few important pieces of information.

  • Application ID
  • Application Secret
  • Tenant ID
  • Region code

These are available on the dashboard in the web UI. You'll need to create an application (if you don't have one already) in order to generate the ID and secret. This can be done under Settings --> Integrations --> Add Application.

Configuring the application

First, you'll need to clone our repo from GitHub.

git clone https://github.com/ReconInfoSec/cylance-logs.git /opt
cd /opt/cylance-logs

Those values from the previous section? This is why you need them. The application's configuration file reads those values from environment variables set in the systemd file you'll be creating in the next step. Of course, you can also define them as values here. Up to you.

In the interest of not having them touch any GitHub repo, we left them in environment variables.

config.py:

import os
basedir = os.path.abspath(os.path.dirname(__file__))

class Config(object):
    APP_ID=os.environ.get('APP_ID')
    APP_SECRET=os.environ.get('APP_SECRET')
    TENANT_ID=os.environ.get('TENANT_ID')
    REGION_CODE=os.environ.get('REGION_CODE')
    LOG_FILE='/var/log/cylance-protect.log'

If you want the logs to go somewhere else, be sure to change the LOG_FILE variable accordingly.

Creating the service

You will need to create a systemd script (sample script is available in init.d in the repo), /etc/systemd/system/cylance-logging.service so the application can run in the background.

Feel free to modify the following script as necessary, and be sure to replace the INSERT lines with your own values from the first section.

[Unit]
Description=cylance-logging
After=multi-user.target

[Service]
Type=idle
Environment="APP_ID=INSERT_APP_ID_HERE"
Environment="APP_SECRET=INSERT_SECRET_HERE"
Environment="TENANT_ID=INSERT_TENANT_ID_HERE"
Environment="REGION_CODE=INSERT_REGION_CODE_HERE"

WorkingDirectory=/opt/cylance-logging
ExecStart=/usr/bin/python app.py runserver

# Connects standard output to /dev/null
StandardOutput=journal

# Connects standard error to journal
StandardError=journal

[Install]
WantedBy=multi-user.target

Once this is created, you can start the service:

sudo systemctl daemon-reload
sudo systemctl enable cylance-logging
sudo systemctl start cylance-logging

Verify you can see logs coming in under /var/log/cylance-protect.log, and that you don't see any errors there or in /var/log/syslog. If you see 400's, the API credentials are likely configured incorrectly.

Sending logs to Graylog

If you're already using Graylog's sidecar collector to ship logs, great! You're already halfway there. You'll simply add a configuration that tells it to pull in /var/log/cylance-protect.log (or whatever you set the LOG_FILE variable to).

Similarly, if you're using Filebeat on your endpoint(s), you'll just add that path to your beats configuration.

If you aren't familiar with Graylog's sidecar, I won't reinvent the wheel--their documentation goes into plenty of detail about how to get you up and running in minutes. Just make sure to aim it at the log file specified in config.py.

Pro tip: any additional data sources you add to your Graylog environment should be assigned an event type, or similar, to help differentiate it and identify it coming in. In our case, our collector configuration assigns a static field event_type as cylance.

Normalizing logs in Graylog

Now that logs are shipping, you'll want to parse the data.

Cylance logs are pretty straight forward, and in our case, they're in nicely formatted JSON.

Sample threat object:

{
  "name": "threatfile.exe",
  "sha256": "bf17366ee3bb8068a9ad70fc9e68496e7e311a055bf4ffeeff53cc5d29ccce52",
  "md5": "d41d8cd98f00b204e9800998ecf8427e",
  "signed": true,
  "cylance_score": -1,
  "av_industry": null,
  "classification": "PUP",
  "sub_classification": "Generic",
  "global_quarantine": false,
  "safelisted": false,
  "cert_publisher": "Publisher Name",
  "cert_issuer": "Issuer Name",
  "cert_timestamp": "0001-01-01T00:00:00",
  "file_size": 1500000,
  "unique_to_cylance": false,
  "running": false,
  "auto_run": false,
  "detected_by": "Background Threat Detection"
}

Sample detection object:

{
  "PhoneticId": "47E7-0635",
  "Status": "New",
  "Id": "47e70635-b10a-4bde-9ffe-b0258f656a1e",
  "Severity": "Medium",
  "OccurrenceTime": "2019-05-10T22:00:54.289Z",
  "ReceivedTime": "2019-05-10T22:00:56.000Z",
  "Device": {
    "Name": "e378dacb-9324-453a-b8c6-5a8406952195",
    "CylanceId": "User-Laptop-A123"
  },
  "DetectionDescription": "Fileless Powershell Malware"
}

Simple, yes. But there are fields you may want to drop or rename or enrich later on down the line, depending on your use case or environment.

For this, we use Graylog's pipeline rules. And that event_type field I mentioned.

We're going to use Graylog's parse_json function to split out the fields, and then normalize/rename as necessary.

Here is a basic example that splits up the data into cylance_* fields with a few of them normalized:

rule "normalize_cylance"
when
  has_field("event_type") AND contains(to_string($message.event_type), "cylance")
then

  let json = parse_json(to_string($message.message));
  set_fields(fields: to_map(json), prefix: "cylance_");

  // normalize field names
  rename_field("cylance_md5", "md5");
  rename_field("cylance_sha256", "sha256");
  rename_field("cylance_name", "file_name");

end

Next Steps

Graylog has plenty of features that you can integrate now that you have this data available.

For example, lookup tables that check the hashes against other threat intelligence APIs. You could set up alerts when a cylance_score over a certain value shows up. Dashboards that display the last 24 hours of detections, etc.

But wait! There's more!

If you want to learn more about Graylog, their documentation is fantastic.

We thrive on giving back to a community that has provided us with so much of what we use and rely on. Everything we've open sourced can be found in our GitHub, @ReconInfoSec.

Show Comments