Skip to content

Commit 7d573dc

Browse files
committed
added falco support to enhance log sources. Specifically process logs as
we can now get full process audit and file audit logs for our containers
1 parent 6f65ae2 commit 7d573dc

10 files changed

Lines changed: 918 additions & 10 deletions

File tree

README.md

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ set_logs/
142142
```
143143
usage: setc [-h] [-v] [-p PASSWORD] [--volume VOLUME] [--network NETWORK]
144144
[--msf MSF] [--prefix PREFIX] [--splunk] [--postgres] [--elk]
145-
[--no-zeek] [--cleanup_network] [--cleanup_volume]
145+
[--falco] [--no-zeek] [--cleanup_network] [--cleanup_volume]
146146
[--cleanup_splunk] [--cleanup_postgres] [--cleanup_elk]
147147
config
148148
```
@@ -159,13 +159,41 @@ usage: setc [-h] [-v] [-p PASSWORD] [--volume VOLUME] [--network NETWORK]
159159
| `--splunk` | Launch a Splunk instance and ingest logs |
160160
| `--postgres` | Launch a PostgreSQL instance and ingest logs |
161161
| `--elk` | Launch Elasticsearch + Kibana and ingest logs (Kibana UI at `http://localhost:5601`) |
162+
| `--falco` | Run Falco for runtime syscall monitoring during exploitation |
162163
| `--no-zeek` | Disable Zeek PCAP parsing |
163164
| `--cleanup_network` | Delete the Docker network before running |
164165
| `--cleanup_volume` | Delete the log volume before running |
165166
| `--cleanup_splunk` | Remove Splunk container after completion |
166167
| `--cleanup_postgres` | Remove PostgreSQL container after completion |
167168
| `--cleanup_elk` | Remove Elasticsearch and Kibana containers after completion |
168169

170+
### Falco runtime monitoring
171+
172+
The `--falco` flag deploys [Falco](https://falco.org/) as a privileged sidecar container using the modern eBPF driver (requires kernel >= 5.8). Falco monitors target containers in real-time during exploitation, capturing:
173+
174+
- **Process execution** -- spawned shells, reverse connections, privilege escalation commands
175+
- **Network connections** -- connect/accept syscalls with source/destination details
176+
- **File writes** -- file system modifications during exploitation
177+
178+
Events are captured continuously throughout the exploit lifecycle, complementing `DockerProcessLogs` which only takes point-in-time snapshots via `docker top`. Falco events are converted to all standard log formats.
179+
180+
Output structure per CVE:
181+
```
182+
set_logs/CVE-XXXX/
183+
falco/falco_events.log # Raw Falco NDJSON events
184+
cim/cim_falco_*.log # Splunk CIM format
185+
ecs/ecs_falco_*.log # Elastic Common Schema
186+
ocsf/ocsf_falco_*.log # OCSF 1.4.0
187+
cef/cef_falco_*.log # ArcSight CEF
188+
udm/udm_falco_*.log # Google Chronicle UDM
189+
```
190+
191+
```bash
192+
# Run with Falco monitoring
193+
python3 setc/setc.py example_configurations/docker_small.json \
194+
--falco --cleanup_volume --cleanup_network
195+
```
196+
169197
### RPC exploit mode
170198

171199
By default SETC drives Metasploit via `msfconsole -x` (CLI mode). Setting `"exploit_mode": "rpc"` in a config entry switches to the MSGRPC API via `pymetasploit3`, which provides:

docker_images/falco/falco.yaml

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
engine:
2+
kind: modern_ebpf
3+
4+
base_syscalls:
5+
custom_set: []
6+
repair: false
7+
all: true
8+
9+
load_plugins: [container]
10+
11+
plugins:
12+
- name: container
13+
library_path: libcontainer.so
14+
init_config:
15+
label_max_len: 100
16+
with_size: false
17+
18+
json_output: true
19+
json_include_output_property: true
20+
json_include_output_fields_property: true
21+
json_include_tags_property: true
22+
23+
file_output:
24+
enabled: true
25+
keep_alive: true
26+
filename: /falco_output/falco_events.jsonl
27+
28+
stdout_output:
29+
enabled: false
30+
31+
priority: NOTICE
32+
33+
rules_files:
34+
- /etc/falco/falco_rules.yaml
35+
- /etc/falco/falco_rules.local.yaml
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
- rule: SETC Process Execution
2+
desc: All process executions in containers
3+
condition: spawned_process and container.id != host
4+
output: >
5+
Process spawned
6+
(container_id=%container.id container_name=%container.name
7+
proc_name=%proc.name proc_pid=%proc.pid proc_ppid=%proc.ppid
8+
proc_cmdline=%proc.cmdline user_name=%user.name
9+
proc_exepath=%proc.exepath evt_type=%evt.type)
10+
priority: NOTICE
11+
tags: [process, setc]
12+
13+
- rule: SETC Network Connection
14+
desc: Network connections in containers
15+
condition: >
16+
(evt.type in (connect, accept)) and fd.typechar='4' and
17+
container.id != host
18+
output: >
19+
Network connection
20+
(container_id=%container.id container_name=%container.name
21+
proc_name=%proc.name fd_name=%fd.name
22+
fd_sip=%fd.sip fd_cip=%fd.cip fd_sport=%fd.sport fd_cport=%fd.cport
23+
evt_type=%evt.type user_name=%user.name)
24+
priority: NOTICE
25+
tags: [network, setc]
26+
27+
- rule: SETC File Write
28+
desc: File writes in containers
29+
condition: >
30+
(evt.type in (write, writev, pwritev)) and fd.typechar='f' and
31+
container.id != host
32+
output: >
33+
File write
34+
(container_id=%container.id container_name=%container.name
35+
proc_name=%proc.name fd_name=%fd.name user_name=%user.name
36+
evt_type=%evt.type proc_cmdline=%proc.cmdline)
37+
priority: NOTICE
38+
tags: [file, setc]

setc/modules/elasticsearch.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,13 @@
1414

1515
# Maps format directory names (as written under /data/<cve>/) to (index_name, is_json).
1616
INDEX_MAP = {
17-
"zeek": ("zeek", True),
18-
"cim": ("cim", True),
19-
"ecs": ("ecs", True),
20-
"ocsf": ("ocsf", True),
21-
"cef": ("cef", False),
22-
"udm": ("udm", True),
17+
"zeek": ("zeek", True),
18+
"cim": ("cim", True),
19+
"ecs": ("ecs", True),
20+
"ocsf": ("ocsf", True),
21+
"cef": ("cef", False),
22+
"udm": ("udm", True),
23+
"falco": ("falco", True),
2324
}
2425

2526

setc/modules/falco.py

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
"""Falco runtime security module for continuous syscall monitoring during exploitation."""
2+
from __future__ import annotations
3+
4+
import io
5+
import json
6+
import logging
7+
import os
8+
import tarfile
9+
import time
10+
from typing import Any
11+
12+
import docker
13+
import docker.models.containers
14+
15+
from utils import prefixed_name, safe_stop_remove
16+
from modules.falco_log_converter import convert_falco_events
17+
18+
logger = logging.getLogger(__name__)
19+
20+
# Resolve paths to Falco config files relative to this module
21+
_MODULE_DIR = os.path.dirname(os.path.abspath(__file__))
22+
_FALCO_DIR = os.path.join(os.path.dirname(_MODULE_DIR), "..", "docker_images", "falco")
23+
_FALCO_YAML = os.path.abspath(os.path.join(_FALCO_DIR, "falco.yaml"))
24+
_FALCO_RULES = os.path.abspath(os.path.join(_FALCO_DIR, "falco_rules.local.yaml"))
25+
26+
27+
class FalcoModule:
28+
"""Optional module that runs Falco as a privileged container for runtime syscall monitoring."""
29+
30+
FALCO_IMAGE = "falcosecurity/falco:0.43.0"
31+
def __init__(self, docker_client: docker.DockerClient, volume_name: str = "set_logs",
32+
network_name: str = "set_framework_net", prefix: str = "") -> None:
33+
"""Initialize Falco module with Docker client and shared resource names."""
34+
self.client = docker_client
35+
self.volume = volume_name
36+
self.network = network_name
37+
self.prefix = prefix
38+
self.falco = None
39+
self._file_offset = 0
40+
41+
def _prefixed(self, name: str) -> str:
42+
"""Return the session-prefixed version of a container name."""
43+
return prefixed_name(self.prefix, name)
44+
45+
def _find_existing(self) -> docker.models.containers.Container | None:
46+
"""Find a running Falco container already mounted to our volume."""
47+
for container in self.client.containers.list(all=True,
48+
filters={"ancestor": self.FALCO_IMAGE}):
49+
mounts = container.attrs.get("Mounts", [])
50+
for m in mounts:
51+
if m.get("Name") == self.volume:
52+
if container.status != "running":
53+
logger.info("Starting stopped Falco container: %s", container.name)
54+
container.start()
55+
return container
56+
return None
57+
58+
def setup(self) -> None:
59+
"""Start the Falco container with modern eBPF, or reuse one already running."""
60+
existing = self._find_existing()
61+
if existing:
62+
logger.info("Reusing existing Falco container: %s", existing.name)
63+
self.falco = existing
64+
return
65+
66+
logger.info("Starting Falco container for runtime monitoring")
67+
self.falco = self.client.containers.run(
68+
self.FALCO_IMAGE,
69+
command=["falco"],
70+
detach=True,
71+
name=self._prefixed("falco"),
72+
privileged=True,
73+
pid_mode="host",
74+
volumes={
75+
self.volume: {"bind": "/falco_output", "mode": "rw"},
76+
"/sys/kernel/tracing": {"bind": "/sys/kernel/tracing", "mode": "ro"},
77+
"/var/run/docker.sock": {"bind": "/host/var/run/docker.sock", "mode": "ro"},
78+
"/proc": {"bind": "/host/proc", "mode": "ro"},
79+
"/etc": {"bind": "/host/etc", "mode": "ro"},
80+
_FALCO_YAML: {"bind": "/etc/falco/falco.yaml", "mode": "ro"},
81+
_FALCO_RULES: {"bind": "/etc/falco/falco_rules.local.yaml", "mode": "ro"},
82+
},
83+
network=self.network,
84+
)
85+
logger.info("Falco container started: %s", self.falco.name)
86+
87+
def is_ready(self) -> bool:
88+
"""Return True if the Falco container is running."""
89+
if not self.falco:
90+
return False
91+
try:
92+
self.falco.reload()
93+
return self.falco.status == "running"
94+
except (docker.errors.NotFound, docker.errors.APIError):
95+
return False
96+
97+
def create_log_directories(self, name: str, write_container: docker.models.containers.Container) -> None:
98+
"""Create the falco subdirectory for a CVE on the volume."""
99+
cmd = ["mkdir", "-p", "/data/%s/falco" % name]
100+
try:
101+
result = write_container.exec_run(cmd=cmd)
102+
if result.exit_code != 0:
103+
logger.warning("Failed to create falco directory for %s: %s", name, result.output)
104+
except (docker.errors.NotFound, docker.errors.APIError) as e:
105+
logger.warning("Could not create falco log directory: %s", e)
106+
107+
def extract_events(self, vuln_name: str, container_names: list[str],
108+
write_container: docker.models.containers.Container) -> None:
109+
"""Extract Falco events for a specific CVE, filter by container, and convert to log formats.
110+
111+
Args:
112+
vuln_name: CVE/vuln name for organizing output.
113+
container_names: Container names to filter events for.
114+
write_container: Container with the shared volume mounted at /data.
115+
"""
116+
if not self.falco:
117+
return
118+
119+
try:
120+
# Get current file size
121+
result = self.falco.exec_run(
122+
cmd=["wc", "-c", "/falco_output/falco_events.jsonl"],
123+
demux=True)
124+
if result.exit_code != 0:
125+
logger.warning("Falco events file not found yet")
126+
return
127+
stdout = result.output[0] if result.output[0] else b""
128+
current_size = int(stdout.strip().split()[0])
129+
130+
if current_size <= self._file_offset:
131+
logger.debug("No new Falco events since last extraction")
132+
return
133+
134+
# Read new content since last offset
135+
result = self.falco.exec_run(
136+
cmd=["tail", "-c", "+%d" % (self._file_offset + 1),
137+
"/falco_output/falco_events.jsonl"],
138+
demux=True)
139+
if result.exit_code != 0:
140+
logger.warning("Failed to read Falco events")
141+
return
142+
143+
self._file_offset = current_size
144+
raw_output = result.output[0] if result.output[0] else b""
145+
raw_text = raw_output.decode("utf-8", errors="replace")
146+
147+
# Parse NDJSON and filter by container name
148+
all_events = []
149+
filtered_events = []
150+
for line in raw_text.strip().split("\n"):
151+
if not line.strip():
152+
continue
153+
try:
154+
event = json.loads(line)
155+
all_events.append(event)
156+
output_fields = event.get("output_fields", {})
157+
cname = output_fields.get("container.name", "")
158+
if cname in container_names:
159+
filtered_events.append(event)
160+
except json.JSONDecodeError:
161+
continue
162+
163+
logger.info("Falco: %d total events, %d matched target containers for %s",
164+
len(all_events), len(filtered_events), vuln_name)
165+
166+
if not filtered_events:
167+
return
168+
169+
# Create falco directory
170+
self.create_log_directories(vuln_name, write_container)
171+
172+
# Write raw filtered events to falco subdirectory
173+
raw_content = "\n".join(json.dumps(e) for e in filtered_events) + "\n"
174+
tar_fileobj = io.BytesIO()
175+
with tarfile.open(fileobj=tar_fileobj, mode="w|") as tar:
176+
data = raw_content.encode("utf-8")
177+
tf = tarfile.TarInfo("falco_events.log")
178+
tf.size = len(data)
179+
tar.addfile(tf, io.BytesIO(data))
180+
tar_fileobj.flush()
181+
tar_fileobj.seek(0)
182+
write_container.put_archive("/data/%s/falco" % vuln_name, tar_fileobj)
183+
184+
# Convert to all log formats
185+
convert_falco_events(filtered_events, write_container, vuln_name)
186+
logger.info("Falco events converted and written for %s", vuln_name)
187+
188+
except (docker.errors.NotFound, docker.errors.APIError) as e:
189+
logger.warning("Falco event extraction failed: %s", e)
190+
191+
def cleanup(self) -> None:
192+
"""Stop and remove the Falco container."""
193+
if self.falco:
194+
safe_stop_remove(self.falco, label=self._prefixed("falco"))
195+
logger.info("Falco container removed")

0 commit comments

Comments
 (0)