diff --git a/.isort.cfg b/.isort.cfg index 327b5b08..c85fc637 100644 --- a/.isort.cfg +++ b/.isort.cfg @@ -1,2 +1,2 @@ [settings] -known_third_party = Queue,alerta,alerta_azuremonitor,alerta_msteamswebhook,alerta_sentry,alerta_slack,alertaclient,boto,cachetclient,consul,dateutil,dingtalkchatbot,flask,google,influxdb,jinja2,kombu,mailer,matterhook,mock,op5,pymsteams,pytest,pyzabbix,requests,settings,setuptools,telepot,twilio,yaml +known_third_party = Queue,alerta,alerta_azuremonitor,alerta_falco,alerta_msteamswebhook,alerta_sentry,alerta_slack,alertaclient,boto,cachetclient,consul,dateutil,dingtalkchatbot,flask,google,influxdb,jinja2,kombu,mailer,matterhook,mock,op5,pymsteams,pytest,pyzabbix,requests,settings,setuptools,telepot,twilio,yaml diff --git a/README.md b/README.md index aec5d88e..d0d8a25f 100644 --- a/README.md +++ b/README.md @@ -82,6 +82,7 @@ Webhooks * [Slack](https://github.com/alerta/alerta/blob/master/alerta/webhooks/slack.py) * [Stackdriver](https://github.com/alerta/alerta/blob/master/alerta/webhooks/stackdriver.py) * [Telegram](https://github.com/alerta/alerta/blob/master/alerta/webhooks/telegram.py) + * [Falco](webhooks/falco) Tests ----- diff --git a/webhooks/falco/README.md b/webhooks/falco/README.md new file mode 100644 index 00000000..f52d384e --- /dev/null +++ b/webhooks/falco/README.md @@ -0,0 +1,95 @@ +Falco Webhook +============== + +Receive [falco](https://falco.org/) alerts via `falcosidekick` webhook. + +For help, join [![Slack chat](https://img.shields.io/badge/chat-on%20slack-blue?logo=slack)](https://slack.alerta.dev) + +Falco webhook version support +------------------------------- + +[TBD] + +Installation +------------ + +Clone the GitHub repo and run: + + $ python setup.py install + +Or, to install remotely from GitHub run: + + $ pip install git+https://github.com/alerta/alerta-contrib.git#subdirectory=webhooks/falco + +Note: If Alerta is installed in a python virtual environment then plugins +need to be installed into the same environment for Alerta to dynamically +discover them. + +Configuration +------------- + +`falcosidekick` custom outputs are used here. + +Specifically, the `webhook` output. Read more [here](https://github.com/falcosecurity/falcosidekick/blob/master/docs/outputs/webhook.md). + +First, an Alerta Api Key has to be set. It can be created in the Alerta's UI, under "Api Keys" menu. + +Then, note a custom field is being used to identify environments. This is set in the installation with `customfields="environment:Development"`. Set the right environment for your Falco deployment here. + +Finally, if you are using Helm to install Falco, the webhook can be set like this: + +``` shell +helm install falco -n falco --set driver.kind=modern_ebpf --set tty=true falcosecurity/falco \ +--set falcosidekick.enabled=true \ +--set falcosidekick.config.webhook.address=":///api/webhooks/falco" \ +--set falcosidekick.config.webhook.minimumpriority=notice \ +--set falcosidekick.config.webhook.customHeaders="X-Api-Key: " \ +--set falcosidekick.config.webhook.mutualtls=false \ +--set falcosidekick.config.webhook.checkcert=false \ +--set falcosidekick.config.customfields="environment:Development" +``` + +FalcoSidekick payload example +----------------------------- + +This is an example of a payload sent by `falcosidekick`: + +``` json + { + "uuid": "06f73663-b1d4-42e4-b236-eacbd2b96998", + "output": "20:39:17.500016161: Notice A shell was spawned in a container with an attached terminal (evt_type=execve user=root user_uid=0 user_loginuid=-1 process=sh proc_exepath=/bin/busybox parent=containerd-shim command=sh -c uptime terminal=34816 exe_flags=EXE_WRITABLE container_id=9273d0110c4e container_image= container_image_tag= container_name= k8s_ns= k8s_pod_name=)", + "priority": "Notice", + "rule": "Terminal shell in container", + "time": "2024-07-16T20:39:17.500016161Z", + "output_fields": { + "container.id": "9273d0110c4e", + "container.image.repository": "None", + "container.image.tag": "None", + "container.name": "None", + "evt.arg.flags": "EXE_WRITABLE", + "evt.time": 1721162357500016161, + "evt.type": "execve", + "k8s.ns.name": "None", + "k8s.pod.name": "None", + "proc.cmdline": "sh -c uptime", + "proc.exepath": "/bin/busybox", + "proc.name": "sh", + "proc.pname": "containerd-shim", + "proc.tty": 34816, + "user": "jdelacamara", + "user.loginuid": -1, + "user.name": "root", + "user.uid": 0, + "environment": "Development" + }, + "source": "syscall", + "tags": [ + "T1059", + "container", + "maturity_stable", + "mitre_execution", + "shell" + ], + "hostname": "nixos" + } +``` diff --git a/webhooks/falco/alerta_falco.py b/webhooks/falco/alerta_falco.py new file mode 100644 index 00000000..9188cde7 --- /dev/null +++ b/webhooks/falco/alerta_falco.py @@ -0,0 +1,127 @@ +from flask import current_app +from alerta.app import alarm_model +from alerta.models.alert import Alert +from alerta.webhooks import WebhookBase + + +class FalcoWebhook(WebhookBase): + """ + Falco webhook + """ + + def incoming(self, query_string, payload): + + additional_tags = [] + additional_attributes = {} + + # checking fields + # + expected_fields = [ + 'priority', + 'hostname', + 'rule', + 'output_fields', + 'source', + 'output' + ] + for field in expected_fields: + if field not in payload: + raise Exception(f'{field} not found in payload') + expected_fields_in_outputfields = ['environment'] + for field in expected_fields_in_outputfields: + if field not in payload['output_fields']: + raise Exception(f'{field} not found in payload->output_fields') + + # resource+event + # + # these are the fields used for de duplication, + # so we fill their values here + resource = f"{payload['hostname']}" + event = payload['rule'] + + # priority + # + # falco priorities: + # emergency, alert, critical, error, warning, notice, + # informational, debug + if payload['priority'].lower() in [ + 'emergency', + 'alert', + 'critical', + 'error' + ]: + severity = 'critical' + elif payload['priority'].lower() in [ + 'warning', + 'notice', + 'informational', + 'debug' + ]: + severity = 'warning' + else: + severity = alarm_model.DEFAULT_NORMAL_SEVERITY + additional_attributes['falco_priority'] = payload['priority'] + + # environment + # + # we set a custom field environment in our setup + environment = current_app.config['DEFAULT_ENVIRONMENT'] + if 'output_fields' in payload and 'environment' in payload['output_fields']: + environment = payload['output_fields']['environment'] + + # attributes + # + attributes = additional_attributes + + # tags + tags = [] + if 'tags' in payload and isinstance(payload['tags'], list): + tags = additional_tags.extend(payload['tags']) + else: + tags = additional_tags + + # group + # + # how to group + group = f"{payload['rule']}-{payload['source']}" + + # value + # + value = payload['output'] + + # service + # + # service is a List + service = [payload['source']] + + # origin + # + origin = f"{payload['hostname']}-{payload['source']}" + + # event type + # + # in this case is a Falco Alert + event_type = 'falcoAlert' + + # text + # + # the alert text + text = f"{severity}: {payload['output_fields']}" + + return Alert( + # alerta will group by these + resource=resource, + event=event, + # ################ + environment=environment, + severity=severity, + service=service, + group=group, + value=value, + text=text, + tags=tags, + origin=origin, + attributes=attributes, + event_type=event_type, + raw_data=payload + ) diff --git a/webhooks/falco/setup.py b/webhooks/falco/setup.py new file mode 100644 index 00000000..d5bfceec --- /dev/null +++ b/webhooks/falco/setup.py @@ -0,0 +1,24 @@ +from setuptools import find_packages, setup + +version = '0.0.4' + +setup( + name='alerta-falco', + version=version, + description='Alerta webhook for Falco', + url='https://github.com/alerta/alerta-contrib', + license='MIT', + author='Juan Kungfoo @ binbash', + author_email='juan.delacamara@binbash.com.ar', + packages=find_packages(), + py_modules=['alerta_falco'], + install_requires=[ + ], + include_package_data=True, + zip_safe=True, + entry_points={ + 'alerta.webhooks': [ + 'falco = alerta_falco:FalcoWebhook' + ] + } +) diff --git a/webhooks/falco/test_falco.py b/webhooks/falco/test_falco.py new file mode 100644 index 00000000..6a70ea9c --- /dev/null +++ b/webhooks/falco/test_falco.py @@ -0,0 +1,117 @@ +import json +import unittest + +import alerta_falco +from alerta.app import create_app, custom_webhooks + + +class FalcoWebhookTestCase(unittest.TestCase): + + def setUp(self): + + test_config = { + 'TESTING': True, + 'AUTH_REQUIRED': False + } + self.app = create_app(test_config) + self.client = self.app.test_client() + custom_webhooks.webhooks['falco'] = alerta_falco.FalcoWebhook( + ) + + self.headers = { + 'Content-Type': 'application/json' + } + + self.alert_id = 'f0c55228-c61d-462a-9aeb-f6048d37fdf6' + + def test_falcowebhook(self): + + payload_cmd = r""" + { + "uuid": "06f73663-b1d4-42e4-b236-eacbd2b96998", + "output": "20:39:17.500016161: Notice A shell was spawned in a container with an attached terminal (evt_type=execve user=root user_uid=0 user_loginuid=-1 process=sh proc_exepath=/bin/busybox parent=containerd-shim command=sh -c uptime terminal=34816 exe_flags=EXE_WRITABLE container_id=9273d0110c4e container_image= container_image_tag= container_name= k8s_ns= k8s_pod_name=)", + "priority": "Notice", + "rule": "Terminal shell in container", + "time": "2024-07-16T20:39:17.500016161Z", + "output_fields": { + "container.id": "9273d0110c4e", + "container.image.repository": "None", + "container.image.tag": "None", + "container.name": "None", + "evt.arg.flags": "EXE_WRITABLE", + "evt.time": 1721162357500016161, + "evt.type": "execve", + "k8s.ns.name": "None", + "k8s.pod.name": "None", + "proc.cmdline": "sh -c uptime", + "proc.exepath": "/bin/busybox", + "proc.name": "sh", + "proc.pname": "containerd-shim", + "proc.tty": 34816, + "user": "jdelacamara", + "user.loginuid": -1, + "user.name": "root", + "user.uid": 0, + "environment": "Development" + }, + "source": "syscall", + "tags": [ + "T1059", + "container", + "maturity_stable", + "mitre_execution", + "shell" + ], + "hostname": "nixos" + } + """ + + # Missing fields + payload_invalidcmd = r""" + { + "uuid": "06f73663-b1d4-42e4-b236-eacbd2b96998", + "time": "2024-07-16T20:39:17.500016161Z", + "output_fields": { + "container.id": "9273d0110c4e", + "container.image.repository": "None", + "container.image.tag": "None", + "container.name": "None", + "evt.arg.flags": "EXE_WRITABLE", + "evt.time": 1721162357500016161, + "evt.type": "execve", + "k8s.ns.name": "None", + "k8s.pod.name": "None", + "proc.cmdline": "sh -c uptime", + "proc.exepath": "/bin/busybox", + "proc.name": "sh", + "proc.pname": "containerd-shim", + "proc.tty": 34816, + "user": "jdelacamara", + "user.loginuid": -1, + "user.name": "root", + "user.uid": 0, + "environment": "Development" + }, + "source": "syscall", + "tags": [ + "T1059", + "container", + "maturity_stable", + "mitre_execution", + "shell" + ], + "hostname": "nixos" + } + """ + + # ack with missing fields + response = self.client.post('/webhooks/falco', data=payload_invalidcmd, + content_type='application/json', headers=self.headers) + self.assertEqual(response.status_code, 500) + data = json.loads(response.data.decode('utf-8')) + + # ack + response = self.client.post( + '/webhooks/falco', data=payload_cmd, content_type='application/json', headers=self.headers) + self.assertEqual(response.status_code, 201) + data = json.loads(response.data.decode('utf-8'))