From f746c4cd5760de2154758f2f68eff89a4bbefbd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?N=C3=A9stor=20Salceda?= Date: Wed, 10 Oct 2018 19:28:35 +0200 Subject: [PATCH] Add a integration with Demisto (#408) * Create a DemistoClient for publishing Falco alerts to Demisto * Extract a function for extracting description from Falco output * Add a playbook which creates a Falco alert as a Demisto incident * Add a Kubeless Demisto Handler for Demisto integration * Document the integration with Demisto * Allow changing SSL certificate verification * Fix naming for playbook specs * Call to lower() before checking value of VERIFY_SSL. Allow case insensitive. --- .../playbooks/README.md | 16 +++++ .../playbooks/demisto.py | 22 ++++++ .../playbooks/playbooks/__init__.py | 52 ++++++++++++-- .../playbooks/playbooks/infrastructure.py | 24 +++++++ .../infrastructure/demisto_client_spec.py | 32 +++++++++ ...reate_incident_in_demisto_playbook_spec.py | 70 +++++++++++++++++++ 6 files changed, 209 insertions(+), 7 deletions(-) create mode 100644 integrations/kubernetes-response-engine/playbooks/demisto.py create mode 100644 integrations/kubernetes-response-engine/playbooks/specs/infrastructure/demisto_client_spec.py create mode 100644 integrations/kubernetes-response-engine/playbooks/specs/playbooks/create_incident_in_demisto_playbook_spec.py diff --git a/integrations/kubernetes-response-engine/playbooks/README.md b/integrations/kubernetes-response-engine/playbooks/README.md index e3868501..951a8b04 100644 --- a/integrations/kubernetes-response-engine/playbooks/README.md +++ b/integrations/kubernetes-response-engine/playbooks/README.md @@ -162,3 +162,19 @@ Kubernetes. So as soon as we notice someone wrote under /bin (and additional binaries) or /etc, we disconnect that pod. It's like a trap for our attackers. + +### Create an incident in Demisto + +This playbook creates an incident in Demisto + +``` +./deploy_playbook -p demisto -t "falco.*.*" -e DEMISTO_API_KEY=XxXxxXxxXXXx -e DEMISTO_BASE_URL=https://..." +``` + +#### Parameters + +* DEMISTO_API_KEY: This is the API key used for authenticating against Demisto. Create one under settings -> API keys +* DEMISTO_BASE_URL: This is the base URL where your Demisto server lives on. Ensure there's no trailing slash. +* VERIFY_SSL: Verify SSL certificates for HTTPS requests. By default is enabled. + +In this example, when Falco raises any kind of alert, the alert will be created in Demisto diff --git a/integrations/kubernetes-response-engine/playbooks/demisto.py b/integrations/kubernetes-response-engine/playbooks/demisto.py new file mode 100644 index 00000000..4262ba41 --- /dev/null +++ b/integrations/kubernetes-response-engine/playbooks/demisto.py @@ -0,0 +1,22 @@ +import sys +import os.path +sys.path.append(os.path.join(os.path.abspath(os.path.dirname(__file__)))) + +import os +import playbooks +from playbooks import infrastructure + + +def _to_bool(value): + return value.lower() in ('yes', 'true', '1') + + +playbook = playbooks.CreateIncidentInDemisto( + infrastructure.DemistoClient(os.environ['DEMISTO_API_KEY'], + os.environ['DEMISTO_BASE_URL'] + verify_ssl=_to_bool(os.environ.get('VERIFY_SSL', 'True'))) +) + + +def handler(event, context): + playbook.run(event['data']) diff --git a/integrations/kubernetes-response-engine/playbooks/playbooks/__init__.py b/integrations/kubernetes-response-engine/playbooks/playbooks/__init__.py index 0259b9e5..210da6ff 100644 --- a/integrations/kubernetes-response-engine/playbooks/playbooks/__init__.py +++ b/integrations/kubernetes-response-engine/playbooks/playbooks/__init__.py @@ -23,7 +23,7 @@ class AddMessageToSlack: def _build_slack_message(self, alert): return { - 'text': self._output(alert), + 'text': _output_from_alert(alert), 'attachments': [{ 'color': self._color_from(alert['priority']), 'fields': [ @@ -56,12 +56,6 @@ class AddMessageToSlack: }] } - def _output(self, alert): - output = alert['output'].split(': ')[1] - priority_plus_whitespace_length = len(alert['priority']) + 1 - - return output[priority_plus_whitespace_length:] - _COLORS = { 'Emergency': '#b12737', 'Alert': '#f24141', @@ -77,6 +71,13 @@ class AddMessageToSlack: return self._COLORS.get(priority, '#eeeeee') +def _output_from_alert(alert): + output = alert['output'].split(': ')[1] + priority_plus_whitespace_length = len(alert['priority']) + 1 + + return output[priority_plus_whitespace_length:] + + class TaintNode: def __init__(self, k8s_client, key, value, effect): self._k8s_client = k8s_client @@ -99,3 +100,40 @@ class NetworkIsolatePod: pod = alert['output_fields']['k8s.pod.name'] self._k8s_client.add_label_to_pod(pod, 'isolated', 'true') + + +class CreateIncidentInDemisto: + def __init__(self, demisto_client): + self._demisto_client = demisto_client + + def run(self, alert): + incident = { + 'type': 'Policy Violation', + 'name': alert['rule'], + 'details': _output_from_alert(alert), + 'severity': self._severity_from(alert['priority']), + 'occurred': alert['time'], + 'labels': [ + {'type': 'Brand', 'value': 'Sysdig'}, + {'type': 'Application', 'value': 'Falco'}, + {'type': 'container.id', 'value': alert['output_fields']['container.id']}, + {'type': 'k8s.pod.name', 'value': alert['output_fields']['k8s.pod.name']} + ] + } + self._demisto_client.create_incident(incident) + + return incident + + def _severity_from(self, priority): + return self._SEVERITIES.get(priority, 0) + + _SEVERITIES = { + 'Emergency': 4, + 'Alert': 4, + 'Critical': 4, + 'Error': 3, + 'Warning': 2, + 'Notice': 1, + 'Informational': 5, + 'Debug': 5, + } diff --git a/integrations/kubernetes-response-engine/playbooks/playbooks/infrastructure.py b/integrations/kubernetes-response-engine/playbooks/playbooks/infrastructure.py index 5f485169..75ed3c98 100644 --- a/integrations/kubernetes-response-engine/playbooks/playbooks/infrastructure.py +++ b/integrations/kubernetes-response-engine/playbooks/playbooks/infrastructure.py @@ -1,5 +1,6 @@ import os import json +import http from kubernetes import client, config import requests @@ -72,3 +73,26 @@ class SlackClient: def post_message(self, message): requests.post(self._slack_webhook_url, data=json.dumps(message)) + + +class DemistoClient: + def __init__(self, api_key, base_url, verify_ssl=True): + self._api_key = api_key + self._base_url = base_url + self._verify_ssl = verify_ssl + + def create_incident(self, incident): + response = requests.post(self._base_url + '/incident', + headers=self._headers(), + data=json.dumps(incident), + verify=self._verify_ssl) + + if response.status_code != http.HTTPStatus.CREATED: + raise RuntimeError(response.text) + + def _headers(self): + return { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'Authorization': self._api_key, + } diff --git a/integrations/kubernetes-response-engine/playbooks/specs/infrastructure/demisto_client_spec.py b/integrations/kubernetes-response-engine/playbooks/specs/infrastructure/demisto_client_spec.py new file mode 100644 index 00000000..25a6ffb6 --- /dev/null +++ b/integrations/kubernetes-response-engine/playbooks/specs/infrastructure/demisto_client_spec.py @@ -0,0 +1,32 @@ +from mamba import description, it, context, before +from expects import expect, raise_error + +import os + +from playbooks import infrastructure + + +with description(infrastructure.DemistoClient) as self: + with before.each: + self.demisto_client = infrastructure.DemistoClient( + os.environ['DEMISTO_API_KEY'], + os.environ['DEMISTO_BASE_URL'], + verify_ssl=False + ) + + with it('creates an incident'): + incident = { + "type": "Policy Violation", + "name": "Falco incident", + "severity": 2, + "details": "Some incident details" + } + + self.demisto_client.create_incident(incident) + + with context('when an error happens'): + with it('raises an exception'): + incident = {} + + expect(lambda: self.demisto_client.create_incident(incident)).\ + to(raise_error(RuntimeError)) diff --git a/integrations/kubernetes-response-engine/playbooks/specs/playbooks/create_incident_in_demisto_playbook_spec.py b/integrations/kubernetes-response-engine/playbooks/specs/playbooks/create_incident_in_demisto_playbook_spec.py new file mode 100644 index 00000000..41a157d7 --- /dev/null +++ b/integrations/kubernetes-response-engine/playbooks/specs/playbooks/create_incident_in_demisto_playbook_spec.py @@ -0,0 +1,70 @@ +from mamba import description, it, before, context +from expects import expect, have_key, have_keys, contain + +from doublex import Spy +from doublex_expects import have_been_called_with + +from playbooks import infrastructure +import playbooks + +import os + + +with description(playbooks.CreateIncidentInDemisto) as self: + with before.each: + self.demisto_client = Spy(infrastructure.DemistoClient) + self.playbook = playbooks.CreateIncidentInDemisto(self.demisto_client) + + with context('when publishing a message to slack'): + with before.each: + self.alert = { + "output": "10:22:15.576767292: Notice Unexpected setuid call by non-sudo, non-root program (user=bin cur_uid=2 parent=event_generator command=event_generator uid=root) k8s.pod=falco-event-generator-6fd89678f9-cdkvz container=1c76f49f40b4", + "output_fields": { + "container.id": "1c76f49f40b4", + "evt.arg.uid": "root", + "evt.time": 1527157335576767292, + "k8s.pod.name": "falco-event-generator-6fd89678f9-cdkvz", + "proc.cmdline": "event_generator ", + "proc.pname": "event_generator", + "user.name": "bin", + "user.uid": 2 + }, + "priority": "Notice", + "rule": "Non sudo setuid", + "time": "2018-05-24T10:22:15.576767292Z" + } + + self.incident = self.playbook.run(self.alert) + + with it('creates incident in demisto'): + expect(self.demisto_client.create_incident).to(have_been_called_with(self.incident)) + + with it('sets incident type as Policy Violation'): + expect(self.incident).to(have_key('type', 'Policy Violation')) + + with it('includes rule name'): + expect(self.incident).to(have_key('name', 'Non sudo setuid')) + + with it('includes falco output'): + falco_output = 'Unexpected setuid call by non-sudo, non-root program (user=bin cur_uid=2 parent=event_generator command=event_generator uid=root) k8s.pod=falco-event-generator-6fd89678f9-cdkvz container=1c76f49f40b4' + + expect(self.incident).to(have_key('details', falco_output)) + + with it('includes severity'): + expect(self.incident).to(have_key('severity', 1)) + + with it('includes time when alert happened'): + expect(self.incident).to(have_key('occurred', "2018-05-24T10:22:15.576767292Z")) + + with context('when adding labels'): + with it('includes Sysdig as Brand'): + expect(self.incident['labels']).to(contain(have_keys(type='Brand', value='Sysdig'))) + + with it('includes Falco as Application'): + expect(self.incident['labels']).to(contain(have_keys(type='Application', value='Falco'))) + + with it('includes container.id'): + expect(self.incident['labels']).to(contain(have_keys(type='container.id', value='1c76f49f40b4'))) + + with it('includes k8s.pod.name'): + expect(self.incident['labels']).to(contain(have_keys(type='k8s.pod.name', value='falco-event-generator-6fd89678f9-cdkvz')))