mirror of
https://github.com/falcosecurity/falco.git
synced 2025-09-28 21:57:37 +00:00
Create standalone classes falco_engine/falco_outputs that can be embedded in other programs. falco_engine is responsible for matching events against rules, and falco_output is responsible for formatting an alert string given an event and writing the alert string to all configured outputs. falco_engine's main interfaces are: - load_rules/load_rules_file: Given a path to a rules file or a string containing a set of rules, load the rules. Also loads needed lua code. - process_event(): check the event against the set of rules and return the results of a match, if any. - describe_rule(): print details on a specific rule or all rules. - print_stats(): print stats on the rules that matched. - enable_rule(): enable/disable any rules matching a pattern. New falco command line option -D allows you to disable one or more rules on the command line. falco_output's main interfaces are: - init(): load needed lua code. - add_output(): add an output channel for alert notifications. - handle_event(): given an event that matches one or more rules, format an alert message and send it to any output channels. Each of falco_engine/falco_output maintains a separate lua state and loads separate sets of lua files. The code to create and initialize the lua state is in a base class falco_common. falco_engine no longer logs anything. In the case of errors, it throws exceptions. falco_logger is now only used as a logging mechanism for falco itself and as an output method for alert messages. (This should really probably be split, but it's ok for now). falco_engine contains an sinsp_evttype_filter object containing the set of eventtype filters. Instead of calling m_inspector->add_evttype_filter() to add a filter created by the compiler, call falco_engine::add_evttype_filter() instead. This means that the inspector runs with a NULL filter and all events are returned from do_inspect. This depends on https://github.com/draios/sysdig/pull/633 which has a wrapper around a set of eventtype filters. Some additional changes along with creating these classes: - Some cleanups of unnecessary header files, cmake include_directory()s, etc to only include necessary includes and only include them in header files when required. - Try to avoid 'using namespace std' in header files, or assuming someone else has done that. Generally add 'using namespace std' to all source files. - Instead of using sinsp_exception for all errors, define a falco_engine_exception class for exceptions coming from the falco engine and use it instead. For falco program code, switch to general exceptions under std::exception and catch + display an error for all exceptions, not just sinsp_exceptions. - Remove fields.{cpp,h}. This was dead code. - Start tracking counts of rules by priority string (i.e. what's in the falco rules file) as compared to priority level (i.e. roughtly corresponding to a syslog level). This keeps the rule processing and rule output halves separate. This led to some test changes. The regex used in the test is now case insensitive to be a bit more flexible. - Now that https://github.com/draios/sysdig/pull/632 is merged, we can delete the rules object (and its lua_parser) safely. - Move loading the initial lua script to the constructor. Otherwise, calling load_rules() twice re-loads the lua script and throws away any state like the mapping from rule index to rule. - Allow an empty rules file. Finally, fix most memory leaks found by valgrind: - falco_configuration wasn't deleting the allocated m_config yaml config. - several ifstreams were being created simply to test which falco config file to use. - In the lua output methods, an event formatter was being created using falco.formatter() but there was no corresponding free_formatter(). This depends on changes in https://github.com/draios/sysdig/pull/640.
155 lines
5.9 KiB
Python
155 lines
5.9 KiB
Python
#!/usr/bin/env python
|
|
|
|
import os
|
|
import re
|
|
import json
|
|
import sets
|
|
|
|
from avocado import Test
|
|
from avocado.utils import process
|
|
from avocado.utils import linux_modules
|
|
|
|
class FalcoTest(Test):
|
|
|
|
def setUp(self):
|
|
"""
|
|
Load the sysdig kernel module if not already loaded.
|
|
"""
|
|
self.falcodir = self.params.get('falcodir', '/', default=os.path.join(self.basedir, '../build'))
|
|
|
|
self.should_detect = self.params.get('detect', '*', default=False)
|
|
self.trace_file = self.params.get('trace_file', '*')
|
|
|
|
if not os.path.isabs(self.trace_file):
|
|
self.trace_file = os.path.join(self.basedir, self.trace_file)
|
|
|
|
self.json_output = self.params.get('json_output', '*', default=False)
|
|
self.rules_file = self.params.get('rules_file', '*', default=os.path.join(self.basedir, '../rules/falco_rules.yaml'))
|
|
|
|
if not os.path.isabs(self.rules_file):
|
|
self.rules_file = os.path.join(self.basedir, self.rules_file)
|
|
|
|
self.rules_warning = self.params.get('rules_warning', '*', default=False)
|
|
if self.rules_warning == False:
|
|
self.rules_warning = sets.Set()
|
|
else:
|
|
self.rules_warning = sets.Set(self.rules_warning)
|
|
|
|
# Maps from rule name to set of evttypes
|
|
self.rules_events = self.params.get('rules_events', '*', default=False)
|
|
if self.rules_events == False:
|
|
self.rules_events = {}
|
|
else:
|
|
events = {}
|
|
for item in self.rules_events:
|
|
for item2 in item:
|
|
events[item2[0]] = sets.Set(item2[1])
|
|
self.rules_events = events
|
|
|
|
if self.should_detect:
|
|
self.detect_level = self.params.get('detect_level', '*')
|
|
|
|
# Doing this in 2 steps instead of simply using
|
|
# module_is_loaded to avoid logging lsmod output to the log.
|
|
lsmod_output = process.system_output("lsmod", verbose=False)
|
|
|
|
if linux_modules.parse_lsmod_for_module(lsmod_output, 'sysdig_probe') == {}:
|
|
self.log.debug("Loading sysdig kernel module")
|
|
process.run('sudo insmod {}/driver/sysdig-probe.ko'.format(self.falcodir))
|
|
|
|
self.str_variant = self.trace_file
|
|
|
|
def check_rules_warnings(self, res):
|
|
|
|
found_warning = sets.Set()
|
|
|
|
for match in re.finditer('Rule ([^:]+): warning \(([^)]+)\):', res.stderr):
|
|
rule = match.group(1)
|
|
warning = match.group(2)
|
|
found_warning.add(rule)
|
|
|
|
self.log.debug("Expected warning rules: {}".format(self.rules_warning))
|
|
self.log.debug("Actual warning rules: {}".format(found_warning))
|
|
|
|
if found_warning != self.rules_warning:
|
|
self.fail("Expected rules with warnings {} does not match actual rules with warnings {}".format(self.rules_warning, found_warning))
|
|
|
|
def check_rules_events(self, res):
|
|
|
|
found_events = {}
|
|
|
|
for match in re.finditer('Event types for rule ([^:]+): (\S+)', res.stderr):
|
|
rule = match.group(1)
|
|
events = sets.Set(match.group(2).split(","))
|
|
found_events[rule] = events
|
|
|
|
self.log.debug("Expected events for rules: {}".format(self.rules_events))
|
|
self.log.debug("Actual events for rules: {}".format(found_events))
|
|
|
|
for rule in found_events.keys():
|
|
if found_events.get(rule) != self.rules_events.get(rule):
|
|
self.fail("rule {}: expected events {} differs from actual events {}".format(rule, self.rules_events.get(rule), found_events.get(rule)))
|
|
|
|
def check_detections(self, res):
|
|
# Get the number of events detected.
|
|
match = re.search('Events detected: (\d+)', res.stdout)
|
|
if match is None:
|
|
self.fail("Could not find a line 'Events detected: <count>' in falco output")
|
|
|
|
events_detected = int(match.group(1))
|
|
|
|
if not self.should_detect and events_detected > 0:
|
|
self.fail("Detected {} events when should have detected none".format(events_detected))
|
|
|
|
if self.should_detect:
|
|
if events_detected == 0:
|
|
self.fail("Detected {} events when should have detected > 0".format(events_detected))
|
|
|
|
level_line = '(?i){}: (\d+)'.format(self.detect_level)
|
|
match = re.search(level_line, res.stdout)
|
|
|
|
if match is None:
|
|
self.fail("Could not find a line '{}: <count>' in falco output".format(self.detect_level))
|
|
|
|
events_detected = int(match.group(1))
|
|
|
|
if not events_detected > 0:
|
|
self.fail("Detected {} events at level {} when should have detected > 0".format(events_detected, self.detect_level))
|
|
|
|
def check_json_output(self, res):
|
|
if self.json_output:
|
|
# Just verify that any lines starting with '{' are valid json objects.
|
|
# Doesn't do any deep inspection of the contents.
|
|
for line in res.stdout.splitlines():
|
|
if line.startswith('{'):
|
|
obj = json.loads(line)
|
|
for attr in ['time', 'rule', 'priority', 'output']:
|
|
if not attr in obj:
|
|
self.fail("Falco JSON object {} does not contain property \"{}\"".format(line, attr))
|
|
|
|
def test(self):
|
|
self.log.info("Trace file %s", self.trace_file)
|
|
|
|
# Run the provided trace file though falco
|
|
cmd = '{}/userspace/falco/falco -r {} -c {}/../falco.yaml -e {} -o json_output={} -v'.format(
|
|
self.falcodir, self.rules_file, self.falcodir, self.trace_file, self.json_output)
|
|
|
|
self.falco_proc = process.SubProcess(cmd)
|
|
|
|
res = self.falco_proc.run(timeout=180, sig=9)
|
|
|
|
if res.exit_status != 0:
|
|
self.error("Falco command \"{}\" exited with non-zero return value {}".format(
|
|
cmd, res.exit_status))
|
|
|
|
self.check_rules_warnings(res)
|
|
if len(self.rules_events) > 0:
|
|
self.check_rules_events(res)
|
|
self.check_detections(res)
|
|
self.check_json_output(res)
|
|
pass
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|