|
4 | 4 | import io |
5 | 5 | import json |
6 | 6 | import logging |
| 7 | +import re |
7 | 8 | import tarfile |
8 | 9 | import time |
9 | 10 | from typing import Any |
|
14 | 15 |
|
15 | 16 | logger = logging.getLogger(__name__) |
16 | 17 |
|
| 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 | + |
17 | 45 | # =================================================================== |
18 | 46 | # Process event schemas |
19 | 47 | # =================================================================== |
|
331 | 359 | }, |
332 | 360 | } |
333 | 361 |
|
| 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 | + |
334 | 472 | # =================================================================== |
335 | 473 | # Rule → schema mapping |
336 | 474 | # =================================================================== |
@@ -382,42 +520,78 @@ def convert_falco_events(events: list[dict[str, Any]], |
382 | 520 | cef_all: list[str] = [] |
383 | 521 | udm_all: list[dict] = [] |
384 | 522 |
|
| 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 | + |
385 | 529 | for event in events: |
386 | 530 | rule = event.get("rule", "") |
387 | | - schemas = _RULE_SCHEMAS.get(rule) |
388 | | - if not schemas: |
389 | | - continue |
390 | | - |
391 | 531 | fields = event.get("output_fields", {}) |
392 | 532 | fields["time"] = event.get("time", time.time()) |
| 533 | + schemas = _RULE_SCHEMAS.get(rule) |
393 | 534 |
|
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"])) |
401 | 541 |
|
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) |
403 | 568 | for log_type, data in [("cim", cim_all), ("ecs", ecs_all), |
404 | 569 | ("ocsf", ocsf_all), ("udm", udm_all), |
405 | 570 | ("cef", cef_all)]: |
406 | 571 | if not data: |
407 | 572 | 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") |
409 | 582 |
|
410 | 583 |
|
411 | 584 | 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: |
413 | 587 | """Write converted logs to the shared Docker volume as a tar archive.""" |
414 | 588 | tar_fileobj = io.BytesIO() |
415 | 589 | with tarfile.open(fileobj=tar_fileobj, mode="w|") as tar: |
416 | 590 | if isinstance(data, list) and data and isinstance(data[0], str): |
417 | 591 | my_content = ("\n".join(data) + "\n").encode('utf-8') |
418 | 592 | else: |
419 | 593 | 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()))) |
421 | 595 | tf.size = len(my_content) |
422 | 596 | tar.addfile(tf, io.BytesIO(my_content)) |
423 | 597 | tar_fileobj.flush() |
|
0 commit comments