mirror of
https://github.com/falcosecurity/falco.git
synced 2025-09-29 06:07:25 +00:00
1177 lines
34 KiB
Lua
1177 lines
34 KiB
Lua
-- Copyright (C) 2019 The Falco Authors.
|
|
--
|
|
-- Licensed under the Apache License, Version 2.0 (the "License");
|
|
-- you may not use this file except in compliance with the License.
|
|
-- You may obtain a copy of the License at
|
|
--
|
|
-- http://www.apache.org/licenses/LICENSE-2.0
|
|
--
|
|
-- Unless required by applicable law or agreed to in writing, software
|
|
-- distributed under the License is distributed on an "AS IS" BASIS,
|
|
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
-- See the License for the specific language governing permissions and
|
|
-- limitations under the License.
|
|
--
|
|
|
|
--[[
|
|
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 sinsp_rule_utils = require "sinsp_rule_utils"
|
|
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
|
|
|
|
|
|
-- Permissive for case and for common abbreviations.
|
|
priorities = {
|
|
Emergency=0, Alert=1, Critical=2, Error=3, Warning=4, Notice=5, Informational=5, Debug=7,
|
|
emergency=0, alert=1, critical=2, error=3, warning=4, notice=5, informational=5, debug=7,
|
|
EMERGENCY=0, ALERT=1, CRITICAL=2, ERROR=3, WARNING=4, NOTICE=5, INFORMATIONAL=5, DEBUG=7,
|
|
INFO=5, info=5
|
|
}
|
|
|
|
--[[
|
|
Take a filter AST and set it up in the libsinsp runtime, using the filter API.
|
|
--]]
|
|
local function install_filter(node, filter_api_lib, lua_parser, 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_api_lib.nest(lua_parser) -- io.write("(")
|
|
end
|
|
|
|
install_filter(node.left, filter_api_lib, lua_parser, node.operator)
|
|
filter_api_lib.bool_op(lua_parser, node.operator) -- io.write(" "..node.operator.." ")
|
|
install_filter(node.right, filter_api_lib, lua_parser, node.operator)
|
|
|
|
if (not (node.operator == parent_bool_op)) then
|
|
filter_api_lib.unnest(lua_parser) -- io.write(")")
|
|
end
|
|
|
|
elseif t == "UnaryBoolOp" then
|
|
filter_api_lib.nest(lua_parser) --io.write("(")
|
|
filter_api_lib.bool_op(lua_parser, node.operator) -- io.write(" "..node.operator.." ")
|
|
install_filter(node.argument, filter_api_lib, lua_parser)
|
|
filter_api_lib.unnest(lua_parser) -- io.write(")")
|
|
|
|
elseif t == "BinaryRelOp" then
|
|
if (node.operator == "in" or
|
|
node.operator == "intersects" or
|
|
node.operator == "pmatch") then
|
|
elements = map(function (el) return el.value end, node.right.elements)
|
|
filter_api_lib.rel_expr(lua_parser, node.left.value, node.operator, elements, node.index)
|
|
else
|
|
filter_api_lib.rel_expr(lua_parser, 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_api_lib.rel_expr(lua_parser, 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
|
|
|
|
-- This should be keep in sync with parser.lua
|
|
defined_comp_operators = {
|
|
["="]=1,
|
|
["=="] = 1,
|
|
["!="] = 1,
|
|
["<="] = 1,
|
|
[">="] = 1,
|
|
["<"] = 1,
|
|
[">"] = 1,
|
|
["contains"] = 1,
|
|
["icontains"] = 1,
|
|
["glob"] = 1,
|
|
["startswith"] = 1,
|
|
["endswith"] = 1,
|
|
["in"] = 1,
|
|
["intersects"] = 1,
|
|
["pmatch"] = 1
|
|
}
|
|
|
|
defined_list_comp_operators = {
|
|
["in"] = 1,
|
|
["intersects"] = 1,
|
|
["pmatch"] = 1
|
|
}
|
|
|
|
-- 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={},
|
|
skipped_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
|
|
|
|
-- Split rules_content by lines and also remember the line numbers for
|
|
-- each top -level object. Returns a table of lines and a table of
|
|
-- line numbers for objects.
|
|
|
|
function split_lines(rules_content)
|
|
lines = {}
|
|
indices = {}
|
|
|
|
idx = 1
|
|
last_pos = 1
|
|
pos = string.find(rules_content, "\n", 1, true)
|
|
|
|
while pos ~= nil do
|
|
line = string.sub(rules_content, last_pos, pos-1)
|
|
if line ~= "" then
|
|
lines[#lines+1] = line
|
|
if string.len(line) >= 3 and string.sub(line, 1, 3) == "---" then
|
|
-- Document marker, skip
|
|
elseif string.sub(line, 1, 1) == '-' then
|
|
indices[#indices+1] = idx
|
|
end
|
|
|
|
idx = idx + 1
|
|
end
|
|
|
|
last_pos = pos+1
|
|
pos = string.find(rules_content, "\n", pos+1, true)
|
|
end
|
|
|
|
if last_pos < string.len(rules_content) then
|
|
line = string.sub(rules_content, last_pos)
|
|
lines[#lines+1] = line
|
|
if string.sub(line, 1, 1) == '-' then
|
|
indices[#indices+1] = idx
|
|
end
|
|
|
|
idx = idx + 1
|
|
end
|
|
|
|
-- Add a final index for last line in document
|
|
indices[#indices+1] = idx
|
|
|
|
return lines, indices
|
|
end
|
|
|
|
function get_orig_yaml_obj(rules_lines, row)
|
|
local ret = ""
|
|
|
|
idx = row
|
|
while (idx <= #rules_lines) do
|
|
ret = ret..rules_lines[idx].."\n"
|
|
idx = idx + 1
|
|
|
|
if idx > #rules_lines or rules_lines[idx] == "" or string.sub(rules_lines[idx], 1, 1) == '-' then
|
|
break
|
|
end
|
|
end
|
|
|
|
return ret
|
|
end
|
|
|
|
function get_lines(rules_lines, row, num_lines)
|
|
local ret = ""
|
|
|
|
idx = row
|
|
while (idx < (row + num_lines) and idx <= #rules_lines) do
|
|
ret = ret..rules_lines[idx].."\n"
|
|
idx = idx + 1
|
|
end
|
|
|
|
return ret
|
|
end
|
|
|
|
function quote_item(item)
|
|
|
|
-- Add quotes if the string contains spaces and doesn't start/end
|
|
-- w/ quotes
|
|
if string.find(item, " ") then
|
|
if string.sub(item, 1, 1) ~= "'" and string.sub(item, 1, 1) ~= '"' then
|
|
item = "\""..item.."\""
|
|
end
|
|
end
|
|
|
|
return item
|
|
end
|
|
|
|
function paren_item(item)
|
|
if string.sub(item, 1, 1) ~= "(" then
|
|
item = "("..item..")"
|
|
end
|
|
|
|
return item
|
|
end
|
|
|
|
function build_error(rules_lines, row, num_lines, err)
|
|
local ret = err.."\n---\n"..get_lines(rules_lines, row, num_lines).."---"
|
|
|
|
return {ret}
|
|
end
|
|
|
|
function build_error_with_context(ctx, err)
|
|
local ret = err.."\n---\n"..ctx.."---"
|
|
return {ret}
|
|
end
|
|
|
|
function validate_exception_item_multi_fields(eitem, context)
|
|
|
|
local name = eitem['name']
|
|
local fields = eitem['fields']
|
|
local values = eitem['values']
|
|
local comps = eitem['comps']
|
|
|
|
if comps == nil then
|
|
comps = {}
|
|
for c=1,#fields do
|
|
table.insert(comps, "=")
|
|
end
|
|
eitem['comps'] = comps
|
|
else
|
|
if #fields ~= #comps then
|
|
return false, build_error_with_context(context, "Rule exception item "..name..": fields and comps lists must have equal length"), warnings
|
|
end
|
|
end
|
|
for k, fname in ipairs(fields) do
|
|
if not is_defined_filter(fname) then
|
|
return false, build_error_with_context(context, "Rule exception item "..name..": field name "..fname.." is not a supported filter field"), warnings
|
|
end
|
|
end
|
|
for k, comp in ipairs(comps) do
|
|
if defined_comp_operators[comp] == nil then
|
|
return false, build_error_with_context(context, "Rule exception item "..name..": comparison operator "..comp.." is not a supported comparison operator"), warnings
|
|
end
|
|
end
|
|
end
|
|
|
|
function validate_exception_item_single_field(eitem, context)
|
|
|
|
local name = eitem['name']
|
|
local fields = eitem['fields']
|
|
local values = eitem['values']
|
|
local comps = eitem['comps']
|
|
|
|
if comps == nil then
|
|
eitem['comps'] = "in"
|
|
comps = eitem['comps']
|
|
else
|
|
if type(fields) ~= "string" or type(comps) ~= "string" then
|
|
return false, build_error_with_context(context, "Rule exception item "..name..": fields and comps must both be strings"), warnings
|
|
end
|
|
end
|
|
if not is_defined_filter(fields) then
|
|
return false, build_error_with_context(context, "Rule exception item "..name..": field name "..fields.." is not a supported filter field"), warnings
|
|
end
|
|
if defined_comp_operators[comps] == nil then
|
|
return false, build_error_with_context(context, "Rule exception item "..name..": comparison operator "..comps.." is not a supported comparison operator"), warnings
|
|
end
|
|
end
|
|
|
|
function is_defined_filter(filter)
|
|
if defined_noarg_filters[filter] ~= nil then
|
|
return true
|
|
else
|
|
bracket_idx = string.find(filter, "[", 1, true)
|
|
|
|
if bracket_idx ~= nil then
|
|
subfilter = string.sub(filter, 1, bracket_idx-1)
|
|
|
|
if defined_arg_filters[subfilter] ~= nil then
|
|
return true
|
|
end
|
|
end
|
|
|
|
dot_idx = string.find(filter, ".", 1, true)
|
|
|
|
while dot_idx ~= nil do
|
|
subfilter = string.sub(filter, 1, dot_idx-1)
|
|
|
|
if defined_arg_filters[subfilter] ~= nil then
|
|
return true
|
|
end
|
|
|
|
dot_idx = string.find(filter, ".", dot_idx+1, true)
|
|
end
|
|
end
|
|
|
|
return false
|
|
end
|
|
|
|
|
|
function load_rules_doc(rules_mgr, doc, load_state)
|
|
|
|
local warnings = {}
|
|
|
|
-- 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(doc) do
|
|
|
|
load_state.cur_item_idx = load_state.cur_item_idx + 1
|
|
|
|
-- Save back the original object as it appeared in the file. Will be used to provide context.
|
|
local context = get_orig_yaml_obj(load_state.lines,
|
|
load_state.indices[load_state.cur_item_idx])
|
|
|
|
if (not (type(v) == "table")) then
|
|
return false, build_error_with_context(context, "Unexpected element of type " ..type(v)..". Each element should be a yaml associative array."), warnings
|
|
end
|
|
|
|
v['context'] = context
|
|
|
|
if (v['required_engine_version']) then
|
|
load_state.required_engine_version = v['required_engine_version']
|
|
if type(load_state.required_engine_version) ~= "number" then
|
|
return false, build_error_with_context(v['context'], "Value of required_engine_version must be a number")
|
|
end
|
|
|
|
if falco_rules.engine_version(rules_mgr) < v['required_engine_version'] then
|
|
return false, build_error_with_context(v['context'], "Rules require engine version "..v['required_engine_version']..", but engine version is "..falco_rules.engine_version(rules_mgr)), warnings
|
|
end
|
|
|
|
elseif (v['macro']) then
|
|
|
|
if (v['macro'] == nil or type(v['macro']) == "table") then
|
|
return false, build_error_with_context(v['context'], "Macro name is empty"), warnings
|
|
end
|
|
|
|
if v['source'] == nil then
|
|
v['source'] = "syscall"
|
|
end
|
|
|
|
if state.macros_by_name[v['macro']] == nil then
|
|
state.ordered_macro_names[#state.ordered_macro_names+1] = v['macro']
|
|
end
|
|
|
|
for j, field in ipairs({'condition'}) do
|
|
if (v[field] == nil) then
|
|
return false, build_error_with_context(v['context'], "Macro must have property "..field), warnings
|
|
end
|
|
end
|
|
|
|
-- Possibly append to the condition field of an existing macro
|
|
append = false
|
|
|
|
if v['append'] then
|
|
append = v['append']
|
|
end
|
|
|
|
if append then
|
|
if state.macros_by_name[v['macro']] == nil then
|
|
return false, build_error_with_context(v['context'], "Macro " ..v['macro'].. " has 'append' key but no macro by that name already exists"), warnings
|
|
end
|
|
|
|
state.macros_by_name[v['macro']]['condition'] = state.macros_by_name[v['macro']]['condition'] .. " " .. v['condition']
|
|
|
|
-- Add the current object to the context of the base macro
|
|
state.macros_by_name[v['macro']]['context'] = state.macros_by_name[v['macro']]['context'].."\n"..v['context']
|
|
|
|
else
|
|
state.macros_by_name[v['macro']] = v
|
|
end
|
|
|
|
elseif (v['list']) then
|
|
|
|
if (v['list'] == nil or type(v['list']) == "table") then
|
|
return false, build_error_with_context(v['context'], "List name is empty"), warnings
|
|
end
|
|
|
|
if state.lists_by_name[v['list']] == nil then
|
|
state.ordered_list_names[#state.ordered_list_names+1] = v['list']
|
|
end
|
|
|
|
for j, field in ipairs({'items'}) do
|
|
if (v[field] == nil) then
|
|
return false, build_error_with_context(v['context'], "List must have property "..field), warnings
|
|
end
|
|
end
|
|
|
|
-- Possibly append to an existing list
|
|
append = false
|
|
|
|
if v['append'] then
|
|
append = v['append']
|
|
end
|
|
|
|
if append then
|
|
if state.lists_by_name[v['list']] == nil then
|
|
return false, build_error_with_context(v['context'], "List " ..v['list'].. " has 'append' key but no list by that name already exists"), warnings
|
|
end
|
|
|
|
for j, elem in ipairs(v['items']) do
|
|
table.insert(state.lists_by_name[v['list']]['items'], elem)
|
|
end
|
|
else
|
|
state.lists_by_name[v['list']] = v
|
|
end
|
|
|
|
elseif (v['rule']) then
|
|
|
|
if (v['rule'] == nil or type(v['rule']) == "table") then
|
|
return false, build_error_with_context(v['context'], "Rule name is empty"), warnings
|
|
end
|
|
|
|
if (v['condition'] == nil and v['exceptions'] == nil) then
|
|
return false, build_error_with_context(v['context'], "Rule must have exceptions or condition property"), warnings
|
|
end
|
|
|
|
-- By default, if a rule's condition refers to an unknown
|
|
-- filter like evt.type, etc the loader throws an error.
|
|
if v['skip-if-unknown-filter'] == nil then
|
|
v['skip-if-unknown-filter'] = false
|
|
end
|
|
|
|
if v['source'] == nil then
|
|
v['source'] = "syscall"
|
|
end
|
|
|
|
-- Add an empty exceptions property to the rule if not defined
|
|
if v['exceptions'] == nil then
|
|
v['exceptions'] = {}
|
|
end
|
|
|
|
-- Possibly append to the condition field of an existing rule
|
|
append = false
|
|
|
|
if v['append'] then
|
|
append = v['append']
|
|
end
|
|
|
|
-- Validate the contents of the rule exception
|
|
if next(v['exceptions']) ~= nil then
|
|
|
|
-- This validation only applies if append=false. append=true validation is handled below
|
|
if append == false then
|
|
|
|
for _, eitem in ipairs(v['exceptions']) do
|
|
|
|
if eitem['name'] == nil then
|
|
return false, build_error_with_context(v['context'], "Rule exception item must have name property"), warnings
|
|
end
|
|
|
|
if eitem['fields'] == nil then
|
|
return false, build_error_with_context(v['context'], "Rule exception item "..eitem['name']..": must have fields property with a list of fields"), warnings
|
|
end
|
|
|
|
if eitem['values'] == nil then
|
|
-- An empty values array is okay
|
|
eitem['values'] = {}
|
|
end
|
|
|
|
-- Different handling if the fields property is a single item vs a list
|
|
local valid, err
|
|
if type(eitem['fields']) == "table" then
|
|
valid, err = validate_exception_item_multi_fields(eitem, v['context'])
|
|
else
|
|
valid, err = validate_exception_item_single_field(eitem, v['context'])
|
|
end
|
|
|
|
if valid == false then
|
|
return valid, err
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
if append then
|
|
|
|
if state.rules_by_name[v['rule']] == nil then
|
|
if state.skipped_rules_by_name[v['rule']] == nil then
|
|
return false, build_error_with_context(v['context'], "Rule " ..v['rule'].. " has 'append' key but no rule by that name already exists"), warnings
|
|
end
|
|
else
|
|
|
|
if next(v['exceptions']) ~= nil then
|
|
|
|
for _, eitem in ipairs(v['exceptions']) do
|
|
local name = eitem['name']
|
|
local fields = eitem['fields']
|
|
local comps = eitem['comps']
|
|
|
|
if name == nil then
|
|
return false, build_error_with_context(v['context'], "Rule exception item must have name property"), warnings
|
|
end
|
|
|
|
-- You can't append exception fields or comps to a rule
|
|
if fields ~= nil then
|
|
return false, build_error_with_context(v['context'], "Can not append exception fields to existing rule, only values"), warnings
|
|
end
|
|
|
|
if comps ~= nil then
|
|
return false, build_error_with_context(v['context'], "Can not append exception comps to existing rule, only values"), warnings
|
|
end
|
|
|
|
-- You can append values. They are added to the
|
|
-- corresponding name, if it exists. If no
|
|
-- exception with that name exists, add a
|
|
-- warning.
|
|
if eitem['values'] ~= nil then
|
|
local found=false
|
|
for _, reitem in ipairs(state.rules_by_name[v['rule']]['exceptions']) do
|
|
if reitem['name'] == eitem['name'] then
|
|
found=true
|
|
for _, values in ipairs(eitem['values']) do
|
|
reitem['values'][#reitem['values'] + 1] = values
|
|
end
|
|
end
|
|
end
|
|
|
|
if found == false then
|
|
warnings[#warnings + 1] = "Rule "..v['rule'].." with append=true: no set of fields matching name "..eitem['name']
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
if v['condition'] ~= nil then
|
|
state.rules_by_name[v['rule']]['condition'] = state.rules_by_name[v['rule']]['condition'] .. " " .. v['condition']
|
|
end
|
|
|
|
-- Add the current object to the context of the base rule
|
|
state.rules_by_name[v['rule']]['context'] = state.rules_by_name[v['rule']]['context'].."\n"..v['context']
|
|
end
|
|
|
|
else
|
|
|
|
for j, field in ipairs({'condition', 'output', 'desc', 'priority'}) do
|
|
if (v[field] == nil) then
|
|
return false, build_error_with_context(v['context'], "Rule must have property "..field), warnings
|
|
end
|
|
end
|
|
|
|
-- Convert the priority-as-string to a priority-as-number now
|
|
v['priority_num'] = priorities[v['priority']]
|
|
|
|
if v['priority_num'] == nil then
|
|
error("Invalid priority level: "..v['priority'])
|
|
end
|
|
|
|
if v['priority_num'] <= load_state.min_priority then
|
|
-- 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
|
|
|
|
-- The output field might be a folded-style, which adds a
|
|
-- newline to the end. Remove any trailing newlines.
|
|
v['output'] = compiler.trim(v['output'])
|
|
|
|
state.rules_by_name[v['rule']] = v
|
|
else
|
|
state.skipped_rules_by_name[v['rule']] = v
|
|
end
|
|
end
|
|
else
|
|
local context = v['context']
|
|
|
|
arr = build_error_with_context(context, "Unknown top level object: "..table.tostring(v))
|
|
warnings[#warnings + 1] = arr[1]
|
|
end
|
|
end
|
|
|
|
return true, {}, warnings
|
|
end
|
|
|
|
-- cond and not ((proc.name=apk and fd.directory=/usr/lib/alpine) or (proc.name=npm and fd.directory=/usr/node/bin) or ...)
|
|
-- Populates exfields with all fields used
|
|
function build_exception_condition_string_multi_fields(eitem, exfields)
|
|
|
|
local fields = eitem['fields']
|
|
local comps = eitem['comps']
|
|
|
|
local icond = "("
|
|
|
|
for i, values in ipairs(eitem['values']) do
|
|
|
|
if #fields ~= #values then
|
|
return nil, "Exception item "..eitem['name']..": fields and values lists must have equal length"
|
|
end
|
|
|
|
if icond ~= "(" then
|
|
icond=icond.." or "
|
|
end
|
|
|
|
icond=icond.."("
|
|
|
|
for k=1,#fields do
|
|
if k > 1 then
|
|
icond=icond.." and "
|
|
end
|
|
local ival = values[k]
|
|
local istr = ""
|
|
|
|
-- If ival is a table, express it as (titem1, titem2, etc)
|
|
if type(ival) == "table" then
|
|
istr = "("
|
|
for _, item in ipairs(ival) do
|
|
if istr ~= "(" then
|
|
istr = istr..", "
|
|
end
|
|
istr = istr..quote_item(item)
|
|
end
|
|
istr = istr..")"
|
|
else
|
|
-- If the corresponding operator is one that works on lists, possibly add surrounding parentheses.
|
|
if defined_list_comp_operators[comps[k]] then
|
|
istr = paren_item(ival)
|
|
else
|
|
-- Quote the value if not already quoted
|
|
istr = quote_item(ival)
|
|
end
|
|
end
|
|
|
|
icond = icond..fields[k].." "..comps[k].." "..istr
|
|
exfields[fields[k]] = true
|
|
end
|
|
|
|
icond=icond..")"
|
|
end
|
|
|
|
icond = icond..")"
|
|
|
|
-- Don't return a trivially empty condition string
|
|
if icond == "()" then
|
|
icond = ""
|
|
end
|
|
|
|
return icond, nil
|
|
|
|
end
|
|
|
|
function build_exception_condition_string_single_field(eitem, exfields)
|
|
|
|
local icond = ""
|
|
|
|
for i, value in ipairs(eitem['values']) do
|
|
|
|
if type(value) ~= "string" then
|
|
return "", "Expected values array for item "..eitem['name'].." to contain a list of strings"
|
|
end
|
|
|
|
if icond == "" then
|
|
icond = "("..eitem['fields'].." "..eitem['comps'].." ("
|
|
else
|
|
icond = icond..", "
|
|
end
|
|
|
|
exfields[eitem['fields']] = true
|
|
|
|
icond = icond..quote_item(value)
|
|
end
|
|
|
|
if icond ~= "" then
|
|
icond = icond.."))"
|
|
end
|
|
|
|
return icond, nil
|
|
|
|
end
|
|
|
|
-- Returns:
|
|
-- - Load Result: bool
|
|
-- - required engine version. will be nil when load result is false
|
|
-- - List of Errors
|
|
-- - List of Warnings
|
|
function load_rules(sinsp_lua_parser,
|
|
json_lua_parser,
|
|
rules_content,
|
|
rules_mgr,
|
|
verbose,
|
|
all_events,
|
|
extra,
|
|
replace_container_info,
|
|
min_priority)
|
|
|
|
local warnings = {}
|
|
|
|
local load_state = {lines={}, indices={}, cur_item_idx=0, min_priority=min_priority, required_engine_version=0}
|
|
|
|
load_state.lines, load_state.indices = split_lines(rules_content)
|
|
|
|
local status, docs = pcall(yaml.load, rules_content, { all = true })
|
|
|
|
if status == false then
|
|
local pat = "^([%d]+):([%d]+): "
|
|
-- docs is actually an error string
|
|
|
|
local row = 0
|
|
local col = 0
|
|
|
|
row, col = string.match(docs, pat)
|
|
if row ~= nil and col ~= nil then
|
|
docs = string.gsub(docs, pat, "")
|
|
end
|
|
|
|
row = tonumber(row)
|
|
col = tonumber(col)
|
|
|
|
return false, nil, build_error(load_state.lines, row, 3, docs), warnings
|
|
end
|
|
|
|
if docs == nil then
|
|
-- An empty rules file is acceptable
|
|
return true, load_state.required_engine_version, {}, warnings
|
|
end
|
|
|
|
if type(docs) ~= "table" then
|
|
return false, nil, build_error(load_state.lines, 1, 1, "Rules content is not yaml"), warnings
|
|
end
|
|
|
|
for docidx, doc in ipairs(docs) do
|
|
|
|
if type(doc) ~= "table" then
|
|
return false, nil, build_error(load_state.lines, 1, 1, "Rules content is not yaml"), warnings
|
|
end
|
|
|
|
-- Look for non-numeric indices--implies that document is not array
|
|
-- of objects.
|
|
for key, val in pairs(doc) do
|
|
if type(key) ~= "number" then
|
|
return false, nil, build_error(load_state.lines, 1, 1, "Rules content is not yaml array of objects"), warnings
|
|
end
|
|
end
|
|
|
|
res, errors, doc_warnings = load_rules_doc(rules_mgr, doc, load_state)
|
|
|
|
if (doc_warnings ~= nil) then
|
|
for idx, warning in pairs(doc_warnings) do
|
|
table.insert(warnings, warning)
|
|
end
|
|
end
|
|
|
|
if not res then
|
|
return res, nil, errors, warnings
|
|
end
|
|
end
|
|
|
|
-- We've now loaded all the rules, macros, and lists. 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] = quote_item(item)
|
|
else
|
|
state.lists[item].used = true
|
|
for i, exp_item in ipairs(state.lists[item].items) do
|
|
items[#items+1] = exp_item
|
|
end
|
|
end
|
|
end
|
|
|
|
state.lists[v['list']] = {["items"] = items, ["used"] = false}
|
|
end
|
|
|
|
for _, name in ipairs(state.ordered_macro_names) do
|
|
|
|
local v = state.macros_by_name[name]
|
|
|
|
local status, ast = compiler.compile_macro(v['condition'], state.macros, state.lists)
|
|
|
|
if status == false then
|
|
return false, nil, build_error_with_context(v['context'], ast), warnings
|
|
end
|
|
|
|
if v['source'] == "syscall" then
|
|
if not all_events then
|
|
sinsp_rule_utils.check_for_ignored_syscalls_events(ast, 'macro', v['condition'])
|
|
end
|
|
end
|
|
|
|
state.macros[v['macro']] = {["ast"] = ast.filter.value, ["used"] = false}
|
|
end
|
|
|
|
for _, name in ipairs(state.ordered_rule_names) do
|
|
|
|
local v = state.rules_by_name[name]
|
|
|
|
local econd = ""
|
|
|
|
local exfields = {}
|
|
|
|
-- Turn exceptions into condition strings and add them to each
|
|
-- rule's condition
|
|
for _, eitem in ipairs(v['exceptions']) do
|
|
|
|
local icond, err
|
|
if type(eitem['fields']) == "table" then
|
|
icond, err = build_exception_condition_string_multi_fields(eitem, exfields)
|
|
else
|
|
icond, err = build_exception_condition_string_single_field(eitem, exfields)
|
|
end
|
|
|
|
if err ~= nil then
|
|
return false, nil, build_error_with_context(v['context'], err), warnings
|
|
end
|
|
|
|
if icond ~= "" then
|
|
econd = econd.." and not "..icond
|
|
end
|
|
end
|
|
|
|
state.rules_by_name[name]['exception_fields'] = exfields
|
|
|
|
if econd ~= "" then
|
|
state.rules_by_name[name]['compile_condition'] = "("..state.rules_by_name[name]['condition']..") "..econd
|
|
else
|
|
state.rules_by_name[name]['compile_condition'] = state.rules_by_name[name]['condition']
|
|
end
|
|
|
|
warn_evttypes = true
|
|
if v['warn_evttypes'] ~= nil then
|
|
warn_evttypes = v['warn_evttypes']
|
|
end
|
|
|
|
local status, filter_ast, filters = compiler.compile_filter(v['rule'], v['compile_condition'],
|
|
state.macros, state.lists)
|
|
|
|
if status == false then
|
|
return false, nil, build_error_with_context(v['context'], filter_ast), warnings
|
|
end
|
|
|
|
local evtttypes = {}
|
|
local syscallnums = {}
|
|
|
|
if v['source'] == "syscall" then
|
|
if not all_events then
|
|
sinsp_rule_utils.check_for_ignored_syscalls_events(filter_ast, 'rule', v['rule'])
|
|
end
|
|
|
|
evttypes, syscallnums = sinsp_rule_utils.get_evttypes_syscalls(name, filter_ast, v['compile_condition'], warn_evttypes, verbose)
|
|
end
|
|
|
|
-- If a filter in the rule doesn't exist, either skip the rule
|
|
-- or raise an error, depending on the value of
|
|
-- skip-if-unknown-filter.
|
|
for filter, _ in pairs(filters) do
|
|
if not is_defined_filter(filter) then
|
|
msg = "rule \""..v['rule'].."\": contains unknown filter "..filter
|
|
warnings[#warnings + 1] = msg
|
|
|
|
if not v['skip-if-unknown-filter'] then
|
|
return false, nil, build_error_with_context(v['context'], msg), warnings
|
|
else
|
|
print("Skipping "..msg)
|
|
goto next_rule
|
|
end
|
|
end
|
|
end
|
|
|
|
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)
|
|
|
|
if (v['tags'] == nil) then
|
|
v['tags'] = {}
|
|
end
|
|
if v['source'] == "syscall" then
|
|
install_filter(filter_ast.filter.value, filter, sinsp_lua_parser)
|
|
-- Pass the filter and event types back up
|
|
falco_rules.add_filter(rules_mgr, v['rule'], evttypes, syscallnums, v['tags'])
|
|
|
|
elseif v['source'] == "k8s_audit" then
|
|
install_filter(filter_ast.filter.value, k8s_audit_filter, json_lua_parser)
|
|
|
|
falco_rules.add_k8s_audit_filter(rules_mgr, v['rule'], v['tags'])
|
|
end
|
|
|
|
-- 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)
|
|
else
|
|
falco_rules.enable_rule(rules_mgr, v['rule'], 1)
|
|
end
|
|
|
|
-- If the format string contains %container.info, replace it
|
|
-- with extra. Otherwise, add extra onto the end of the format
|
|
-- string.
|
|
if v['source'] == "syscall" then
|
|
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
|
|
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.
|
|
local err, formatter = formats.formatter(v['source'], v['output'])
|
|
if err == nil then
|
|
formats.free_formatter(v['source'], formatter)
|
|
else
|
|
return false, nil, build_error_with_context(v['context'], err), warnings
|
|
end
|
|
else
|
|
return false, nil, build_error_with_context(v['context'], "Unexpected type in load_rule: "..filter_ast.type), warnings
|
|
end
|
|
|
|
::next_rule::
|
|
end
|
|
|
|
-- Print info on any dangling lists or macros that were not used anywhere
|
|
for name, macro in pairs(state.macros) do
|
|
if macro.used == false then
|
|
msg = "macro "..name.." not refered to by any rule/macro"
|
|
warnings[#warnings + 1] = msg
|
|
end
|
|
end
|
|
|
|
for name, list in pairs(state.lists) do
|
|
if list.used == false then
|
|
msg = "list "..name.." not refered to by any rule/macro/list"
|
|
warnings[#warnings + 1] = msg
|
|
end
|
|
end
|
|
|
|
io.flush()
|
|
|
|
return true, load_state.required_engine_version, {}, warnings
|
|
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(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
|
|
|
|
-- Also return all fields from all exceptions
|
|
combined_rule = state.rules_by_name[rule.rule]
|
|
|
|
if combined_rule == nil then
|
|
error ("rule_loader.on_event(): could not find rule by name: ", rule.rule)
|
|
end
|
|
|
|
return rule.rule, rule.priority_num, output, combined_rule.exception_fields, rule.tags
|
|
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
|
|
|
|
|
|
|