From e1b9b047d0cfe666b439c5fe4c610dfc41560cbd Mon Sep 17 00:00:00 2001 From: Henri DF Date: Wed, 4 May 2016 16:44:16 -0700 Subject: [PATCH] Support new yaml format for rules Uses yaml parsing lib to parse a yaml file comprising of a list of macros and rules, like: - macro: bin_dir condition: fd.directory in (/bin, /sbin, /usr/bin, /usr/sbin) - macro: core_binaries condition: proc.name in (ls, mkdir, cat, less, ps) - condition: (fd.typechar = 4 or fd.typechar=6) and core_binaries output: "%evt.time: %proc.name network with %fd.l4proto" - condition: evt.type = write and bin_dir output: "%evt.time: System binary modified (file '%fd.filename' written by process %proc.name)" - condition: container.id != host and proc.name = bash output: "%evt.time: Shell running in container (%proc.name, %container.id)" --- userspace/falco/lua/compiler.lua | 46 ++++------- userspace/falco/lua/rule_loader.lua | 123 ++++++++++++++++------------ userspace/falco/rules.cpp | 37 +-------- userspace/falco/rules.h | 3 +- 4 files changed, 89 insertions(+), 120 deletions(-) diff --git a/userspace/falco/lua/compiler.lua b/userspace/falco/lua/compiler.lua index 59c2eeba..9bfc5b4f 100644 --- a/userspace/falco/lua/compiler.lua +++ b/userspace/falco/lua/compiler.lua @@ -155,17 +155,6 @@ end -- grammar -local function normalize_level(level) - valid_levels = {"emergency", "alert", "critical", "error", "warning", "notice", "informational", "debug"} - level = string.lower(level) - for i,v in ipairs(valid_levels) do - if (string.find(v, "^"..level)) then - return i - 1 -- (syslog levels start at 0, lua indices start at 1) - end - end - error("Invalid severity level: "..level) -end - local function filter(e) return {type = "Filter", value=e} @@ -417,10 +406,7 @@ function compiler.parser.parse_filter (subject) end ---[[ - Compiles a single line from a falco ruleset and updates the passed-in macros table. Returns the AST of the line. ---]] -function compiler.compile_line(line, macro_defs) +function compiler.compile_macro(line) local ast, error_msg = compiler.parser.parse_filter(line) if (error_msg) then @@ -428,24 +414,22 @@ function compiler.compile_line(line, macro_defs) error(error_msg) end - if (type(ast) == "number") then - -- hack: we get a number (# of matched chars) V"Skip" back if this line - -- only contained whitespace. (According to docs 'v"Skip" / 0' in Start - -- rule should not capture anything but it doesn't seem to work that - -- way...) - return {} + return ast +end + +--[[ + Parses a single filter, then expands macros using passed-in table of definitions. Returns resulting AST. +--]] +function compiler.compile_filter(source, macro_defs) + local ast, error_msg = compiler.parser.parse_filter(source) + + if (error_msg) then + print ("Compilation error: ", error_msg) + error(error_msg) end - if (ast.type == "MacroDef") then - -- Parsed line is a macro definition, so update our dictionary of macros and - -- return - macro_defs[ast.name] = ast.value - return ast - - elseif (ast.type == "Rule") then - -- Line is a filter, so expand macro references then - -- stitch it into global ast - + if (ast.type == "Rule") then + -- Line is a filter, so expand macro references repeat expanded = expand_macros(ast, macro_defs, false) until expanded == false diff --git a/userspace/falco/lua/rule_loader.lua b/userspace/falco/lua/rule_loader.lua index ae589b97..c17c7ced 100644 --- a/userspace/falco/lua/rule_loader.lua +++ b/userspace/falco/lua/rule_loader.lua @@ -6,8 +6,11 @@ --]] local DEFAULT_OUTPUT_FORMAT = "%evt.time: %evt.num %evt.cpu %proc.name (%thread.tid) %evt.dir %evt.type %evt.args" +local DEFAULT_PRIORITY = "WARNING" + local compiler = require "compiler" +local yaml = require"lyaml" --[[ Traverse AST, adding the passed-in 'index' to each node that contains a relational expression @@ -89,78 +92,90 @@ local function install_filter(node, parent_bool_op) end end -local state - ---[[ - Sets up compiler state and returns it. - - It holds state such as macro definitions that must be kept across calls - to the line-oriented compiler. ---]] -local function init() - return {macros={}, filter_ast=nil, n_rules=0, outputs={}} -end - - -function set_output(output_ast) +function set_output(output_format, state) if(output_ast.type == "OutputFormat") then local format - if output_ast.value==nil then - format = DEFAULT_OUTPUT_FORMAT - else - format = output_ast.value - end - - state.outputs[state.n_rules] = {format=format, level = output_ast.level} else error ("Unexpected type in set_output: ".. output_ast.type) end end -function load_rule(r) - if (state == nil) then - state = init() - end - local line_ast = compiler.compile_line(r, state.macros) - - if (line_ast.type == nil) then -- blank line - return - elseif (line_ast.type == "MacroDef") then - return - elseif (not (line_ast.type == "Rule")) then - error ("Unexpected type in load_rule: "..line_ast.type) - end - - state.n_rules = state.n_rules + 1 - - set_output(line_ast.output) - - -- 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(line_ast.filter.value, state.n_rules) - - -- Rule ASTs are merged together into one big AST, with "OR" between each - -- rule. - if (state.filter_ast == nil) then - state.filter_ast = line_ast.filter.value - else - state.filter_ast = { type = "BinaryBoolOp", operator = "or", left = state.filter_ast, right = line_ast.filter.value } +local function priority(s) + valid_levels = {"emergency", "alert", "critical", "error", "warning", "notice", "informational", "debug"} + s = string.lower(s) + for i,v in ipairs(valid_levels) do + if (string.find(v, "^"..s)) then + return i - 1 -- (syslog levels start at 0, lua indices start at 1) + end end + error("Invalid severity level: "..level) end -function on_done() +local state = {macros={}, filter_ast=nil, n_rules=0, outputs={}} + +function load_rules(filename) + + local f = assert(io.open(filename, "r")) + local s = f:read("*all") + f:close() + local rules = yaml.load(s) + + 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.macros[v['macro']] = ast.filter.value + + else -- filter + + if (v['condition'] == nil) then + error ("Missing condition in rule") + end + + if (v['output'] == nil) then + error ("Missing output in rule with condition"..v['condition']) + end + + local filter_ast = compiler.compile_filter(v['condition'], state.macros) + + if (filter_ast.type == "Rule") then + state.n_rules = state.n_rules + 1 + + state.outputs[state.n_rules] = {format=v['output'] or DEFAULT_OUTPUT_FORMAT, + level=priority(v['priority'] or DEFAULT_PRIORITY)} + + -- 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) + + -- 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 + install_filter(state.filter_ast) io.flush() end local output_functions = require('output') - outputs = {} function add_output(output_name, config) diff --git a/userspace/falco/rules.cpp b/userspace/falco/rules.cpp index 921c1e6c..07ef01a2 100644 --- a/userspace/falco/rules.cpp +++ b/userspace/falco/rules.cpp @@ -41,48 +41,19 @@ void falco_rules::load_compiler(string lua_main_filename) void falco_rules::load_rules(string rules_filename) { - ifstream is; - is.open(rules_filename); - if(!is.is_open()) - { - throw sinsp_exception("Can't open file " + rules_filename + ". Try setting file location in config file or use '-r' flag."); - } - - lua_getglobal(m_ls, m_lua_load_rule.c_str()); + lua_getglobal(m_ls, m_lua_load_rules.c_str()); if(lua_isfunction(m_ls, -1)) { - lua_pop(m_ls, 1); - } else { - throw sinsp_exception("No function " + m_lua_load_rule + " found in lua compiler module"); - } - - std::string line; - while (std::getline(is, line)) - { - lua_getglobal(m_ls, m_lua_load_rule.c_str()); - lua_pushstring(m_ls, line.c_str()); - + lua_pushstring(m_ls, rules_filename.c_str()); if(lua_pcall(m_ls, 1, 0, 0) != 0) { const char* lerr = lua_tostring(m_ls, -1); - string err = "Error loading rule '" + line + "':" + string(lerr); - throw sinsp_exception(err); - } - } - - lua_getglobal(m_ls, m_lua_on_done.c_str()); - if(lua_isfunction(m_ls, -1)) - { - if(lua_pcall(m_ls, 0, 0, 0) != 0) - { - const char* lerr = lua_tostring(m_ls, -1); - string err = "Error installing rules: " + string(lerr); + string err = "Error loading rules:" + string(lerr); throw sinsp_exception(err); } } else { - throw sinsp_exception("No function " + m_lua_on_done + " found in lua compiler module"); + throw sinsp_exception("No function " + m_lua_load_rules + " found in lua compiler module"); } - } sinsp_filter* falco_rules::get_filter() diff --git a/userspace/falco/rules.h b/userspace/falco/rules.h index 64f19916..17d1b18b 100644 --- a/userspace/falco/rules.h +++ b/userspace/falco/rules.h @@ -17,7 +17,6 @@ class falco_rules lua_parser* m_lua_parser; lua_State* m_ls; - string m_lua_load_rule = "load_rule"; - string m_lua_on_done = "on_done"; + string m_lua_load_rules = "load_rules"; string m_lua_on_event = "on_event"; };