Skip to content

Commit 689ac24

Browse files
committed
added falco as a module for process, network, file and alert logging.
1 parent d11154e commit 689ac24

5 files changed

Lines changed: 353 additions & 27 deletions

File tree

docker_images/falco/falco.yaml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,15 @@ json_include_output_property: true
2020
json_include_output_fields_property: true
2121
json_include_tags_property: true
2222

23+
append_output:
24+
- suggested_output: true
25+
- match:
26+
source: syscall
27+
extra_output: "container_id=%container.id container_name=%container.name"
28+
extra_fields:
29+
- container.id
30+
- container.name
31+
2332
file_output:
2433
enabled: true
2534
keep_alive: true

setc/modules/falco.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,8 +160,20 @@ def extract_events(self, vuln_name: str, container_names: list[str],
160160
except json.JSONDecodeError:
161161
continue
162162

163+
# Log distinct rule names for diagnostics
164+
filtered_rules = {}
165+
for e in filtered_events:
166+
r = e.get("rule", "unknown")
167+
filtered_rules[r] = filtered_rules.get(r, 0) + 1
168+
all_rules = {}
169+
for e in all_events:
170+
r = e.get("rule", "unknown")
171+
all_rules[r] = all_rules.get(r, 0) + 1
172+
163173
logger.info("Falco: %d total events, %d matched target containers for %s",
164174
len(all_events), len(filtered_events), vuln_name)
175+
logger.debug("Falco rules fired (all containers): %s", dict(all_rules))
176+
logger.debug("Falco rules fired (target only): %s", dict(filtered_rules))
165177

166178
if not filtered_events:
167179
return

setc/modules/falco_log_converter.py

Lines changed: 189 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import io
55
import json
66
import logging
7+
import re
78
import tarfile
89
import time
910
from typing import Any
@@ -14,6 +15,33 @@
1415

1516
logger = logging.getLogger(__name__)
1617

18+
# ===================================================================
19+
# Priority mapping + MITRE tag extraction (for alert schemas)
20+
# ===================================================================
21+
22+
_FALCO_PRIORITY_MAP = {
23+
"Emergency": {"cim": "critical", "ecs": 4, "ocsf": "5", "cef": "10", "udm": "CRITICAL"},
24+
"Alert": {"cim": "critical", "ecs": 4, "ocsf": "5", "cef": "9", "udm": "CRITICAL"},
25+
"Critical": {"cim": "critical", "ecs": 4, "ocsf": "5", "cef": "9", "udm": "CRITICAL"},
26+
"Error": {"cim": "high", "ecs": 3, "ocsf": "4", "cef": "7", "udm": "HIGH"},
27+
"Warning": {"cim": "high", "ecs": 3, "ocsf": "4", "cef": "7", "udm": "HIGH"},
28+
"Notice": {"cim": "medium", "ecs": 2, "ocsf": "3", "cef": "5", "udm": "MEDIUM"},
29+
"Informational": {"cim": "low", "ecs": 1, "ocsf": "2", "cef": "3", "udm": "LOW"},
30+
"Debug": {"cim": "low", "ecs": 1, "ocsf": "1", "cef": "1", "udm": "LOW"},
31+
}
32+
33+
34+
def _extract_mitre_tags(tags):
35+
"""Return (tactic_name, [technique_ids]) from Falco tags list."""
36+
tactic = None
37+
technique_ids = []
38+
for tag in (tags or []):
39+
if tag.startswith("mitre_"):
40+
tactic = tag[len("mitre_"):].replace("_", " ").title()
41+
elif re.match(r"^T\d{4}", tag):
42+
technique_ids.append(tag)
43+
return tactic, technique_ids
44+
1745
# ===================================================================
1846
# Process event schemas
1947
# ===================================================================
@@ -331,6 +359,116 @@
331359
},
332360
}
333361

362+
# ===================================================================
363+
# Alert schemas (for built-in Falco detection rules)
364+
# ===================================================================
365+
366+
falco_cim_alert = {
367+
"timestamp": lambda x: x.get("time", time.time()),
368+
"action": lambda x: "detected",
369+
"severity": lambda x: _FALCO_PRIORITY_MAP.get(x.get("_priority", "Notice"), {}).get("cim", "medium"),
370+
"signature": lambda x: x.get("_rule"),
371+
"description": lambda x: x.get("_output"),
372+
"process_name": lambda x: x.get("proc.name"),
373+
"process_id": lambda x: x.get("proc.pid"),
374+
"user": lambda x: x.get("user.name"),
375+
"dest": lambda x: x.get("container.name"),
376+
}
377+
378+
falco_ecs_alert = {
379+
"@timestamp": lambda x: x.get("time", time.time()),
380+
"ecs.version": lambda x: "8.17",
381+
"event.kind": lambda x: "alert",
382+
"event.category": lambda x: "intrusion_detection",
383+
"event.type": lambda x: "info",
384+
"event.severity": lambda x: _FALCO_PRIORITY_MAP.get(x.get("_priority", "Notice"), {}).get("ecs", 2),
385+
"event.action": lambda x: x.get("evt.type"),
386+
"rule.name": lambda x: x.get("_rule"),
387+
"rule.description": lambda x: x.get("_output"),
388+
"threat.framework": lambda x: "MITRE ATT&CK" if _extract_mitre_tags(x.get("_tags"))[0] else None,
389+
"threat.tactic.name": lambda x: _extract_mitre_tags(x.get("_tags"))[0],
390+
"threat.technique.id": lambda x: _extract_mitre_tags(x.get("_tags"))[1] or None,
391+
"process.name": lambda x: x.get("proc.name"),
392+
"process.pid": lambda x: x.get("proc.pid"),
393+
"process.command_line": lambda x: x.get("proc.cmdline"),
394+
"container.name": lambda x: x.get("container.name"),
395+
"container.id": lambda x: x.get("container.id"),
396+
"user.name": lambda x: x.get("user.name"),
397+
}
398+
399+
falco_ocsf_alert = {
400+
"time": lambda x: x.get("time", time.time()),
401+
"activity_name": lambda x: "Create",
402+
"activity_id": lambda x: "1",
403+
"category_uid": lambda x: "2",
404+
"category_name": lambda x: "Findings",
405+
"class_uid": lambda x: "2004",
406+
"class_name": lambda x: "Detection Finding",
407+
"severity_id": lambda x: _FALCO_PRIORITY_MAP.get(x.get("_priority", "Notice"), {}).get("ocsf", "3"),
408+
"finding_info": {
409+
"title": lambda x: x.get("_rule"),
410+
"desc": lambda x: x.get("_output"),
411+
},
412+
"attacks": lambda x: [{
413+
"tactic": {"name": _extract_mitre_tags(x.get("_tags"))[0]},
414+
"technique": {"uid": tid for tid in _extract_mitre_tags(x.get("_tags"))[1]},
415+
"version": "14.1",
416+
}] if _extract_mitre_tags(x.get("_tags"))[0] else None,
417+
"process": {
418+
"name": lambda x: x.get("proc.name"),
419+
"pid": lambda x: x.get("proc.pid"),
420+
"cmd_line": lambda x: x.get("proc.cmdline"),
421+
},
422+
"metadata": {
423+
"version": lambda x: "1.4.0",
424+
"product": {
425+
"name": lambda x: "Falco",
426+
"vendor_name": lambda x: "SETC",
427+
},
428+
},
429+
}
430+
431+
falco_cef_alert = {
432+
"rt": lambda x: x.get("time", time.time()),
433+
"msg": lambda x: x.get("_output"),
434+
"sproc": lambda x: x.get("proc.name"),
435+
"spid": lambda x: x.get("proc.pid"),
436+
"suser": lambda x: x.get("user.name"),
437+
"cs2Label": lambda x: "ruleName",
438+
"cs2": lambda x: x.get("_rule"),
439+
"cs3Label": lambda x: "tags",
440+
"cs3": lambda x: ",".join(x.get("_tags", [])),
441+
"cs4Label": lambda x: "containerName",
442+
"cs4": lambda x: x.get("container.name"),
443+
}
444+
445+
falco_udm_alert = {
446+
"metadata": {
447+
"event_timestamp": lambda x: x.get("time", time.time()),
448+
"event_type": lambda x: "GENERIC_EVENT",
449+
"vendor_name": lambda x: "SETC",
450+
"product_name": lambda x: "Falco",
451+
"product_version": lambda x: "0.43.0",
452+
},
453+
"security_result": {
454+
"alert_state": lambda x: "ALERTING",
455+
"severity": lambda x: _FALCO_PRIORITY_MAP.get(x.get("_priority", "Notice"), {}).get("udm", "MEDIUM"),
456+
"rule_name": lambda x: x.get("_rule"),
457+
"description": lambda x: x.get("_output"),
458+
},
459+
"target": {
460+
"process": {
461+
"pid": lambda x: x.get("proc.pid"),
462+
"commandLine": lambda x: x.get("proc.cmdline"),
463+
},
464+
},
465+
"principal": {
466+
"user": {
467+
"userid": lambda x: x.get("user.name"),
468+
},
469+
},
470+
}
471+
334472
# ===================================================================
335473
# Rule → schema mapping
336474
# ===================================================================
@@ -382,42 +520,78 @@ def convert_falco_events(events: list[dict[str, Any]],
382520
cef_all: list[str] = []
383521
udm_all: list[dict] = []
384522

523+
cim_alerts: list[dict] = []
524+
ecs_alerts: list[dict] = []
525+
ocsf_alerts: list[dict] = []
526+
cef_alerts: list[str] = []
527+
udm_alerts: list[dict] = []
528+
385529
for event in events:
386530
rule = event.get("rule", "")
387-
schemas = _RULE_SCHEMAS.get(rule)
388-
if not schemas:
389-
continue
390-
391531
fields = event.get("output_fields", {})
392532
fields["time"] = event.get("time", time.time())
533+
schemas = _RULE_SCHEMAS.get(rule)
393534

394-
cim_all.append(apply_schema(fields, schemas["cim"]))
395-
ecs_all.append(apply_schema(fields, schemas["ecs"]))
396-
ocsf_all.append(apply_schema(fields, schemas["ocsf"]))
397-
udm_all.append(apply_schema(fields, schemas["udm"]))
398-
399-
cef_extensions = apply_schema(fields, schemas["cef"])
400-
cef_all.append(format_cef_line(schemas["cef_header"], cef_extensions))
535+
if schemas:
536+
# SETC telemetry rule → observational event
537+
cim_all.append(apply_schema(fields, schemas["cim"]))
538+
ecs_all.append(apply_schema(fields, schemas["ecs"]))
539+
ocsf_all.append(apply_schema(fields, schemas["ocsf"]))
540+
udm_all.append(apply_schema(fields, schemas["udm"]))
401541

402-
# Write each format to the volume
542+
cef_extensions = apply_schema(fields, schemas["cef"])
543+
cef_all.append(format_cef_line(schemas["cef_header"], cef_extensions))
544+
else:
545+
# Built-in Falco detection rule → alert event
546+
fields["_rule"] = rule
547+
fields["_priority"] = event.get("priority", "Notice")
548+
fields["_output"] = event.get("output", "")
549+
fields["_tags"] = event.get("tags", [])
550+
551+
cim_alerts.append(apply_schema(fields, falco_cim_alert))
552+
ecs_alerts.append(apply_schema(fields, falco_ecs_alert))
553+
ocsf_alerts.append(apply_schema(fields, falco_ocsf_alert))
554+
udm_alerts.append(apply_schema(fields, falco_udm_alert))
555+
556+
priority = event.get("priority", "Notice")
557+
cef_sev = _FALCO_PRIORITY_MAP.get(priority, {}).get("cef", "5")
558+
header = ("SETC", "Falco", "0.43.0", "FALCO-DETECT",
559+
f"Falco Detection: {rule}", str(cef_sev))
560+
cef_alerts.append(format_cef_line(header, apply_schema(fields, falco_cef_alert)))
561+
562+
telemetry_count = len(cim_all)
563+
alert_count = len(cim_alerts)
564+
logger.info("Falco conversion: %d telemetry events, %d alert events for %s",
565+
telemetry_count, alert_count, vuln_name)
566+
567+
# Write telemetry (SETC rules)
403568
for log_type, data in [("cim", cim_all), ("ecs", ecs_all),
404569
("ocsf", ocsf_all), ("udm", udm_all),
405570
("cef", cef_all)]:
406571
if not data:
407572
continue
408-
_write_to_volume(write_container, log_type, data, vuln_name)
573+
_write_to_volume(write_container, log_type, data, vuln_name, suffix="falco")
574+
575+
# Write alerts (built-in Falco detection rules)
576+
for log_type, data in [("cim", cim_alerts), ("ecs", ecs_alerts),
577+
("ocsf", ocsf_alerts), ("udm", udm_alerts),
578+
("cef", cef_alerts)]:
579+
if not data:
580+
continue
581+
_write_to_volume(write_container, log_type, data, vuln_name, suffix="falco_alert")
409582

410583

411584
def _write_to_volume(write_container: docker.models.containers.Container,
412-
log_type: str, data: list, directory: str) -> None:
585+
log_type: str, data: list, directory: str,
586+
suffix: str = "falco") -> None:
413587
"""Write converted logs to the shared Docker volume as a tar archive."""
414588
tar_fileobj = io.BytesIO()
415589
with tarfile.open(fileobj=tar_fileobj, mode="w|") as tar:
416590
if isinstance(data, list) and data and isinstance(data[0], str):
417591
my_content = ("\n".join(data) + "\n").encode('utf-8')
418592
else:
419593
my_content = json.dumps(data).encode('utf-8')
420-
tf = tarfile.TarInfo("%s_falco_%s.log" % (log_type, str(time.time())))
594+
tf = tarfile.TarInfo("%s_%s_%s.log" % (log_type, suffix, str(time.time())))
421595
tf.size = len(my_content)
422596
tar.addfile(tf, io.BytesIO(my_content))
423597
tar_fileobj.flush()

setc/setc.py

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -468,18 +468,20 @@ def main() -> None:
468468
target = setc._get_target_container()
469469
tp.pre_down(target, system_config["name"])
470470

471-
if falco and falco.is_ready():
472-
target_containers = [setc.target_name]
473-
if setc_type == "compose" and hasattr(setc, 'wdocker'):
474-
target_containers = [c.name for c in setc.wdocker.compose.ps()]
475-
write_cont = zeek.zeek if zeek else falco.falco
476-
falco.extract_events(system_config["name"], target_containers, write_cont)
477-
478-
if not args.no_zeek:
479-
zeek.pcap_parse(system_config["name"])
480-
zeek.to_logstandard(system_config["name"])
481-
482-
setc.cleanup_all()
471+
# Falco/Zeek extraction runs outside the spinner so log
472+
# messages don't collide with the status line.
473+
if falco and falco.is_ready():
474+
target_containers = [setc.target_name]
475+
if setc_type == "compose" and hasattr(setc, 'wdocker'):
476+
target_containers = [c.name for c in setc.wdocker.compose.ps()]
477+
write_cont = zeek.zeek if zeek else falco.falco
478+
falco.extract_events(system_config["name"], target_containers, write_cont)
479+
480+
if not args.no_zeek:
481+
zeek.pcap_parse(system_config["name"])
482+
zeek.to_logstandard(system_config["name"])
483+
484+
setc.cleanup_all()
483485
logger.info("Cleanup complete for %s", system_config["name"])
484486

485487
########################################

0 commit comments

Comments
 (0)