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.
289 lines
8.0 KiB
Lua
289 lines
8.0 KiB
Lua
--[[
|
|
Compile and install falco rules.
|
|
|
|
This module exports functions that are called from falco c++-side to compile and install a set of rules.
|
|
|
|
--]]
|
|
|
|
local compiler = require "compiler"
|
|
local yaml = require"lyaml"
|
|
|
|
|
|
--[[
|
|
Traverse AST, adding the passed-in 'index' to each node that contains a relational expression
|
|
--]]
|
|
local function mark_relational_nodes(ast, index)
|
|
local t = ast.type
|
|
|
|
if t == "BinaryBoolOp" then
|
|
mark_relational_nodes(ast.left, index)
|
|
mark_relational_nodes(ast.right, index)
|
|
|
|
elseif t == "UnaryBoolOp" then
|
|
mark_relational_nodes(ast.argument, index)
|
|
|
|
elseif t == "BinaryRelOp" then
|
|
ast.index = index
|
|
|
|
elseif t == "UnaryRelOp" then
|
|
ast.index = index
|
|
|
|
else
|
|
error ("Unexpected type in mark_relational_nodes: "..t)
|
|
end
|
|
end
|
|
|
|
function map(f, arr)
|
|
local res = {}
|
|
for i,v in ipairs(arr) do
|
|
res[i] = f(v)
|
|
end
|
|
return res
|
|
end
|
|
|
|
|
|
--[[
|
|
Take a filter AST and set it up in the libsinsp runtime, using the filter API.
|
|
--]]
|
|
local function install_filter(node, parent_bool_op)
|
|
local t = node.type
|
|
|
|
if t == "BinaryBoolOp" then
|
|
|
|
-- "nesting" (the runtime equivalent of placing parens in syntax) is
|
|
-- never necessary when we have identical successive operators. so we
|
|
-- avoid it as a runtime performance optimization.
|
|
if (not(node.operator == parent_bool_op)) then
|
|
filter.nest() -- io.write("(")
|
|
end
|
|
|
|
install_filter(node.left, node.operator)
|
|
filter.bool_op(node.operator) -- io.write(" "..node.operator.." ")
|
|
install_filter(node.right, node.operator)
|
|
|
|
if (not (node.operator == parent_bool_op)) then
|
|
filter.unnest() -- io.write(")")
|
|
end
|
|
|
|
elseif t == "UnaryBoolOp" then
|
|
filter.nest() --io.write("(")
|
|
filter.bool_op(node.operator) -- io.write(" "..node.operator.." ")
|
|
install_filter(node.argument)
|
|
filter.unnest() -- io.write(")")
|
|
|
|
elseif t == "BinaryRelOp" then
|
|
if (node.operator == "in") then
|
|
elements = map(function (el) return el.value end, node.right.elements)
|
|
filter.rel_expr(node.left.value, node.operator, elements, node.index)
|
|
else
|
|
filter.rel_expr(node.left.value, node.operator, node.right.value, node.index)
|
|
end
|
|
-- io.write(node.left.value.." "..node.operator.." "..node.right.value)
|
|
|
|
elseif t == "UnaryRelOp" then
|
|
filter.rel_expr(node.argument.value, node.operator, node.index)
|
|
--io.write(node.argument.value.." "..node.operator)
|
|
|
|
else
|
|
error ("Unexpected type in install_filter: "..t)
|
|
end
|
|
end
|
|
|
|
function set_output(output_format, state)
|
|
|
|
if(output_ast.type == "OutputFormat") then
|
|
|
|
local format
|
|
|
|
else
|
|
error ("Unexpected type in set_output: ".. output_ast.type)
|
|
end
|
|
end
|
|
|
|
-- Note that the rules_by_name and rules_by_idx refer to the same rule
|
|
-- object. The by_name index is used for things like describing rules,
|
|
-- and the by_idx index is used to map the relational node index back
|
|
-- to a rule.
|
|
local state = {macros={}, lists={}, filter_ast=nil, rules_by_name={}, n_rules=0, rules_by_idx={}}
|
|
|
|
function load_rules(rules_content, rules_mgr, verbose, all_events)
|
|
|
|
compiler.set_verbose(verbose)
|
|
compiler.set_all_events(all_events)
|
|
|
|
local rules = yaml.load(rules_content)
|
|
|
|
if rules == nil then
|
|
-- An empty rules file is acceptable
|
|
return
|
|
end
|
|
|
|
for i,v in ipairs(rules) do -- iterate over yaml list
|
|
|
|
if (not (type(v) == "table")) then
|
|
error ("Unexpected element of type " ..type(v)..". Each element should be a yaml associative array.")
|
|
end
|
|
|
|
if (v['macro']) then
|
|
local ast = compiler.compile_macro(v['condition'], state.lists)
|
|
state.macros[v['macro']] = ast.filter.value
|
|
|
|
elseif (v['list']) then
|
|
-- list items are represented in yaml as a native list, so no
|
|
-- parsing necessary
|
|
local items = {}
|
|
|
|
-- List items may be references to other lists, so go through
|
|
-- the items and expand any references to the items in the list
|
|
for i, item in ipairs(v['items']) do
|
|
if (state.lists[item] == nil) then
|
|
items[#items+1] = item
|
|
else
|
|
for i, exp_item in ipairs(state.lists[item]) do
|
|
items[#items+1] = exp_item
|
|
end
|
|
end
|
|
end
|
|
|
|
state.lists[v['list']] = items
|
|
|
|
else -- rule
|
|
|
|
if (v['rule'] == nil) then
|
|
error ("Missing name in rule")
|
|
end
|
|
|
|
for i, field in ipairs({'condition', 'output', 'desc', 'priority'}) do
|
|
if (v[field] == nil) then
|
|
error ("Missing "..field.." in rule with name "..v['rule'])
|
|
end
|
|
end
|
|
|
|
state.rules_by_name[v['rule']] = v
|
|
|
|
local filter_ast, evttypes = compiler.compile_filter(v['rule'], v['condition'],
|
|
state.macros, state.lists)
|
|
|
|
if (filter_ast.type == "Rule") then
|
|
state.n_rules = state.n_rules + 1
|
|
|
|
state.rules_by_idx[state.n_rules] = v
|
|
|
|
-- Store the index of this formatter in each relational expression that
|
|
-- this rule contains.
|
|
-- This index will eventually be stamped in events passing this rule, and
|
|
-- we'll use it later to determine which output to display when we get an
|
|
-- event.
|
|
mark_relational_nodes(filter_ast.filter.value, state.n_rules)
|
|
|
|
install_filter(filter_ast.filter.value)
|
|
|
|
-- Pass the filter and event types back up
|
|
falco_rules.add_filter(rules_mgr, v['rule'], evttypes)
|
|
|
|
-- Rule ASTs are merged together into one big AST, with "OR" between each
|
|
-- rule.
|
|
if (state.filter_ast == nil) then
|
|
state.filter_ast = filter_ast.filter.value
|
|
else
|
|
state.filter_ast = { type = "BinaryBoolOp", operator = "or", left = state.filter_ast, right = filter_ast.filter.value }
|
|
end
|
|
else
|
|
error ("Unexpected type in load_rule: "..filter_ast.type)
|
|
end
|
|
end
|
|
end
|
|
|
|
io.flush()
|
|
end
|
|
|
|
local rule_fmt = "%-50s %s"
|
|
|
|
-- http://lua-users.org/wiki/StringRecipes, with simplifications and bugfixes
|
|
local function wrap(str, limit, indent)
|
|
indent = indent or ""
|
|
limit = limit or 72
|
|
local here = 1
|
|
return str:gsub("(%s+)()(%S+)()",
|
|
function(sp, st, word, fi)
|
|
if fi-here > limit then
|
|
here = st
|
|
return "\n"..indent..word
|
|
end
|
|
end)
|
|
end
|
|
|
|
local function describe_single_rule(name)
|
|
if (state.rules_by_name[name] == nil) then
|
|
error ("No such rule: "..name)
|
|
end
|
|
|
|
-- Wrap the description into an multiple lines each of length ~ 60
|
|
-- chars, with indenting to line up with the first line.
|
|
local wrapped = wrap(state.rules_by_name[name]['desc'], 60, string.format(rule_fmt, "", ""))
|
|
|
|
local line = string.format(rule_fmt, name, wrapped)
|
|
print(line)
|
|
print()
|
|
end
|
|
|
|
-- If name is nil, describe all rules
|
|
function describe_rule(name)
|
|
|
|
print()
|
|
local line = string.format(rule_fmt, "Rule", "Description")
|
|
print(line)
|
|
line = string.format(rule_fmt, "----", "-----------")
|
|
print(line)
|
|
|
|
if name == nil then
|
|
for rulename, rule in pairs(state.rules_by_name) do
|
|
describe_single_rule(rulename)
|
|
end
|
|
else
|
|
describe_single_rule(name)
|
|
end
|
|
end
|
|
|
|
local rule_output_counts = {total=0, by_priority={}, by_name={}}
|
|
|
|
function on_event(evt_, rule_id)
|
|
|
|
if state.rules_by_idx[rule_id] == nil then
|
|
error ("rule_loader.on_event(): event with invalid rule_id: ", rule_id)
|
|
end
|
|
|
|
rule_output_counts.total = rule_output_counts.total + 1
|
|
local rule = state.rules_by_idx[rule_id]
|
|
|
|
if rule_output_counts.by_priority[rule.priority] == nil then
|
|
rule_output_counts.by_priority[rule.priority] = 1
|
|
else
|
|
rule_output_counts.by_priority[rule.priority] = rule_output_counts.by_priority[rule.priority] + 1
|
|
end
|
|
|
|
if rule_output_counts.by_name[rule.rule] == nil then
|
|
rule_output_counts.by_name[rule.rule] = 1
|
|
else
|
|
rule_output_counts.by_name[rule.rule] = rule_output_counts.by_name[rule.rule] + 1
|
|
end
|
|
|
|
return rule.rule, rule.priority, rule.output
|
|
end
|
|
|
|
function print_stats()
|
|
print("Events detected: "..rule_output_counts.total)
|
|
print("Rule counts by severity:")
|
|
for priority, count in pairs(rule_output_counts.by_priority) do
|
|
print (" "..priority..": "..count)
|
|
end
|
|
|
|
print("Triggered rules by rule name:")
|
|
for name, count in pairs(rule_output_counts.by_name) do
|
|
print (" "..name..": "..count)
|
|
end
|
|
end
|
|
|
|
|
|
|