mirror of
https://github.com/falcosecurity/falco.git
synced 2025-07-31 22:16:49 +00:00
Add a playbook which starts to capturing data using Sysdig and uploads capture to a s3 bucket (#414)
* Fix spec name * Add a playbook for capturing stuff using sysdig in a container * Add event-name to job name for avoid collisions among captures * Implement job for starting container in Pod in Kubernetes Client We are going to pick data for all Pod, not limited to one container * Use sysdig/capturer image for capture and upload to s3 the capture * There is a bug with environment string splitting in kubeless https://github.com/kubeless/kubeless/issues/824 So here is a workaround which uses multiple --env flags, one for each environment. * Use shorter job name. Kubernetes limit is 64 characters. * Add a deployable playbook with Kubeless for capturing stuff with Sysdig * Document the integration with Sysdig capture * Add Dockerfile for creating sysdig-capturer
This commit is contained in:
parent
f746c4cd57
commit
e4ffa55d58
@ -178,3 +178,24 @@ This playbook creates an incident in Demisto
|
|||||||
* VERIFY_SSL: Verify SSL certificates for HTTPS requests. By default is enabled.
|
* 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
|
In this example, when Falco raises any kind of alert, the alert will be created in Demisto
|
||||||
|
|
||||||
|
### Start a capture using Sysdig
|
||||||
|
|
||||||
|
This playbook starts to capture information about pod using sysdig and uploads
|
||||||
|
to a s3 bucket.
|
||||||
|
|
||||||
|
```
|
||||||
|
$ ./deploy_playbook -p capture -e CAPTURE_DURATION=300 -e AWS_S3_BUCKET=s3://xxxxxxx -e AWS_ACCESS_KEY_ID=xxxxXXXxxXXxXX -e AWS_SECRET_ACCESS_KEY=xxXxXXxxxxXXX -t "falco.notice.terminal_shell_in_container"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Parameters:
|
||||||
|
* CAPTURE_DURATION: Captures data for this duration in seconds. By default is
|
||||||
|
120 seconds (2 minutes)
|
||||||
|
* AWS_S3_BUCKET: This is the bucket where data is going to be uploaded. Jobs
|
||||||
|
starts with sysdig- prefix and contain pod name and time where event starts.
|
||||||
|
* AWS_ACCESS_KEY_ID: This is the Amazon access key id.
|
||||||
|
* AWS_SECRET_ACCESS_KEY: This is the Amazon secret access key.
|
||||||
|
|
||||||
|
In this example, when we detect a shell in a container, we start to collect data
|
||||||
|
for 300 seconds. This playbook requires permissions for creating a new pod from
|
||||||
|
a Kubeless function.
|
||||||
|
20
integrations/kubernetes-response-engine/playbooks/capture.py
Normal file
20
integrations/kubernetes-response-engine/playbooks/capture.py
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
playbook = playbooks.StartSysdigCaptureForContainer(
|
||||||
|
infrastructure.KubernetesClient(),
|
||||||
|
int(os.environ.get('CAPTURE_DURATION', 120)),
|
||||||
|
os.environ['AWS_S3_BUCKET'],
|
||||||
|
os.environ['AWS_ACCESS_KEY_ID'],
|
||||||
|
os.environ['AWS_SECRET_ACCESS_KEY']
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def handler(event, context):
|
||||||
|
playbook.run(event['data'])
|
@ -21,7 +21,11 @@ EOF
|
|||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
function join { local IFS="$1"; shift; echo "$*"; }
|
function create_environment_flags {
|
||||||
|
for env in ${environment[*]}; do
|
||||||
|
echo "--env ${env} "
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
playbook=""
|
playbook=""
|
||||||
environment=()
|
environment=()
|
||||||
@ -54,7 +58,7 @@ zip "${playbook}".zip -r playbooks/*.py "${playbook}".py
|
|||||||
|
|
||||||
kubeless function deploy --from-file "${playbook}".zip \
|
kubeless function deploy --from-file "${playbook}".zip \
|
||||||
--dependencies requirements.txt \
|
--dependencies requirements.txt \
|
||||||
--env "$(join , ${environment[*]})" \
|
$(create_environment_flags ${environment[*]}) \
|
||||||
--runtime python3.6 \
|
--runtime python3.6 \
|
||||||
--handler "${playbook}".handler \
|
--handler "${playbook}".handler \
|
||||||
falco-"${playbook}"
|
falco-"${playbook}"
|
||||||
|
@ -137,3 +137,24 @@ class CreateIncidentInDemisto:
|
|||||||
'Informational': 5,
|
'Informational': 5,
|
||||||
'Debug': 5,
|
'Debug': 5,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class StartSysdigCaptureForContainer:
|
||||||
|
def __init__(self, k8s_client, duration_in_seconds, s3_bucket,
|
||||||
|
aws_access_key_id, aws_secret_access_key):
|
||||||
|
self._k8s_client = k8s_client
|
||||||
|
self._duration_in_seconds = duration_in_seconds
|
||||||
|
self._s3_bucket = s3_bucket
|
||||||
|
self._aws_access_key_id = aws_access_key_id
|
||||||
|
self._aws_secret_access_key = aws_secret_access_key
|
||||||
|
|
||||||
|
def run(self, alert):
|
||||||
|
pod = alert['output_fields']['k8s.pod.name']
|
||||||
|
event_time = alert['output_fields']['evt.time']
|
||||||
|
|
||||||
|
self._k8s_client.start_sysdig_capture_for(pod,
|
||||||
|
event_time,
|
||||||
|
self._duration_in_seconds,
|
||||||
|
self._s3_bucket,
|
||||||
|
self._aws_access_key_id,
|
||||||
|
self._aws_secret_access_key)
|
||||||
|
@ -14,6 +14,7 @@ class KubernetesClient:
|
|||||||
config.load_incluster_config()
|
config.load_incluster_config()
|
||||||
|
|
||||||
self._v1 = client.CoreV1Api()
|
self._v1 = client.CoreV1Api()
|
||||||
|
self._batch_v1 = client.BatchV1Api()
|
||||||
|
|
||||||
def delete_pod(self, name):
|
def delete_pod(self, name):
|
||||||
namespace = self._find_pod_namespace(name)
|
namespace = self._find_pod_namespace(name)
|
||||||
@ -65,6 +66,152 @@ class KubernetesClient:
|
|||||||
|
|
||||||
return self._v1.patch_namespaced_pod(name, namespace, body)
|
return self._v1.patch_namespaced_pod(name, namespace, body)
|
||||||
|
|
||||||
|
def start_sysdig_capture_for(self, pod_name, event_time,
|
||||||
|
duration_in_seconds, s3_bucket,
|
||||||
|
aws_access_key_id, aws_secret_access_key):
|
||||||
|
job_name = 'sysdig-{}-{}'.format(pod_name, event_time)
|
||||||
|
|
||||||
|
node_name = self.find_node_running_pod(pod_name)
|
||||||
|
namespace = self._find_pod_namespace(pod_name)
|
||||||
|
body = self._build_sysdig_capture_job_body(job_name,
|
||||||
|
node_name,
|
||||||
|
duration_in_seconds,
|
||||||
|
s3_bucket,
|
||||||
|
aws_access_key_id,
|
||||||
|
aws_secret_access_key)
|
||||||
|
|
||||||
|
return self._batch_v1.create_namespaced_job(namespace, body)
|
||||||
|
|
||||||
|
def _build_sysdig_capture_job_body(self, job_name, node_name,
|
||||||
|
duration_in_seconds, s3_bucket,
|
||||||
|
aws_access_key_id, aws_secret_access_key):
|
||||||
|
return client.V1Job(
|
||||||
|
metadata=client.V1ObjectMeta(
|
||||||
|
name=job_name
|
||||||
|
),
|
||||||
|
spec=client.V1JobSpec(
|
||||||
|
template=client.V1PodTemplateSpec(
|
||||||
|
metadata=client.V1ObjectMeta(
|
||||||
|
name=job_name
|
||||||
|
),
|
||||||
|
spec=client.V1PodSpec(
|
||||||
|
containers=[client.V1Container(
|
||||||
|
name='capturer',
|
||||||
|
image='sysdig/capturer',
|
||||||
|
image_pull_policy='Always',
|
||||||
|
security_context=client.V1SecurityContext(
|
||||||
|
privileged=True
|
||||||
|
),
|
||||||
|
env=[
|
||||||
|
client.V1EnvVar(
|
||||||
|
name='AWS_S3_BUCKET',
|
||||||
|
value=s3_bucket
|
||||||
|
),
|
||||||
|
client.V1EnvVar(
|
||||||
|
name='CAPTURE_DURATION',
|
||||||
|
value=str(duration_in_seconds)
|
||||||
|
),
|
||||||
|
client.V1EnvVar(
|
||||||
|
name='CAPTURE_FILE_NAME',
|
||||||
|
value=job_name
|
||||||
|
),
|
||||||
|
client.V1EnvVar(
|
||||||
|
name='AWS_ACCESS_KEY_ID',
|
||||||
|
value=aws_access_key_id,
|
||||||
|
),
|
||||||
|
client.V1EnvVar(
|
||||||
|
name='AWS_SECRET_ACCESS_KEY',
|
||||||
|
value=aws_secret_access_key,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
volume_mounts=[
|
||||||
|
client.V1VolumeMount(
|
||||||
|
mount_path='/host/var/run/docker.sock',
|
||||||
|
name='docker-socket'
|
||||||
|
),
|
||||||
|
client.V1VolumeMount(
|
||||||
|
mount_path='/host/dev',
|
||||||
|
name='dev-fs'
|
||||||
|
),
|
||||||
|
client.V1VolumeMount(
|
||||||
|
mount_path='/host/proc',
|
||||||
|
name='proc-fs',
|
||||||
|
read_only=True
|
||||||
|
),
|
||||||
|
client.V1VolumeMount(
|
||||||
|
mount_path='/host/boot',
|
||||||
|
name='boot-fs',
|
||||||
|
read_only=True
|
||||||
|
),
|
||||||
|
client.V1VolumeMount(
|
||||||
|
mount_path='/host/lib/modules',
|
||||||
|
name='lib-modules',
|
||||||
|
read_only=True
|
||||||
|
),
|
||||||
|
client.V1VolumeMount(
|
||||||
|
mount_path='/host/usr',
|
||||||
|
name='usr-fs',
|
||||||
|
read_only=True
|
||||||
|
),
|
||||||
|
client.V1VolumeMount(
|
||||||
|
mount_path='/dev/shm',
|
||||||
|
name='dshm'
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)],
|
||||||
|
volumes=[
|
||||||
|
client.V1Volume(
|
||||||
|
name='dshm',
|
||||||
|
empty_dir=client.V1EmptyDirVolumeSource(
|
||||||
|
medium='Memory'
|
||||||
|
)
|
||||||
|
),
|
||||||
|
client.V1Volume(
|
||||||
|
name='docker-socket',
|
||||||
|
host_path=client.V1HostPathVolumeSource(
|
||||||
|
path='/var/run/docker.sock'
|
||||||
|
)
|
||||||
|
),
|
||||||
|
client.V1Volume(
|
||||||
|
name='dev-fs',
|
||||||
|
host_path=client.V1HostPathVolumeSource(
|
||||||
|
|
||||||
|
path='/dev'
|
||||||
|
)
|
||||||
|
),
|
||||||
|
client.V1Volume(
|
||||||
|
name='proc-fs',
|
||||||
|
host_path=client.V1HostPathVolumeSource(
|
||||||
|
path='/proc'
|
||||||
|
)
|
||||||
|
),
|
||||||
|
|
||||||
|
client.V1Volume(
|
||||||
|
name='boot-fs',
|
||||||
|
host_path=client.V1HostPathVolumeSource(
|
||||||
|
path='/boot'
|
||||||
|
)
|
||||||
|
),
|
||||||
|
client.V1Volume(
|
||||||
|
name='lib-modules',
|
||||||
|
host_path=client.V1HostPathVolumeSource(
|
||||||
|
path='/lib/modules'
|
||||||
|
)
|
||||||
|
),
|
||||||
|
client.V1Volume(
|
||||||
|
name='usr-fs',
|
||||||
|
host_path=client.V1HostPathVolumeSource(
|
||||||
|
path='/usr'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
],
|
||||||
|
node_name=node_name,
|
||||||
|
restart_policy='Never'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class SlackClient:
|
class SlackClient:
|
||||||
def __init__(self, slack_webhook_url):
|
def __init__(self, slack_webhook_url):
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
from mamba import description, context, it, before
|
from mamba import description, context, it, before
|
||||||
from expects import expect, be_false, be_true, start_with, equal, have_key
|
from expects import expect, be_false, be_true, start_with, equal, have_key, be_none
|
||||||
|
|
||||||
import subprocess
|
import subprocess
|
||||||
import os.path
|
import os.path
|
||||||
|
import time
|
||||||
|
|
||||||
from playbooks import infrastructure
|
from playbooks import infrastructure
|
||||||
|
|
||||||
@ -46,7 +47,7 @@ with description(infrastructure.KubernetesClient) as self:
|
|||||||
expect(node.spec.taints[0].key).to(equal('playbooks'))
|
expect(node.spec.taints[0].key).to(equal('playbooks'))
|
||||||
expect(node.spec.taints[0].value).to(equal('true'))
|
expect(node.spec.taints[0].value).to(equal('true'))
|
||||||
|
|
||||||
with it('adds labels to a pod'):
|
with it('adds label to a pod'):
|
||||||
self._create_nginx_pod()
|
self._create_nginx_pod()
|
||||||
|
|
||||||
pod = self.kubernetes_client.add_label_to_pod('nginx',
|
pod = self.kubernetes_client.add_label_to_pod('nginx',
|
||||||
@ -55,6 +56,18 @@ with description(infrastructure.KubernetesClient) as self:
|
|||||||
|
|
||||||
expect(pod.metadata.labels).to(have_key('testing', 'true'))
|
expect(pod.metadata.labels).to(have_key('testing', 'true'))
|
||||||
|
|
||||||
|
with it('starts sysdig capture for'):
|
||||||
|
self._create_nginx_pod()
|
||||||
|
|
||||||
|
job = self.kubernetes_client.start_sysdig_capture_for('nginx',
|
||||||
|
int(time.time()),
|
||||||
|
10,
|
||||||
|
'any s3 bucket',
|
||||||
|
'any aws key id',
|
||||||
|
'any aws secret key')
|
||||||
|
|
||||||
|
expect(job).not_to(be_none)
|
||||||
|
|
||||||
def _create_nginx_pod(self):
|
def _create_nginx_pod(self):
|
||||||
current_directory = os.path.dirname(os.path.realpath(__file__))
|
current_directory = os.path.dirname(os.path.realpath(__file__))
|
||||||
pod_manifesto = os.path.join(current_directory,
|
pod_manifesto = os.path.join(current_directory,
|
||||||
|
@ -0,0 +1,40 @@
|
|||||||
|
from mamba import description, it, before
|
||||||
|
from expects import expect
|
||||||
|
|
||||||
|
from doublex import Spy
|
||||||
|
from doublex_expects import have_been_called_with
|
||||||
|
|
||||||
|
from playbooks import infrastructure
|
||||||
|
import playbooks
|
||||||
|
|
||||||
|
|
||||||
|
with description(playbooks.StartSysdigCaptureForContainer) as self:
|
||||||
|
with before.each:
|
||||||
|
self.k8s_client = Spy(infrastructure.KubernetesClient)
|
||||||
|
self.duration_in_seconds = 'any duration in seconds'
|
||||||
|
self.s3_bucket = 'any s3 bucket url'
|
||||||
|
self.aws_access_key_id = 'any aws access key id'
|
||||||
|
self.aws_secret_access_key = 'any aws secret access key'
|
||||||
|
self.playbook = playbooks.StartSysdigCaptureForContainer(self.k8s_client,
|
||||||
|
self.duration_in_seconds,
|
||||||
|
self.s3_bucket,
|
||||||
|
self.aws_access_key_id,
|
||||||
|
self.aws_secret_access_key)
|
||||||
|
|
||||||
|
with it('add starts capturing job in same node than Pod alerted'):
|
||||||
|
pod_name = 'any pod name'
|
||||||
|
event_time = 'any event time'
|
||||||
|
alert = {'output_fields': {
|
||||||
|
'k8s.pod.name': pod_name,
|
||||||
|
'evt.time': event_time,
|
||||||
|
}}
|
||||||
|
|
||||||
|
self.playbook.run(alert)
|
||||||
|
|
||||||
|
expect(self.k8s_client.start_sysdig_capture_for)\
|
||||||
|
.to(have_been_called_with(pod_name,
|
||||||
|
event_time,
|
||||||
|
self.duration_in_seconds,
|
||||||
|
self.s3_bucket,
|
||||||
|
self.aws_access_key_id,
|
||||||
|
self.aws_secret_access_key))
|
@ -0,0 +1,24 @@
|
|||||||
|
FROM sysdig/sysdig:latest
|
||||||
|
|
||||||
|
MAINTAINER Néstor Salceda <nestor.salceda@sysdig.com>
|
||||||
|
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get --fix-broken install -y \
|
||||||
|
&& apt-get install -y --no-install-recommends \
|
||||||
|
s3cmd \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# debian:unstable head contains binutils 2.31, which generates
|
||||||
|
# binaries that are incompatible with kernels < 4.16. So manually
|
||||||
|
# forcibly install binutils 2.30-22 instead.
|
||||||
|
RUN curl -s -o binutils_2.30-22_amd64.deb http://snapshot.debian.org/archive/debian/20180622T211149Z/pool/main/b/binutils/binutils_2.30-22_amd64.deb \
|
||||||
|
&& curl -s -o libbinutils_2.30-22_amd64.deb http://snapshot.debian.org/archive/debian/20180622T211149Z/pool/main/b/binutils/libbinutils_2.30-22_amd64.deb \
|
||||||
|
&& curl -s -o binutils-x86-64-linux-gnu_2.30-22_amd64.deb http://snapshot.debian.org/archive/debian/20180622T211149Z/pool/main/b/binutils/binutils-x86-64-linux-gnu_2.30-22_amd64.deb \
|
||||||
|
&& curl -s -o binutils-common_2.30-22_amd64.deb http://snapshot.debian.org/archive/debian/20180622T211149Z/pool/main/b/binutils/binutils-common_2.30-22_amd64.deb \
|
||||||
|
&& dpkg -i *binutils*.deb
|
||||||
|
|
||||||
|
ENV CAPTURE_DURATION 120
|
||||||
|
|
||||||
|
COPY ./docker-entrypoint.sh /
|
||||||
|
|
||||||
|
ENTRYPOINT ["/docker-entrypoint.sh"]
|
15
integrations/kubernetes-response-engine/sysdig-capturer/docker-entrypoint.sh
Executable file
15
integrations/kubernetes-response-engine/sysdig-capturer/docker-entrypoint.sh
Executable file
@ -0,0 +1,15 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -exuo
|
||||||
|
|
||||||
|
echo "* Setting up /usr/src links from host"
|
||||||
|
|
||||||
|
for i in $(ls $SYSDIG_HOST_ROOT/usr/src)
|
||||||
|
do
|
||||||
|
ln -s $SYSDIG_HOST_ROOT/usr/src/$i /usr/src/$i
|
||||||
|
done
|
||||||
|
|
||||||
|
/usr/bin/sysdig-probe-loader
|
||||||
|
|
||||||
|
sysdig -S -M $CAPTURE_DURATION -pk -z -w $CAPTURE_FILE_NAME.scap.gz
|
||||||
|
s3cmd --access_key=$AWS_ACCESS_KEY_ID --secret_key=$AWS_SECRET_ACCESS_KEY put $CAPTURE_FILE_NAME.scap.gz $AWS_S3_BUCKET
|
Loading…
Reference in New Issue
Block a user