mirror of
https://github.com/falcosecurity/falco.git
synced 2026-01-24 14:32:34 +00:00
Also validate macros when they are parsed. Macros are also validated as a part of rules being parsed, but it's possible to have an individual rules file containing only macros, or a macro not explicitly tied to any rule. In this case, it's useful to be able to check the macro to see if it contains dangling macro references.
464 lines
13 KiB
Lua
464 lines
13 KiB
Lua
--
|
|
-- Copyright (C) 2016 Draios inc.
|
|
--
|
|
-- This file is part of falco.
|
|
--
|
|
-- falco is free software; you can redistribute it and/or modify
|
|
-- it under the terms of the GNU General Public License version 2 as
|
|
-- published by the Free Software Foundation.
|
|
--
|
|
-- falco is distributed in the hope that it will be useful,
|
|
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
-- GNU General Public License for more details.
|
|
--
|
|
-- You should have received a copy of the GNU General Public License
|
|
-- along with falco. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
--[[
|
|
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" or node.operator == "pmatch") 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={}, macros_by_name={}, lists_by_name={},
|
|
n_rules=0, rules_by_idx={}, ordered_rule_names={}, ordered_macro_names={}, ordered_list_names={}}
|
|
|
|
local function reset_rules(rules_mgr)
|
|
falco_rules.clear_filters(rules_mgr)
|
|
state.n_rules = 0
|
|
state.rules_by_idx = {}
|
|
state.macros = {}
|
|
state.lists = {}
|
|
end
|
|
|
|
-- From http://lua-users.org/wiki/TableUtils
|
|
--
|
|
function table.val_to_str ( v )
|
|
if "string" == type( v ) then
|
|
v = string.gsub( v, "\n", "\\n" )
|
|
if string.match( string.gsub(v,"[^'\"]",""), '^"+$' ) then
|
|
return "'" .. v .. "'"
|
|
end
|
|
return '"' .. string.gsub(v,'"', '\\"' ) .. '"'
|
|
else
|
|
return "table" == type( v ) and table.tostring( v ) or
|
|
tostring( v )
|
|
end
|
|
end
|
|
|
|
function table.key_to_str ( k )
|
|
if "string" == type( k ) and string.match( k, "^[_%a][_%a%d]*$" ) then
|
|
return k
|
|
else
|
|
return "[" .. table.val_to_str( k ) .. "]"
|
|
end
|
|
end
|
|
|
|
function table.tostring( tbl )
|
|
local result, done = {}, {}
|
|
for k, v in ipairs( tbl ) do
|
|
table.insert( result, table.val_to_str( v ) )
|
|
done[ k ] = true
|
|
end
|
|
for k, v in pairs( tbl ) do
|
|
if not done[ k ] then
|
|
table.insert( result,
|
|
table.key_to_str( k ) .. "=" .. table.val_to_str( v ) )
|
|
end
|
|
end
|
|
return "{" .. table.concat( result, "," ) .. "}"
|
|
end
|
|
|
|
|
|
function load_rules(rules_content, rules_mgr, verbose, all_events, extra, replace_container_info)
|
|
|
|
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
|
|
|
|
if type(rules) ~= "table" then
|
|
error("Rules content \""..rules_content.."\" is not yaml")
|
|
end
|
|
|
|
-- Iterate over yaml list. In this pass, all we're doing is
|
|
-- populating the set of rules, macros, and lists. We're not
|
|
-- expanding/compiling anything yet. All that will happen in a
|
|
-- second pass
|
|
for i,v in ipairs(rules) do
|
|
|
|
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
|
|
if state.macros_by_name[v['macro']] == nil then
|
|
state.ordered_macro_names[#state.ordered_macro_names+1] = v['macro']
|
|
end
|
|
|
|
for i, field in ipairs({'condition'}) do
|
|
if (v[field] == nil) then
|
|
error ("Missing "..field.." in macro with name "..v['macro'])
|
|
end
|
|
end
|
|
|
|
state.macros_by_name[v['macro']] = v
|
|
|
|
elseif (v['list']) then
|
|
|
|
if state.lists_by_name[v['list']] == nil then
|
|
state.ordered_list_names[#state.ordered_list_names+1] = v['list']
|
|
end
|
|
|
|
for i, field in ipairs({'items'}) do
|
|
if (v[field] == nil) then
|
|
error ("Missing "..field.." in list with name "..v['list'])
|
|
end
|
|
end
|
|
|
|
state.lists_by_name[v['list']] = v
|
|
|
|
elseif (v['rule']) then
|
|
|
|
if (v['rule'] == nil or type(v['rule']) == "table") 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
|
|
|
|
-- Note that we can overwrite rules, but the rules are still
|
|
-- loaded in the order in which they first appeared,
|
|
-- potentially across multiple files.
|
|
if state.rules_by_name[v['rule']] == nil then
|
|
state.ordered_rule_names[#state.ordered_rule_names+1] = v['rule']
|
|
end
|
|
|
|
state.rules_by_name[v['rule']] = v
|
|
|
|
else
|
|
error ("Unknown rule object: "..table.tostring(v))
|
|
end
|
|
end
|
|
|
|
-- We've now loaded all the rules, macros, and list. Now
|
|
-- compile/expand the rules, macros, and lists. We use
|
|
-- ordered_rule_{lists,macros,names} to compile them in the order
|
|
-- in which they appeared in the file(s).
|
|
reset_rules(rules_mgr)
|
|
|
|
for i, name in ipairs(state.ordered_list_names) do
|
|
|
|
local v = state.lists_by_name[name]
|
|
|
|
-- 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
|
|
end
|
|
|
|
for i, name in ipairs(state.ordered_macro_names) do
|
|
|
|
local v = state.macros_by_name[name]
|
|
|
|
local ast = compiler.compile_macro(v['condition'], state.macros, state.lists)
|
|
state.macros[v['macro']] = ast.filter.value
|
|
end
|
|
|
|
for i, name in ipairs(state.ordered_rule_names) do
|
|
|
|
local v = state.rules_by_name[name]
|
|
|
|
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)
|
|
|
|
if (v['tags'] == nil) then
|
|
v['tags'] = {}
|
|
end
|
|
|
|
-- Pass the filter and event types back up
|
|
falco_rules.add_filter(rules_mgr, v['rule'], evttypes, v['tags'])
|
|
|
|
-- 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
|
|
|
|
-- Enable/disable the rule
|
|
if (v['enabled'] == nil) then
|
|
v['enabled'] = true
|
|
end
|
|
|
|
if (v['enabled'] == false) then
|
|
falco_rules.enable_rule(rules_mgr, v['rule'], 0)
|
|
end
|
|
|
|
-- If the format string contains %container.info, replace it
|
|
-- with extra. Otherwise, add extra onto the end of the format
|
|
-- string.
|
|
if string.find(v['output'], "%container.info", nil, true) ~= nil then
|
|
|
|
-- There may not be any extra, or we're not supposed
|
|
-- to replace it, in which case we use the generic
|
|
-- "%container.name (id=%container.id)"
|
|
if replace_container_info == false then
|
|
v['output'] = string.gsub(v['output'], "%%container.info", "%%container.name (id=%%container.id)")
|
|
if extra ~= "" then
|
|
v['output'] = v['output'].." "..extra
|
|
end
|
|
else
|
|
safe_extra = string.gsub(extra, "%%", "%%%%")
|
|
v['output'] = string.gsub(v['output'], "%%container.info", safe_extra)
|
|
end
|
|
else
|
|
-- Just add the extra to the end
|
|
if extra ~= "" then
|
|
v['output'] = v['output'].." "..extra
|
|
end
|
|
end
|
|
|
|
-- Ensure that the output field is properly formatted by
|
|
-- creating a formatter from it. Any error will be thrown
|
|
-- up to the top level.
|
|
formatter = formats.formatter(v['output'])
|
|
formats.free_formatter(formatter)
|
|
else
|
|
error ("Unexpected type in load_rule: "..filter_ast.type)
|
|
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
|
|
|
|
-- Prefix output with '*' so formatting is permissive
|
|
output = "*"..rule.output
|
|
|
|
return rule.rule, rule.priority, 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
|
|
|
|
|
|
|