diff --git a/userspace/engine/lua/compiler.lua b/userspace/engine/lua/compiler.lua index b5a11904..3b0c01af 100644 --- a/userspace/engine/lua/compiler.lua +++ b/userspace/engine/lua/compiler.lua @@ -1,4 +1,4 @@ --- Copyright (C) 2019 The Falco Authors. +-- Copyright (C) 2020 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. @@ -11,25 +11,24 @@ -- 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. - local parser = require("parser") local compiler = {} compiler.trim = parser.trim function map(f, arr) - local res = {} - for i,v in ipairs(arr) do - res[i] = f(v) - end - return res + local res = {} + for i, v in ipairs(arr) do + res[i] = f(v) + end + return res end function foldr(f, acc, arr) - for i,v in pairs(arr) do - acc = f(acc, v) - end - return acc + for i, v in pairs(arr) do + acc = f(acc, v) + end + return acc end --[[ @@ -47,181 +46,192 @@ end --]] function copy_ast_obj(obj) - if type(obj) ~= 'table' then return obj end - local res = {} - for k, v in pairs(obj) do res[copy_ast_obj(k)] = copy_ast_obj(v) end - return res + if type(obj) ~= 'table' then + return obj + end + local res = {} + for k, v in pairs(obj) do + res[copy_ast_obj(k)] = copy_ast_obj(v) + end + return res end function expand_macros(ast, defs, changed) - if (ast.type == "Rule") then - return expand_macros(ast.filter, defs, changed) - elseif ast.type == "Filter" then - if (ast.value.type == "Macro") then - if (defs[ast.value.value] == nil) then - return false, "Undefined macro '".. ast.value.value .. "' used in filter." - end - defs[ast.value.value].used = true - ast.value = copy_ast_obj(defs[ast.value.value].ast) - changed = true - return true, changed - end - return expand_macros(ast.value, defs, changed) + if (ast.type == "Rule") then + return expand_macros(ast.filter, defs, changed) + elseif ast.type == "Filter" then + if (ast.value.type == "Macro") then + if (defs[ast.value.value] == nil) then + return false, "Undefined macro '" .. ast.value.value .. "' used in filter." + end + defs[ast.value.value].used = true + ast.value = copy_ast_obj(defs[ast.value.value].ast) + changed = true + return true, changed + end + return expand_macros(ast.value, defs, changed) - elseif ast.type == "BinaryBoolOp" then + elseif ast.type == "BinaryBoolOp" then - if (ast.left.type == "Macro") then - if (defs[ast.left.value] == nil) then - return false, "Undefined macro '".. ast.left.value .. "' used in filter." - end - defs[ast.left.value].used = true - ast.left = copy_ast_obj(defs[ast.left.value].ast) - changed = true - end + if (ast.left.type == "Macro") then + if (defs[ast.left.value] == nil) then + return false, "Undefined macro '" .. ast.left.value .. "' used in filter." + end + defs[ast.left.value].used = true + ast.left = copy_ast_obj(defs[ast.left.value].ast) + changed = true + end - if (ast.right.type == "Macro") then - if (defs[ast.right.value] == nil) then - return false, "Undefined macro ".. ast.right.value .. " used in filter." - end - defs[ast.right.value].used = true - ast.right = copy_ast_obj(defs[ast.right.value].ast) - changed = true - end + if (ast.right.type == "Macro") then + if (defs[ast.right.value] == nil) then + return false, "Undefined macro " .. ast.right.value .. " used in filter." + end + defs[ast.right.value].used = true + ast.right = copy_ast_obj(defs[ast.right.value].ast) + changed = true + end - local status, changed_left = expand_macros(ast.left, defs, false) - if status == false then - return false, changed_left - end - local status, changed_right = expand_macros(ast.right, defs, false) - if status == false then - return false, changed_right - end - return true, changed or changed_left or changed_right + local status, changed_left = expand_macros(ast.left, defs, false) + if status == false then + return false, changed_left + end + local status, changed_right = expand_macros(ast.right, defs, false) + if status == false then + return false, changed_right + end + return true, changed or changed_left or changed_right - elseif ast.type == "UnaryBoolOp" then - if (ast.argument.type == "Macro") then - if (defs[ast.argument.value] == nil) then - return false, "Undefined macro ".. ast.argument.value .. " used in filter." - end - defs[ast.argument.value].used = true - ast.argument = copy_ast_obj(defs[ast.argument.value].ast) - changed = true - end - return expand_macros(ast.argument, defs, changed) - end - return true, changed + elseif ast.type == "UnaryBoolOp" then + if (ast.argument.type == "Macro") then + if (defs[ast.argument.value] == nil) then + return false, "Undefined macro " .. ast.argument.value .. " used in filter." + end + defs[ast.argument.value].used = true + ast.argument = copy_ast_obj(defs[ast.argument.value].ast) + changed = true + end + return expand_macros(ast.argument, defs, changed) + end + return true, changed end function get_macros(ast, set) - if (ast.type == "Macro") then - set[ast.value] = true - return set - end + if (ast.type == "Macro") then + set[ast.value] = true + return set + end - if ast.type == "Filter" then - return get_macros(ast.value, set) - end + if ast.type == "Filter" then + return get_macros(ast.value, set) + end - if ast.type == "BinaryBoolOp" then - local left = get_macros(ast.left, {}) - local right = get_macros(ast.right, {}) + if ast.type == "BinaryBoolOp" then + local left = get_macros(ast.left, {}) + local right = get_macros(ast.right, {}) - for m, _ in pairs(left) do set[m] = true end - for m, _ in pairs(right) do set[m] = true end + for m, _ in pairs(left) do + set[m] = true + end + for m, _ in pairs(right) do + set[m] = true + end - return set - end - if ast.type == "UnaryBoolOp" then - return get_macros(ast.argument, set) - end - return set + return set + end + if ast.type == "UnaryBoolOp" then + return get_macros(ast.argument, set) + end + return set end function get_filters(ast) - local filters = {} + local filters = {} - function cb(node) - if node.type == "FieldName" then - filters[node.value] = 1 - end - end + function cb(node) + if node.type == "FieldName" then + filters[node.value] = 1 + end + end - parser.traverse_ast(ast.filter.value, {FieldName=1} , cb) + parser.traverse_ast(ast.filter.value, { + FieldName = 1 + }, cb) - return filters + return filters end function compiler.expand_lists_in(source, list_defs) - for name, def in pairs(list_defs) do + for name, def in pairs(list_defs) do - local bpos = string.find(source, name, 1, true) + local bpos = string.find(source, name, 1, true) - while bpos ~= nil do - def.used = true + while bpos ~= nil do + def.used = true - local epos = bpos + string.len(name) + local epos = bpos + string.len(name) - -- The characters surrounding the name must be delimiters of beginning/end of string - if (bpos == 1 or string.match(string.sub(source, bpos-1, bpos-1), "[%s(),=]")) and (epos > string.len(source) or string.match(string.sub(source, epos, epos), "[%s(),=]")) then - new_source = "" + -- The characters surrounding the name must be delimiters of beginning/end of string + if (bpos == 1 or string.match(string.sub(source, bpos - 1, bpos - 1), "[%s(),=]")) and + (epos > string.len(source) or string.match(string.sub(source, epos, epos), "[%s(),=]")) then + new_source = "" - if bpos > 1 then - new_source = new_source..string.sub(source, 1, bpos-1) - end + if bpos > 1 then + new_source = new_source .. string.sub(source, 1, bpos - 1) + end - sub = table.concat(def.items, ", ") + sub = table.concat(def.items, ", ") - new_source = new_source..sub + new_source = new_source .. sub - if epos <= string.len(source) then - new_source = new_source..string.sub(source, epos, string.len(source)) - end + if epos <= string.len(source) then + new_source = new_source .. string.sub(source, epos, string.len(source)) + end - source = new_source - bpos = bpos + (string.len(sub)-string.len(name)) - end + source = new_source + bpos = bpos + (string.len(sub) - string.len(name)) + end - bpos = string.find(source, name, bpos+1, true) - end - end + bpos = string.find(source, name, bpos + 1, true) + end + end - return source + return source end function compiler.compile_macro(line, macro_defs, list_defs) - line = compiler.expand_lists_in(line, list_defs) + line = compiler.expand_lists_in(line, list_defs) - local ast, error_msg = parser.parse_filter(line) + local ast, error_msg = parser.parse_filter(line) - if (error_msg) then - msg = "Compilation error when compiling \""..line.."\": ".. error_msg - return false, msg - end + if (error_msg) then + msg = "Compilation error when compiling \"" .. line .. "\": " .. error_msg + return false, msg + end - -- Simply as a validation step, try to expand all macros in this - -- macro's condition. This changes the ast, so we make a copy - -- first. - local ast_copy = copy_ast_obj(ast) + -- Simply as a validation step, try to expand all macros in this + -- macro's condition. This changes the ast, so we make a copy + -- first. + local ast_copy = copy_ast_obj(ast) - if (ast.type == "Rule") then - -- Line is a filter, so expand macro references - repeat - status, expanded = expand_macros(ast_copy, macro_defs, false) - if status == false then - msg = "Compilation error when compiling \""..line.."\": ".. expanded - return false, msg - end - until expanded == false + if (ast.type == "Rule") then + -- Line is a filter, so expand macro references + repeat + status, expanded = expand_macros(ast_copy, macro_defs, false) + if status == false then + msg = "Compilation error when compiling \"" .. line .. "\": " .. expanded + return false, msg + end + until expanded == false - else - return false, "Unexpected top-level AST type: "..ast.type - end + else + return false, "Unexpected top-level AST type: " .. ast.type + end - return true, ast + return true, ast end --[[ @@ -229,32 +239,31 @@ end --]] function compiler.compile_filter(name, source, macro_defs, list_defs) - source = compiler.expand_lists_in(source, list_defs) + source = compiler.expand_lists_in(source, list_defs) - local ast, error_msg = parser.parse_filter(source) + local ast, error_msg = parser.parse_filter(source) - if (error_msg) then - msg = "Compilation error when compiling \""..source.."\": "..error_msg - return false, msg - end + if (error_msg) then + msg = "Compilation error when compiling \"" .. source .. "\": " .. error_msg + return false, msg + end - if (ast.type == "Rule") then - -- Line is a filter, so expand macro references - repeat - status, expanded = expand_macros(ast, macro_defs, false) - if status == false then - return false, expanded - end - until expanded == false + if (ast.type == "Rule") then + -- Line is a filter, so expand macro references + repeat + status, expanded = expand_macros(ast, macro_defs, false) + if status == false then + return false, expanded + end + until expanded == false - else - return false, "Unexpected top-level AST type: "..ast.type - end + else + return false, "Unexpected top-level AST type: " .. ast.type + end - filters = get_filters(ast) + filters = get_filters(ast) - return true, ast, filters + return true, ast, filters end - return compiler diff --git a/userspace/engine/lua/parser.lua b/userspace/engine/lua/parser.lua index 03310036..edd59dae 100644 --- a/userspace/engine/lua/parser.lua +++ b/userspace/engine/lua/parser.lua @@ -1,4 +1,4 @@ --- Copyright (C) 2019 The Falco Authors. +-- Copyright (C) 2020 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. @@ -12,7 +12,6 @@ -- See the License for the specific language governing permissions and -- limitations under the License. -- - --[[ Falco grammar and parser. @@ -40,232 +39,258 @@ local space = lpeg.space -- creates an error message for the input string local function syntaxerror(errorinfo, pos, msg) - local error_msg = "%s: syntax error, %s" - return string.format(error_msg, pos, msg) + local error_msg = "%s: syntax error, %s" + return string.format(error_msg, pos, msg) end -- gets the farthest failure position local function getffp(s, i, t) - return t.ffp or i, t + return t.ffp or i, t end -- gets the table that contains the error information local function geterrorinfo() - return Cmt(Carg(1), getffp) * (C(V "OneWord") + Cc("EOF")) / function(t, u) - t.unexpected = u - return t - end + return Cmt(Carg(1), getffp) * (C(V "OneWord") + Cc("EOF")) / function(t, u) + t.unexpected = u + return t + end end -- creates an errror message using the farthest failure position local function errormsg() - return geterrorinfo() / function(t) - local p = t.ffp or 1 - local msg = "unexpected '%s', expecting %s" - msg = string.format(msg, t.unexpected, t.expected) - return nil, syntaxerror(t, p, msg) - end + return geterrorinfo() / function(t) + local p = t.ffp or 1 + local msg = "unexpected '%s', expecting %s" + msg = string.format(msg, t.unexpected, t.expected) + return nil, syntaxerror(t, p, msg) + end end -- reports a syntactic error local function report_error() - return errormsg() + return errormsg() end --- sets the farthest failure position and the expected tokens local function setffp(s, i, t, n) - if not t.ffp or i > t.ffp then - t.ffp = i - t.list = {} - t.list[n] = n - t.expected = "'" .. n .. "'" - elseif i == t.ffp then - if not t.list[n] then - t.list[n] = n - t.expected = "'" .. n .. "', " .. t.expected - end - end - return false + if not t.ffp or i > t.ffp then + t.ffp = i + t.list = {} + t.list[n] = n + t.expected = "'" .. n .. "'" + elseif i == t.ffp then + if not t.list[n] then + t.list[n] = n + t.expected = "'" .. n .. "', " .. t.expected + end + end + return false end local function updateffp(name) - return Cmt(Carg(1) * Cc(name), setffp) + return Cmt(Carg(1) * Cc(name), setffp) end -- regular combinators and auxiliary functions local function token(pat, name) - return pat * V "Skip" + updateffp(name) * P(false) + return pat * V "Skip" + updateffp(name) * P(false) end local function symb(str) - return token(P(str), str) + return token(P(str), str) end local function kw(str) - return token(P(str) * -V "idRest", str) + return token(P(str) * -V "idRest", str) end local function list(pat, sep) - return Ct(pat ^ -1 * (sep * pat ^ 0) ^ 0) / function(elements) - return {type = "List", elements = elements} - end + return Ct(pat ^ -1 * (sep * pat ^ 0) ^ 0) / function(elements) + return { + type = "List", + elements = elements + } + end end ---http://lua-users.org/wiki/StringTrim +-- http://lua-users.org/wiki/StringTrim function trim(s) - if (type(s) ~= "string") then - return s - end - return (s:gsub("^%s*(.-)%s*$", "%1")) + if (type(s) ~= "string") then + return s + end + return (s:gsub("^%s*(.-)%s*$", "%1")) end parser.trim = trim local function terminal(tag) - -- Rather than trim the whitespace in this way, it would be nicer to exclude it from the capture... - return token(V(tag), tag) / function(tok) - val = tok - if tag ~= "String" then + -- Rather than trim the whitespace in this way, it would be nicer to exclude it from the capture... + return token(V(tag), tag) / function(tok) + val = tok + if tag ~= "String" then val = trim(tok) - end - return {type = tag, value = val} - end + end + return { + type = tag, + value = val + } + end end local function unaryboolop(op, e) - return {type = "UnaryBoolOp", operator = op, argument = e} + return { + type = "UnaryBoolOp", + operator = op, + argument = e + } end local function unaryrelop(e, op) - return {type = "UnaryRelOp", operator = op, argument = e} + return { + type = "UnaryRelOp", + operator = op, + argument = e + } end local function binaryop(e1, op, e2) - if not op then - return e1 - else - return {type = "BinaryBoolOp", operator = op, left = e1, right = e2} - end + if not op then + return e1 + else + return { + type = "BinaryBoolOp", + operator = op, + left = e1, + right = e2 + } + end end local function bool(pat, sep) - return Cf(pat * Cg(sep * pat) ^ 0, binaryop) + return Cf(pat * Cg(sep * pat) ^ 0, binaryop) end local function rel(left, sep, right) - return left * sep * right / function(e1, op, e2) - return {type = "BinaryRelOp", operator = op, left = e1, right = e2} - end + return left * sep * right / function(e1, op, e2) + return { + type = "BinaryRelOp", + operator = op, + left = e1, + right = e2 + } + end end -- grammar local function filter(e) - return {type = "Filter", value = e} + return { + type = "Filter", + value = e + } end local function rule(filter) - return {type = "Rule", filter = filter} + return { + type = "Rule", + filter = filter + } end local G = { - V "Start", -- Entry rule - Start = V "Skip" * (V "Comment" + V "Rule" / rule) ^ -1 * -1 + report_error(), - -- Grammar - Comment = P "#" * P(1) ^ 0, - Rule = V "Filter" / filter * ((V "Skip") ^ -1), - Filter = V "OrExpression", - OrExpression = bool(V "AndExpression", V "OrOp"), - AndExpression = bool(V "NotExpression", V "AndOp"), - NotExpression = V "UnaryBoolOp" * V "NotExpression" / unaryboolop + V "ExistsExpression", - ExistsExpression = terminal "FieldName" * V "ExistsOp" / unaryrelop + V "MacroExpression", - MacroExpression = terminal "Macro" + V "RelationalExpression", - RelationalExpression = rel(terminal "FieldName", V "RelOp", V "Value") + - rel(terminal "FieldName", V "SetOp", V "InList") + - V "PrimaryExp", - PrimaryExp = symb("(") * V "Filter" * symb(")"), - FuncArgs = symb("(") * list(V "Value", symb(",")) * symb(")"), - -- Terminals - Value = terminal "Number" + terminal "String" + terminal "BareString", - InList = symb("(") * list(V "Value", symb(",")) * symb(")"), - -- Lexemes - Space = space ^ 1, - Skip = (V "Space") ^ 0, - idStart = alpha + P("_"), - idRest = alnum + P("_"), - Identifier = V "idStart" * V "idRest" ^ 0, - Macro = V "idStart" * V "idRest" ^ 0 * -P ".", - Int = digit ^ 1, - PathString = (alnum + S ",.-_/*?") ^ 1, - PortRangeString = (V "Int" + S ":,") ^ 1, - Index = V "PortRangeString" + V "Int" + V "PathString", - FieldName = V "Identifier" * (P "." + V "Identifier") ^ 1 * (P "[" * V "Index" * P "]") ^ -1, - Name = C(V "Identifier") * -V "idRest", - Hex = (P("0x") + P("0X")) * xdigit ^ 1, - Expo = S("eE") * S("+-") ^ -1 * digit ^ 1, - Float = (((digit ^ 1 * P(".") * digit ^ 0) + (P(".") * digit ^ 1)) * V "Expo" ^ -1) + (digit ^ 1 * V "Expo"), - Number = C(V "Hex" + V "Float" + V "Int") / function(n) - return tonumber(n) - end, - String = (P '"' * C(((P "\\" * P(1)) + (P(1) - P '"')) ^ 0) * P '"' + - P "'" * C(((P "\\" * P(1)) + (P(1) - P "'")) ^ 0) * P "'"), - BareString = C((P(1) - S " (),=") ^ 1), - OrOp = kw("or") / "or", - AndOp = kw("and") / "and", - Colon = kw(":"), - RelOp = symb("=") / "=" + symb("==") / "==" + symb("!=") / "!=" + symb("<=") / "<=" + symb(">=") / ">=" + - symb("<") / "<" + - symb(">") / ">" + - symb("contains") / "contains" + - symb("icontains") / "icontains" + - symb("glob") / "glob" + - symb("startswith") / "startswith" + - symb("endswith") / "endswith", - SetOp = kw("in") / "in" + kw("intersects") / "intersects" + kw("pmatch") / "pmatch", - UnaryBoolOp = kw("not") / "not", - ExistsOp = kw("exists") / "exists", - -- for error reporting - OneWord = V "Name" + V "Number" + V "String" + P(1) + V "Start", -- Entry rule + Start = V "Skip" * (V "Comment" + V "Rule" / rule) ^ -1 * -1 + report_error(), + -- Grammar + Comment = P "#" * P(1) ^ 0, + Rule = V "Filter" / filter * ((V "Skip") ^ -1), + Filter = V "OrExpression", + OrExpression = bool(V "AndExpression", V "OrOp"), + AndExpression = bool(V "NotExpression", V "AndOp"), + NotExpression = V "UnaryBoolOp" * V "NotExpression" / unaryboolop + V "ExistsExpression", + ExistsExpression = terminal "FieldName" * V "ExistsOp" / unaryrelop + V "MacroExpression", + MacroExpression = terminal "Macro" + V "RelationalExpression", + RelationalExpression = rel(terminal "FieldName", V "RelOp", V "Value") + + rel(terminal "FieldName", V "SetOp", V "InList") + V "PrimaryExp", + PrimaryExp = symb("(") * V "Filter" * symb(")"), + FuncArgs = symb("(") * list(V "Value", symb(",")) * symb(")"), + -- Terminals + Value = terminal "Number" + terminal "String" + terminal "BareString", + InList = symb("(") * list(V "Value", symb(",")) * symb(")"), + -- Lexemes + Space = space ^ 1, + Skip = (V "Space") ^ 0, + idStart = alpha + P("_"), + idRest = alnum + P("_"), + Identifier = V "idStart" * V "idRest" ^ 0, + Macro = V "idStart" * V "idRest" ^ 0 * -P ".", + Int = digit ^ 1, + PathString = (alnum + S ",.-_/*?") ^ 1, + PortRangeString = (V "Int" + S ":,") ^ 1, + Index = V "PortRangeString" + V "Int" + V "PathString", + FieldName = V "Identifier" * (P "." + V "Identifier") ^ 1 * (P "[" * V "Index" * P "]") ^ -1, + Name = C(V "Identifier") * -V "idRest", + Hex = (P("0x") + P("0X")) * xdigit ^ 1, + Expo = S("eE") * S("+-") ^ -1 * digit ^ 1, + Float = (((digit ^ 1 * P(".") * digit ^ 0) + (P(".") * digit ^ 1)) * V "Expo" ^ -1) + (digit ^ 1 * V "Expo"), + Number = C(V "Hex" + V "Float" + V "Int") / function(n) + return tonumber(n) + end, + String = (P '"' * C(((P "\\" * P(1)) + (P(1) - P '"')) ^ 0) * P '"' + P "'" * + C(((P "\\" * P(1)) + (P(1) - P "'")) ^ 0) * P "'"), + BareString = C((P(1) - S " (),=") ^ 1), + OrOp = kw("or") / "or", + AndOp = kw("and") / "and", + Colon = kw(":"), + RelOp = symb("=") / "=" + symb("==") / "==" + symb("!=") / "!=" + symb("<=") / "<=" + symb(">=") / ">=" + symb("<") / + "<" + symb(">") / ">" + symb("contains") / "contains" + symb("icontains") / "icontains" + symb("glob") / "glob" + + symb("startswith") / "startswith" + symb("endswith") / "endswith", + SetOp = kw("in") / "in" + kw("intersects") / "intersects" + kw("pmatch") / "pmatch", + UnaryBoolOp = kw("not") / "not", + ExistsOp = kw("exists") / "exists", + -- for error reporting + OneWord = V "Name" + V "Number" + V "String" + P(1) } --[[ Parses a single filter and returns the AST. --]] function parser.parse_filter(subject) - local errorinfo = {subject = subject} - lpeg.setmaxstack(1000) - local ast, error_msg = lpeg.match(G, subject, nil, errorinfo) - return ast, error_msg + local errorinfo = { + subject = subject + } + lpeg.setmaxstack(1000) + local ast, error_msg = lpeg.match(G, subject, nil, errorinfo) + return ast, error_msg end function print_ast(ast, level) - local t = ast.type - level = level or 0 - local prefix = string.rep(" ", level * 4) - level = level + 1 + local t = ast.type + level = level or 0 + local prefix = string.rep(" ", level * 4) + level = level + 1 - if t == "Rule" then - print_ast(ast.filter, level) - elseif t == "Filter" then - print_ast(ast.value, level) - elseif t == "BinaryBoolOp" or t == "BinaryRelOp" then - print(prefix .. ast.operator) - print_ast(ast.left, level) - print_ast(ast.right, level) - elseif t == "UnaryRelOp" or t == "UnaryBoolOp" then - print(prefix .. ast.operator) - print_ast(ast.argument, level) - elseif t == "List" then - for i, v in ipairs(ast.elements) do - print_ast(v, level) - end - elseif t == "FieldName" or t == "Number" or t == "String" or t == "BareString" or t == "Macro" then - print(prefix .. t .. " " .. ast.value) - elseif t == "MacroDef" then - -- don't print for now - else - error("Unexpected type in print_ast: " .. t) - end + if t == "Rule" then + print_ast(ast.filter, level) + elseif t == "Filter" then + print_ast(ast.value, level) + elseif t == "BinaryBoolOp" or t == "BinaryRelOp" then + print(prefix .. ast.operator) + print_ast(ast.left, level) + print_ast(ast.right, level) + elseif t == "UnaryRelOp" or t == "UnaryBoolOp" then + print(prefix .. ast.operator) + print_ast(ast.argument, level) + elseif t == "List" then + for i, v in ipairs(ast.elements) do + print_ast(v, level) + end + elseif t == "FieldName" or t == "Number" or t == "String" or t == "BareString" or t == "Macro" then + print(prefix .. t .. " " .. ast.value) + elseif t == "MacroDef" then + -- don't print for now + else + error("Unexpected type in print_ast: " .. t) + end end parser.print_ast = print_ast @@ -275,32 +300,32 @@ parser.print_ast = print_ast -- cb(ast_node, ctx) -- ctx is optional. function traverse_ast(ast, node_types, cb, ctx) - local t = ast.type + local t = ast.type - if node_types[t] ~= nil then - cb(ast, ctx) - end + if node_types[t] ~= nil then + cb(ast, ctx) + end - if t == "Rule" then - traverse_ast(ast.filter, node_types, cb, ctx) - elseif t == "Filter" then - traverse_ast(ast.value, node_types, cb, ctx) - elseif t == "BinaryBoolOp" or t == "BinaryRelOp" then - traverse_ast(ast.left, node_types, cb, ctx) - traverse_ast(ast.right, node_types, cb, ctx) - elseif t == "UnaryRelOp" or t == "UnaryBoolOp" then - traverse_ast(ast.argument, node_types, cb, ctx) - elseif t == "List" then - for i, v in ipairs(ast.elements) do - traverse_ast(v, node_types, cb, ctx) - end - elseif t == "MacroDef" then - traverse_ast(ast.value, node_types, cb, ctx) - elseif t == "FieldName" or t == "Number" or t == "String" or t == "BareString" or t == "Macro" then - -- do nothing, no traversal needed - else - error("Unexpected type in traverse_ast: " .. t) - end + if t == "Rule" then + traverse_ast(ast.filter, node_types, cb, ctx) + elseif t == "Filter" then + traverse_ast(ast.value, node_types, cb, ctx) + elseif t == "BinaryBoolOp" or t == "BinaryRelOp" then + traverse_ast(ast.left, node_types, cb, ctx) + traverse_ast(ast.right, node_types, cb, ctx) + elseif t == "UnaryRelOp" or t == "UnaryBoolOp" then + traverse_ast(ast.argument, node_types, cb, ctx) + elseif t == "List" then + for i, v in ipairs(ast.elements) do + traverse_ast(v, node_types, cb, ctx) + end + elseif t == "MacroDef" then + traverse_ast(ast.value, node_types, cb, ctx) + elseif t == "FieldName" or t == "Number" or t == "String" or t == "BareString" or t == "Macro" then + -- do nothing, no traversal needed + else + error("Unexpected type in traverse_ast: " .. t) + end end parser.traverse_ast = traverse_ast diff --git a/userspace/engine/lua/rule_loader.lua b/userspace/engine/lua/rule_loader.lua index d6d2427a..c7a9c0ce 100644 --- a/userspace/engine/lua/rule_loader.lua +++ b/userspace/engine/lua/rule_loader.lua @@ -1,4 +1,4 @@ --- Copyright (C) 2019 The Falco Authors. +-- Copyright (C) 2020 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. @@ -12,172 +12,199 @@ -- 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" - +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 + local t = ast.type - if t == "BinaryBoolOp" then - mark_relational_nodes(ast.left, index) - mark_relational_nodes(ast.right, index) + 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 == "UnaryBoolOp" then + mark_relational_nodes(ast.argument, index) - elseif t == "BinaryRelOp" then - ast.index = index + elseif t == "BinaryRelOp" then + ast.index = index - elseif t == "UnaryRelOp" then - ast.index = index + elseif t == "UnaryRelOp" then + ast.index = index - else - error ("Unexpected type in mark_relational_nodes: "..t) - end + 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 + 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 + 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 + local t = node.type - if t == "BinaryBoolOp" then + 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 + -- "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) + 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 + 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 == "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 == "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) + 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 + else + error("Unexpected type in install_filter: " .. t) + end end function set_output(output_format, state) - if(output_ast.type == "OutputFormat") then + if (output_ast.type == "OutputFormat") then - local format + local format - else - error ("Unexpected type in set_output: ".. output_ast.type) - end + 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={}, - 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 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 = {} + 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 .. "'" +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 - 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 +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 ) ) +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 - end - return "{" .. table.concat( result, "," ) .. "}" + 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 @@ -185,655 +212,680 @@ end -- line numbers for objects. function split_lines(rules_content) - lines = {} - indices = {} + lines = {} + indices = {} - idx = 1 - last_pos = 1 - pos = string.find(rules_content, "\n", 1, true) + 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 + 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 + idx = idx + 1 + end - last_pos = pos+1 - pos = string.find(rules_content, "\n", pos+1, true) - 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 + 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 + idx = idx + 1 + end - -- Add a final index for last line in document - indices[#indices+1] = idx + -- Add a final index for last line in document + indices[#indices + 1] = idx - return lines, indices + return lines, indices end function get_orig_yaml_obj(rules_lines, row) - local ret = "" + local ret = "" - idx = row - while (idx <= #rules_lines) do - ret = ret..rules_lines[idx].."\n" - idx = idx + 1 + 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 + if idx > #rules_lines or rules_lines[idx] == "" or string.sub(rules_lines[idx], 1, 1) == '-' then + break + end + end - return ret + return ret end function get_lines(rules_lines, row, num_lines) - local ret = "" + local ret = "" - idx = row - while (idx < (row + num_lines) and idx <= #rules_lines) do - ret = ret..rules_lines[idx].."\n" - idx = idx + 1 - end + idx = row + while (idx < (row + num_lines) and idx <= #rules_lines) do + ret = ret .. rules_lines[idx] .. "\n" + idx = idx + 1 + end - return ret + return ret end function build_error(rules_lines, row, num_lines, err) - local ret = err.."\n---\n"..get_lines(rules_lines, row, num_lines).."---" + local ret = err .. "\n---\n" .. get_lines(rules_lines, row, num_lines) .. "---" - return ret + return ret end function build_error_with_context(ctx, err) - local ret = err.."\n---\n"..ctx.."---" - return ret + local ret = err .. "\n---\n" .. ctx .. "---" + return ret end function load_rules_doc(rules_mgr, doc, load_state) - -- 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 + -- 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 + 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]) + -- 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.") - end + 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.") + end - v['context'] = context + 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 (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)) - 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)) + end - elseif (v['macro']) then + 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") - end + if (v['macro'] == nil or type(v['macro']) == "table") then + return false, build_error_with_context(v['context'], "Macro name is empty") + end - if v['source'] == nil then - v['source'] = "syscall" - 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 + 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) - end - 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) + end + end - -- Possibly append to the condition field of an existing macro - append = false + -- Possibly append to the condition field of an existing macro + append = false - if v['append'] then - append = v['append'] - end + 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") - 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") + end - state.macros_by_name[v['macro']]['condition'] = state.macros_by_name[v['macro']]['condition'] .. " " .. v['condition'] + 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'] + -- 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 + else + state.macros_by_name[v['macro']] = v + end - elseif (v['list']) then + 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") - end + if (v['list'] == nil or type(v['list']) == "table") then + return false, build_error_with_context(v['context'], "List name is empty") + end - if state.lists_by_name[v['list']] == nil then - state.ordered_list_names[#state.ordered_list_names+1] = v['list'] - 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) - end - 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) + end + end - -- Possibly append to an existing list - append = false + -- Possibly append to an existing list + append = false - if v['append'] then - append = v['append'] - end + 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") - 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") + 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 + 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 + 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") - end + if (v['rule'] == nil or type(v['rule']) == "table") then + return false, build_error_with_context(v['context'], "Rule name is empty") + 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 + -- 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 + if v['source'] == nil then + v['source'] = "syscall" + end - -- Possibly append to the condition field of an existing rule - append = false + -- Possibly append to the condition field of an existing rule + append = false - if v['append'] then - append = v['append'] - end + if v['append'] then + append = v['append'] + end - if append then + if append then - -- For append rules, all you need is the condition - for j, field in ipairs({'condition'}) do - if (v[field] == nil) then - return false, build_error_with_context(v['context'], "Rule must have property "..field) - end - end + -- For append rules, all you need is the condition + for j, field in ipairs({'condition'}) do + if (v[field] == nil) then + return false, build_error_with_context(v['context'], "Rule must have property " .. field) + end + end - 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") - end - else - state.rules_by_name[v['rule']]['condition'] = state.rules_by_name[v['rule']]['condition'] .. " " .. v['condition'] + 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") + end + else + state.rules_by_name[v['rule']]['condition'] = + state.rules_by_name[v['rule']]['condition'] .. " " .. v['condition'] - -- 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 + -- 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 + 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) - end - end + 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) + end + end - -- Convert the priority-as-string to a priority-as-number now - v['priority_num'] = priorities[v['priority']] + -- 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'] == 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 + 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']) + -- 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 - -- Remove the context from the table, so the table is exactly what was parsed - local context = v['context'] - v['context'] = nil - return false, build_error_with_context(context, "Unknown rule object: "..table.tostring(v)) - end - end + state.rules_by_name[v['rule']] = v + else + state.skipped_rules_by_name[v['rule']] = v + end + end + else + -- Remove the context from the table, so the table is exactly what was parsed + local context = v['context'] + v['context'] = nil + return false, build_error_with_context(context, "Unknown rule object: " .. table.tostring(v)) + end + end - return true, "" + return true, "" end -function load_rules(sinsp_lua_parser, - json_lua_parser, - rules_content, - rules_mgr, - verbose, - all_events, - extra, - replace_container_info, - min_priority) +function load_rules(sinsp_lua_parser, json_lua_parser, rules_content, rules_mgr, verbose, all_events, extra, + replace_container_info, min_priority) - local load_state = {lines={}, indices={}, cur_item_idx=0, min_priority=min_priority, required_engine_version=0} + 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) + load_state.lines, load_state.indices = split_lines(rules_content) - local status, docs = pcall(yaml.load, rules_content, { all = true }) + local status, docs = pcall(yaml.load, rules_content, { + all = true + }) - if status == false then - local pat = "^([%d]+):([%d]+): " - -- docs is actually an error string + if status == false then + local pat = "^([%d]+):([%d]+): " + -- docs is actually an error string - local row = 0 - local col = 0 + 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, col = string.match(docs, pat) + if row ~= nil and col ~= nil then + docs = string.gsub(docs, pat, "") + end - row = tonumber(row) - col = tonumber(col) + row = tonumber(row) + col = tonumber(col) - return false, build_error(load_state.lines, row, 3, docs) - end + return false, build_error(load_state.lines, row, 3, docs) + end - if docs == nil then - -- An empty rules file is acceptable - return true, load_state.required_engine_version - end + if docs == nil then + -- An empty rules file is acceptable + return true, load_state.required_engine_version + end - if type(docs) ~= "table" then - return false, build_error(load_state.lines, 1, 1, "Rules content is not yaml") - end + if type(docs) ~= "table" then + return false, build_error(load_state.lines, 1, 1, "Rules content is not yaml") + end - for docidx, doc in ipairs(docs) do + for docidx, doc in ipairs(docs) do - if type(doc) ~= "table" then - return false, build_error(load_state.lines, 1, 1, "Rules content is not yaml") - end + if type(doc) ~= "table" then + return false, build_error(load_state.lines, 1, 1, "Rules content is not yaml") + 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, build_error(load_state.lines, 1, 1, "Rules content is not yaml array of objects") - end - 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, build_error(load_state.lines, 1, 1, "Rules content is not yaml array of objects") + end + end - res, errstr = load_rules_doc(rules_mgr, doc, load_state) + res, errstr = load_rules_doc(rules_mgr, doc, load_state) - if not res then - return res, errstr - end - end + if not res then + return res, errstr + 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) + -- 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 + for i, name in ipairs(state.ordered_list_names) do - local v = state.lists_by_name[name] + 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 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].items) do - items[#items+1] = exp_item - end - end - end + -- 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].items) do + items[#items + 1] = exp_item + end + end + end - state.lists[v['list']] = {["items"] = items, ["used"] = false} - end + state.lists[v['list']] = { + ["items"] = items, + ["used"] = false + } + end - for _, name in ipairs(state.ordered_macro_names) do + for _, name in ipairs(state.ordered_macro_names) do - local v = state.macros_by_name[name] + local v = state.macros_by_name[name] - local status, ast = compiler.compile_macro(v['condition'], state.macros, state.lists) + local status, ast = compiler.compile_macro(v['condition'], state.macros, state.lists) - if status == false then - return false, build_error_with_context(v['context'], ast) - end + if status == false then + return false, build_error_with_context(v['context'], ast) + 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 + 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 + state.macros[v['macro']] = { + ["ast"] = ast.filter.value, + ["used"] = false + } + end - for _, name in ipairs(state.ordered_rule_names) do + for _, name in ipairs(state.ordered_rule_names) do - local v = state.rules_by_name[name] + local v = state.rules_by_name[name] - warn_evttypes = true - if v['warn_evttypes'] ~= nil then - warn_evttypes = v['warn_evttypes'] - 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['condition'], - state.macros, state.lists) + local status, filter_ast, filters = + compiler.compile_filter(v['rule'], v['condition'], state.macros, state.lists) - if status == false then - return false, build_error_with_context(v['context'], filter_ast) - end + if status == false then + return false, build_error_with_context(v['context'], filter_ast) + end - local evtttypes = {} - local syscallnums = {} + 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 + 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['condition'], warn_evttypes, verbose) - end + evttypes, syscallnums = sinsp_rule_utils.get_evttypes_syscalls(name, filter_ast, v['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 - found = false + -- 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 + found = false - if defined_noarg_filters[filter] ~= nil then - found = true - else - bracket_idx = string.find(filter, "[", 1, true) + if defined_noarg_filters[filter] ~= nil then + found = true + else + bracket_idx = string.find(filter, "[", 1, true) - if bracket_idx ~= nil then - subfilter = string.sub(filter, 1, bracket_idx-1) + if bracket_idx ~= nil then + subfilter = string.sub(filter, 1, bracket_idx - 1) - if defined_arg_filters[subfilter] ~= nil then - found = true - end - end + if defined_arg_filters[subfilter] ~= nil then + found = true + end + end - if not found then - dot_idx = string.find(filter, ".", 1, true) + if not found then + dot_idx = string.find(filter, ".", 1, true) - while dot_idx ~= nil do - subfilter = string.sub(filter, 1, dot_idx-1) + while dot_idx ~= nil do + subfilter = string.sub(filter, 1, dot_idx - 1) - if defined_arg_filters[subfilter] ~= nil then - found = true - break - end + if defined_arg_filters[subfilter] ~= nil then + found = true + break + end - dot_idx = string.find(filter, ".", dot_idx+1, true) - end - end - end + dot_idx = string.find(filter, ".", dot_idx + 1, true) + end + end + end - if not found then - if v['skip-if-unknown-filter'] then - if verbose then - print("Skipping rule \""..v['rule'].."\" that contains unknown filter "..filter) - end - goto next_rule - else - error("Rule \""..v['rule'].."\" contains unknown filter "..filter) - end - end - end + if not found then + if v['skip-if-unknown-filter'] then + if verbose then + print("Skipping rule \"" .. v['rule'] .. "\" that contains unknown filter " .. filter) + end + goto next_rule + else + error("Rule \"" .. v['rule'] .. "\" contains unknown filter " .. filter) + end + end + end - if (filter_ast.type == "Rule") then - state.n_rules = state.n_rules + 1 + if (filter_ast.type == "Rule") then + state.n_rules = state.n_rules + 1 - state.rules_by_idx[state.n_rules] = v + 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) + -- 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']) + 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) + 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 + 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 + -- 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 + -- 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 (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 + -- 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 + -- 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. - formatter = formats.formatter(v['source'], v['output']) - formats.free_formatter(v['source'], formatter) - else - return false, build_error_with_context(v['context'], "Unexpected type in load_rule: "..filter_ast.type) - 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['source'], v['output']) + formats.free_formatter(v['source'], formatter) + else + return false, build_error_with_context(v['context'], "Unexpected type in load_rule: " .. filter_ast.type) + end - ::next_rule:: - end + ::next_rule:: + end - if verbose then - -- 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 - print("Warning: macro "..name.." not refered to by any rule/macro") - end - end + if verbose then + -- 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 + print("Warning: macro " .. name .. " not refered to by any rule/macro") + end + end - for name, list in pairs(state.lists) do - if list.used == false then - print("Warning: list "..name.." not refered to by any rule/macro/list") - end - end - end + for name, list in pairs(state.lists) do + if list.used == false then + print("Warning: list " .. name .. " not refered to by any rule/macro/list") + end + end + end - io.flush() + io.flush() - return true, load_state.required_engine_version + return true, load_state.required_engine_version 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) + 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 + 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, "", "")) + -- 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() + 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) + 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 + 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={}} +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 + 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] + 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_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 + 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 + -- Prefix output with '*' so formatting is permissive + output = "*" .. rule.output - return rule.rule, rule.priority_num, output + return rule.rule, rule.priority_num, 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("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 + print("Triggered rules by rule name:") + for name, count in pairs(rule_output_counts.by_name) do + print(" " .. name .. ": " .. count) + end end - - diff --git a/userspace/engine/lua/sinsp_rule_utils.lua b/userspace/engine/lua/sinsp_rule_utils.lua index 97a994d2..23880b58 100644 --- a/userspace/engine/lua/sinsp_rule_utils.lua +++ b/userspace/engine/lua/sinsp_rule_utils.lua @@ -1,4 +1,4 @@ --- Copyright (C) 2019 The Falco Authors. +-- Copyright (C) 2020 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. @@ -12,55 +12,52 @@ -- See the License for the specific language governing permissions and -- limitations under the License. -- - local parser = require("parser") local sinsp_rule_utils = {} function sinsp_rule_utils.check_for_ignored_syscalls_events(ast, filter_type, source) - function check_syscall(val) - if ignored_syscalls[val] then - error("Ignored syscall \""..val.."\" in "..filter_type..": "..source) - end + function check_syscall(val) + if ignored_syscalls[val] then + error("Ignored syscall \"" .. val .. "\" in " .. filter_type .. ": " .. source) + end - end + end - function check_event(val) - if ignored_events[val] then - error("Ignored event \""..val.."\" in "..filter_type..": "..source) - end - end + function check_event(val) + if ignored_events[val] then + error("Ignored event \"" .. val .. "\" in " .. filter_type .. ": " .. source) + end + end - function cb(node) - if node.left.type == "FieldName" and - (node.left.value == "evt.type" or - node.left.value == "syscall.type") then + function cb(node) + if node.left.type == "FieldName" and (node.left.value == "evt.type" or node.left.value == "syscall.type") then - if (node.operator == "in" or - node.operator == "intersects" or - node.operator == "pmatch") then - for i, v in ipairs(node.right.elements) do - if v.type == "BareString" then - if node.left.value == "evt.type" then - check_event(v.value) - else - check_syscall(v.value) - end - end - end - else - if node.right.type == "BareString" then - if node.left.value == "evt.type" then - check_event(node.right.value) - else - check_syscall(node.right.value) - end - end - end - end - end + if (node.operator == "in" or node.operator == "intersects" or node.operator == "pmatch") then + for i, v in ipairs(node.right.elements) do + if v.type == "BareString" then + if node.left.value == "evt.type" then + check_event(v.value) + else + check_syscall(v.value) + end + end + end + else + if node.right.type == "BareString" then + if node.left.value == "evt.type" then + check_event(node.right.value) + else + check_syscall(node.right.value) + end + end + end + end + end - parser.traverse_ast(ast, {BinaryRelOp=1}, cb) + parser.traverse_ast(ast, { + BinaryRelOp = 1 + }, cb) end -- Examine the ast and find the event types/syscalls for which the @@ -75,125 +72,129 @@ end function sinsp_rule_utils.get_evttypes_syscalls(name, ast, source, warn_evttypes, verbose) - local evttypes = {} - local syscallnums = {} - local evtnames = {} - local found_event = false - local found_not = false - local found_event_after_not = false + local evttypes = {} + local syscallnums = {} + local evtnames = {} + local found_event = false + local found_not = false + local found_event_after_not = false - function cb(node) - if node.type == "UnaryBoolOp" then - if node.operator == "not" then - found_not = true - end - else - if node.operator == "!=" then - found_not = true - end - if node.left.type == "FieldName" and node.left.value == "evt.type" then - found_event = true - if found_not then - found_event_after_not = true - end - if (node.operator == "in" or - node.operator == "intersects" or - node.operator == "pmatch") then - for i, v in ipairs(node.right.elements) do - if v.type == "BareString" then + function cb(node) + if node.type == "UnaryBoolOp" then + if node.operator == "not" then + found_not = true + end + else + if node.operator == "!=" then + found_not = true + end + if node.left.type == "FieldName" and node.left.value == "evt.type" then + found_event = true + if found_not then + found_event_after_not = true + end + if (node.operator == "in" or node.operator == "intersects" or node.operator == "pmatch") then + for i, v in ipairs(node.right.elements) do + if v.type == "BareString" then - -- The event must be a known event - if events[v.value] == nil and syscalls[v.value] == nil then - error("Unknown event/syscall \""..v.value.."\" in filter: "..source) - end + -- The event must be a known event + if events[v.value] == nil and syscalls[v.value] == nil then + error("Unknown event/syscall \"" .. v.value .. "\" in filter: " .. source) + end - evtnames[v.value] = 1 - if events[v.value] ~= nil then - for id in string.gmatch(events[v.value], "%S+") do - evttypes[id] = 1 - end - end + evtnames[v.value] = 1 + if events[v.value] ~= nil then + for id in string.gmatch(events[v.value], "%S+") do + evttypes[id] = 1 + end + end - if syscalls[v.value] ~= nil then - for id in string.gmatch(syscalls[v.value], "%S+") do - syscallnums[id] = 1 - end - end - end - end - else - if node.right.type == "BareString" then + if syscalls[v.value] ~= nil then + for id in string.gmatch(syscalls[v.value], "%S+") do + syscallnums[id] = 1 + end + end + end + end + else + if node.right.type == "BareString" then - -- The event must be a known event - if events[node.right.value] == nil and syscalls[node.right.value] == nil then - error("Unknown event/syscall \""..node.right.value.."\" in filter: "..source) - end + -- The event must be a known event + if events[node.right.value] == nil and syscalls[node.right.value] == nil then + error("Unknown event/syscall \"" .. node.right.value .. "\" in filter: " .. source) + end - evtnames[node.right.value] = 1 - if events[node.right.value] ~= nil then - for id in string.gmatch(events[node.right.value], "%S+") do - evttypes[id] = 1 - end - end + evtnames[node.right.value] = 1 + if events[node.right.value] ~= nil then + for id in string.gmatch(events[node.right.value], "%S+") do + evttypes[id] = 1 + end + end - if syscalls[node.right.value] ~= nil then - for id in string.gmatch(syscalls[node.right.value], "%S+") do - syscallnums[id] = 1 - end - end - end - end - end - end - end + if syscalls[node.right.value] ~= nil then + for id in string.gmatch(syscalls[node.right.value], "%S+") do + syscallnums[id] = 1 + end + end + end + end + end + end + end - parser.traverse_ast(ast.filter.value, {BinaryRelOp=1, UnaryBoolOp=1} , cb) + parser.traverse_ast(ast.filter.value, { + BinaryRelOp = 1, + UnaryBoolOp = 1 + }, cb) - if not found_event then - if warn_evttypes == true then - io.stderr:write("Rule "..name..": warning (no-evttype):\n") - io.stderr:write(source.."\n") - io.stderr:write(" did not contain any evt.type restriction, meaning it will run for all event types.\n") - io.stderr:write(" This has a significant performance penalty. Consider adding an evt.type restriction if possible.\n") - end - evttypes = {} - syscallnums = {} - evtnames = {} - end + if not found_event then + if warn_evttypes == true then + io.stderr:write("Rule " .. name .. ": warning (no-evttype):\n") + io.stderr:write(source .. "\n") + io.stderr:write( + " did not contain any evt.type restriction, meaning it will run for all event types.\n") + io.stderr:write( + " This has a significant performance penalty. Consider adding an evt.type restriction if possible.\n") + end + evttypes = {} + syscallnums = {} + evtnames = {} + end - if found_event_after_not then - if warn_evttypes == true then - io.stderr:write("Rule "..name..": warning (trailing-evttype):\n") - io.stderr:write(source.."\n") - io.stderr:write(" does not have all evt.type restrictions at the beginning of the condition,\n") - io.stderr:write(" or uses a negative match (i.e. \"not\"/\"!=\") for some evt.type restriction.\n") - io.stderr:write(" This has a performance penalty, as the rule can not be limited to specific event types.\n") - io.stderr:write(" Consider moving all evt.type restrictions to the beginning of the rule and/or\n") - io.stderr:write(" replacing negative matches with positive matches if possible.\n") - end - evttypes = {} - syscallnums = {} - evtnames = {} - end + if found_event_after_not then + if warn_evttypes == true then + io.stderr:write("Rule " .. name .. ": warning (trailing-evttype):\n") + io.stderr:write(source .. "\n") + io.stderr:write(" does not have all evt.type restrictions at the beginning of the condition,\n") + io.stderr:write(" or uses a negative match (i.e. \"not\"/\"!=\") for some evt.type restriction.\n") + io.stderr:write( + " This has a performance penalty, as the rule can not be limited to specific event types.\n") + io.stderr:write(" Consider moving all evt.type restrictions to the beginning of the rule and/or\n") + io.stderr:write(" replacing negative matches with positive matches if possible.\n") + end + evttypes = {} + syscallnums = {} + evtnames = {} + end - evtnames_only = {} - local num_evtnames = 0 - for name, dummy in pairs(evtnames) do - table.insert(evtnames_only, name) - num_evtnames = num_evtnames + 1 - end + evtnames_only = {} + local num_evtnames = 0 + for name, dummy in pairs(evtnames) do + table.insert(evtnames_only, name) + num_evtnames = num_evtnames + 1 + end - if num_evtnames == 0 then - table.insert(evtnames_only, "all") - end + if num_evtnames == 0 then + table.insert(evtnames_only, "all") + end - table.sort(evtnames_only) + table.sort(evtnames_only) - if verbose then - io.stderr:write("Event types/Syscalls for rule "..name..": "..table.concat(evtnames_only, ",").."\n") - end + if verbose then + io.stderr:write("Event types/Syscalls for rule " .. name .. ": " .. table.concat(evtnames_only, ",") .. "\n") + end - return evttypes, syscallnums + return evttypes, syscallnums end return sinsp_rule_utils