Falco unit tests are a surprisingly powerful way to catch regressions in your Falco rules before they hit production.

Let’s see it in action. Imagine you have a rule that flags when sshd is executed with root privileges.

- rule: Execute sshd as root
  desc: sshd should never be executed as root
  condition: >
    evt.type = execve and
    proc.name = "sshd" and
    user.name = "root"
  output: sshd executed as root (user=%user.name pid=%proc.pid)
  priority: critical

To unit test this, you’d create a test file alongside your rule file (e.g., sshd-root-test.yaml). This test file describes the event that should trigger your rule and what the expected output should be.

- test: sshd executed as root
  input:
    event:
      type: execve
      args: ["/usr/sbin/sshd", "-D"]
      parent:
        name: systemd
      process:
        name: sshd
        pid: 1234
        user:
          name: root
  expected:
    output: sshd executed as root (user=root pid=1234)
    priority: critical

When you run falco-unit-test --rule-file=/path/to/your/rules.yaml --test-file=/path/to/your/sshd-root-test.yaml, Falco simulates the execve event described in input. If the condition in your rule matches this simulated event, it checks if the output and priority also match what’s in expected.

This setup allows you to build a robust suite of tests that cover various scenarios, including edge cases and common attack vectors. You can test for specific command-line arguments, parent processes, network connections, file modifications, and more, all by crafting precise input event data.

The core of falco-unit-test lies in its ability to take a declarative description of an event and feed it directly into Falco’s rule engine. It bypasses the need to actually trigger these events in a live system, making tests fast, repeatable, and safe. Each test case is a miniature Falco execution, isolating the rule logic from external dependencies.

The input section of a test case is a direct representation of a Falco event. You’re essentially telling Falco, "Imagine this happened. Would your rule fire?" The event object maps directly to the fields Falco extracts from syscalls. For instance, evt.type = execve corresponds to the execve syscall, and proc.name = "sshd" maps to the proc.name field Falco populates. You can even nest structures to represent parent processes (parent.name) or user information (user.name).

The expected section then validates Falco’s response. It verifies that the rule’s output string contains the expected message and that the priority is also as anticipated. This ensures not only that the rule fires, but that it fires with the correct details and severity.

A crucial, yet often overlooked, aspect of writing effective unit tests for Falco rules is understanding the order of operations within the Falco engine. Falco processes events sequentially, enriching them with data from various sources (like user lookup tables or network information) as they arrive. Your unit tests should reflect this. If a rule depends on a specific process being present in the /etc/passwd file, your test might need to include a mock getpwnam or a simulated file read event if Falco’s simulation capabilities allow for that level of granular control within the input. More commonly, you’ll rely on the pre-populated fields in the input event itself to represent the state of the system at the time of the syscall.

The next step after mastering unit tests is to explore integration testing with tools like falco-driver-loader and containerd to ensure your rules behave as expected in a more realistic containerized environment.

Want structured learning?

Take the full Falco course →