diff --git a/.gitignore b/.gitignore index 82d1ec7b..3c687439 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,4 @@ test/build .vscode/* -.luacheckcache - *.idea* diff --git a/.luacheckrc b/.luacheckrc deleted file mode 100644 index 5ebc8bb5..00000000 --- a/.luacheckrc +++ /dev/null @@ -1,8 +0,0 @@ -std = "min" -cache = true -include_files = { - "userspace/engine/lua/*.lua", - "userspace/engine/lua/lyaml/*.lua", - "*.luacheckrc" -} -exclude_files = {"build"} diff --git a/userspace/engine/CMakeLists.txt b/userspace/engine/CMakeLists.txt index a5312b8a..72e55599 100644 --- a/userspace/engine/CMakeLists.txt +++ b/userspace/engine/CMakeLists.txt @@ -10,10 +10,7 @@ # "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. -add_subdirectory(lua) - set(FALCO_ENGINE_SOURCE_FILES - rules.cpp falco_common.cpp falco_engine.cpp falco_utils.cpp diff --git a/userspace/engine/falco_common.cpp b/userspace/engine/falco_common.cpp index 9a9a30ce..a25401f4 100644 --- a/userspace/engine/falco_common.cpp +++ b/userspace/engine/falco_common.cpp @@ -14,11 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -#include - #include "falco_common.h" -#include "banned.h" // This raises a compilation error when certain functions are used -#include "falco_engine_lua_files.hh" std::vector falco_common::priority_names = { "Emergency", diff --git a/userspace/engine/falco_common.h b/userspace/engine/falco_common.h index a3a3ea0e..8f262f1f 100644 --- a/userspace/engine/falco_common.h +++ b/userspace/engine/falco_common.h @@ -19,13 +19,6 @@ limitations under the License. #include #include #include - -extern "C" { -#include "lua.h" -#include "lualib.h" -#include "lauxlib.h" -} - #include // diff --git a/userspace/engine/falco_engine.cpp b/userspace/engine/falco_engine.cpp index 5ff5df21..fa620516 100644 --- a/userspace/engine/falco_engine.cpp +++ b/userspace/engine/falco_engine.cpp @@ -28,17 +28,9 @@ limitations under the License. #include "formats.h" -#include "lua_filter_helper.h" -extern "C" { -#include "lyaml.h" -} - #include "utils.h" #include "banned.h" // This raises a compilation error when certain functions are used - -string lua_on_event = "on_event"; -string lua_print_stats = "print_stats"; const std::string falco_engine::s_default_ruleset = "falco-default-ruleset"; using namespace std; diff --git a/userspace/engine/falco_engine.h b/userspace/engine/falco_engine.h index 664be16d..2a6e5ec8 100644 --- a/userspace/engine/falco_engine.h +++ b/userspace/engine/falco_engine.h @@ -29,7 +29,6 @@ limitations under the License. #include #include "gen_filter.h" -#include "rules.h" #include "ruleset.h" #include "falco_common.h" @@ -40,7 +39,7 @@ limitations under the License. // handled in a separate class falco_outputs. // -class falco_engine : public falco_common +class falco_engine { public: falco_engine(bool seed_rng=true); diff --git a/userspace/engine/formats.h b/userspace/engine/formats.h index 2424c0cf..0373e101 100644 --- a/userspace/engine/formats.h +++ b/userspace/engine/formats.h @@ -18,16 +18,7 @@ limitations under the License. #include #include - -extern "C" -{ -#include "lua.h" -#include "lualib.h" -#include "lauxlib.h" -} - #include - #include "falco_engine.h" class falco_formats diff --git a/userspace/engine/lua/CMakeLists.txt b/userspace/engine/lua/CMakeLists.txt deleted file mode 100644 index fb9926c9..00000000 --- a/userspace/engine/lua/CMakeLists.txt +++ /dev/null @@ -1,21 +0,0 @@ -# -# Copyright (C) 2021 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. - -file(GLOB_RECURSE lua_files ${CMAKE_CURRENT_SOURCE_DIR} *.lua) - -add_custom_command(OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/falco_engine_lua_files.cpp - COMMAND bash ${CMAKE_CURRENT_SOURCE_DIR}/lua-to-cpp.sh ${CMAKE_CURRENT_SOURCE_DIR} ${LYAML_LUA_DIR} ${CMAKE_CURRENT_BINARY_DIR} - DEPENDS ${lua_files} ${CMAKE_CURRENT_SOURCE_DIR}/lua-to-cpp.sh lyaml) - -add_library(luafiles falco_engine_lua_files.cpp) - -target_include_directories(luafiles PUBLIC ${CMAKE_CURRENT_BINARY_DIR}) diff --git a/userspace/engine/lua/lua-to-cpp.sh b/userspace/engine/lua/lua-to-cpp.sh deleted file mode 100644 index 332fef79..00000000 --- a/userspace/engine/lua/lua-to-cpp.sh +++ /dev/null @@ -1,78 +0,0 @@ -#!/bin/bash - -set -euo pipefail - -LUA_FILE_DIR=$1 -LYAML_LUA_DIR=$2 -OUTPUT_DIR=$3 - -MODULE_SYMS=() -CODE_SYMS=() - -function add_lua_file { - filename=$1 - is_module=$2 - - # Take the basename of the file - BASE_NAME=$(basename ${file} .lua) - SYMBOL_NAME="${BASE_NAME}_lua_file_contents" - FILE_CONTENTS=$(<${file}) - - # Add a symbol to the .cc file containing the contents of the file - echo "const char *${SYMBOL_NAME}=R\"LUAFILE(${FILE_CONTENTS})LUAFILE\";" >> ${OUTPUT_DIR}/falco_engine_lua_files.cpp - - # Add an extern reference to the .hh file - echo "extern const char *${SYMBOL_NAME};" >> ${OUTPUT_DIR}/falco_engine_lua_files.hh - - if [[ "${is_module}" == "true" ]]; then - # Determine the module name for the file - if [[ "${file}" == *"/"* ]]; then - MODULE_NAME=$(echo ${file} | tr / . | sed -e 's/.lua//') - else - MODULE_NAME=$(basename ${file} .lua) - fi - - # Add the pair (string contents, module name) to MODULE_SYMS - PAIR=$(echo "{${SYMBOL_NAME},\"${MODULE_NAME}\"}") - MODULE_SYMS+=(${PAIR}) - else - # Add the string to CODE_SYMS - CODE_SYMS+=(${SYMBOL_NAME}) - fi -} - -cat < ${OUTPUT_DIR}/falco_engine_lua_files.cpp -// Automatically generated. Do not edit -#include "falco_engine_lua_files.hh" -EOF - -cat < ${OUTPUT_DIR}/falco_engine_lua_files.hh -#pragma once -// Automatically generated. Do not edit -#include -#include -EOF - -# lyaml and any files in the "modules" subdirectory are treated as lua -# modules. -pushd ${LYAML_LUA_DIR} -for file in *.lua */*.lua; do - add_lua_file $file "true" -done -popd - -# Any .lua files in this directory are treated as code with functions -# to execute. -pushd ${LUA_FILE_DIR} -for file in ${LUA_FILE_DIR}/*.lua; do - add_lua_file $file "false" -done -popd - -# Create a list of lua module (string, module name) pairs from MODULE_SYMS -echo "extern std::list> lua_module_strings;" >> ${OUTPUT_DIR}/falco_engine_lua_files.hh -echo "std::list> lua_module_strings = {$(IFS=, ; echo "${MODULE_SYMS[*]}")};" >> ${OUTPUT_DIR}/falco_engine_lua_files.cpp - -# Create a list of lua code strings from CODE_SYMS -echo "extern std::list lua_code_strings;" >> ${OUTPUT_DIR}/falco_engine_lua_files.hh -echo "std::list lua_code_strings = {$(IFS=, ; echo "${CODE_SYMS[*]}")};" >> ${OUTPUT_DIR}/falco_engine_lua_files.cpp diff --git a/userspace/engine/lua/rule_loader.lua b/userspace/engine/lua/rule_loader.lua deleted file mode 100644 index 048138e3..00000000 --- a/userspace/engine/lua/rule_loader.lua +++ /dev/null @@ -1,1247 +0,0 @@ --- 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 yaml = require"lyaml" - ---http://lua-users.org/wiki/StringTrim -function trim(s) - if (type(s) ~= "string") then - return s - end - return (s:gsub("^%s*(.-)%s*$", "%1")) -end - - -function expand_list(source, list_defs) - for name, def in pairs(list_defs) do - local bpos = string.find(source, name, 1, true) - while bpos ~= nil do - def.used = true - 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 - -- Shift pointers to consume all whitespaces - while (bpos > 1 and string.match(string.sub(source, bpos-1, bpos-1), "[%s]")) do - bpos = bpos - 1 - end - while (epos < string.len(source) and string.match(string.sub(source, epos, epos), "[%s]")) do - epos = epos + 1 - end - - -- Create substitution string by concatenating all values - local sub = table.concat(def.items, ", ") - - -- If substituted list is empty, we need to remove a comma from the left or the right - if string.len(sub) == 0 then - if (bpos > 1 and string.sub(source, bpos-1, bpos-1) == ",") then - bpos = bpos - 1 - elseif (epos < string.len(source) and string.sub(source, epos, epos) == ",") then - epos = epos + 1 - end - -- If no comma is removed, this means that the list is only surrounded by parenthesis - -- or other characters - end - - -- Compose new string with substitution - local new_source = "" - if bpos > 1 then - new_source = new_source..string.sub(source, 1, bpos-1).." " - end - new_source = new_source..sub.." " - if epos <= string.len(source) then - new_source = new_source..string.sub(source, epos, string.len(source)) - end - - -- Iterate to the next match - source = new_source - bpos = bpos + (string.len(sub)-string.len(name)) - end - bpos = string.find(source, name, bpos+1, true) - end - end - return source -end - -function parse_macro(line, macro_defs, list_defs) - -- Substitute list in macro filter string - line = expand_list(line, list_defs) - - -- Parse the macro to an AST - local ok, filter_or_error = filter_helper.parse_filter(line) - if (ok == false) then - local err = filter_or_error - local msg = "Compilation error when compiling \""..line.."\": ".. err - return false, msg - end - local filter = filter_or_error - - -- Validate the macro - local filter_copy = filter_helper.clone_ast(filter) - local expand_macros = true - while (expand_macros) do - expand_macros = false - for m_name, macro in pairs(macro_defs) do - local expanded, new_filter_copy = filter_helper.expand_macro( - filter_copy, m_name, macro.ast) - if (expanded) then - expand_macros = true - macro.used = true - end - filter_copy = new_filter_copy - end - end - local has_unknown_macro, unknown_macro = filter_helper.find_unknown_macro(filter_copy) - filter_helper.delete_ast(filter_copy) - if (has_unknown_macro) then - filter_helper.delete_ast(filter) - local msg = "Compilation error when compiling \""..line.."\": Undefined macro '"..unknown_macro.."' used in filter." - return false, msg - end - - return true, filter -end - ---[[ - Parses a single filter, then expands macros using passed-in table of definitions. Returns resulting AST. ---]] -function parse_rule(name, source, macro_defs, list_defs) - -- Substitute list in rule filter string - -- todo(jasondellaluce): we are supposed to do better than text-substitution, this is what - -- breaks escaping in lists and exceptions of Falco rulesets - source = expand_list(source, list_defs) - - -- Parse the rule filter to an AST - local ok, filter_or_err = filter_helper.parse_filter(source) - if (ok == false) then - local err = filter_or_err - local msg = "Compilation error when compiling \""..source.."\": ".. err - return false, msg - end - local filter = filter_or_err - - -- Expand all macros - local expand_macros = true - while (expand_macros) do - expand_macros = false - for m_name, macro in pairs(macro_defs) do - local expanded, new_filter = filter_helper.expand_macro( - filter, m_name, macro.ast) - if (expanded) then - expand_macros = true - macro.used = true - end - filter = new_filter - end - end - local has_unknown_macro, unknown_macro = filter_helper.find_unknown_macro(filter) - if (has_unknown_macro) then - filter_helper.delete_ast(filter) - local msg = "Undefined macro '"..unknown_macro.."' used in filter." - return false, msg - end - - return true, filter -end - --- Permissive for case and for common abbreviations. -priorities = { - Emergency=0, Alert=1, Critical=2, Error=3, Warning=4, Notice=5, Informational=6, Debug=7, - emergency=0, alert=1, critical=2, error=3, warning=4, notice=5, informational=6, debug=7, - EMERGENCY=0, ALERT=1, CRITICAL=2, ERROR=3, WARNING=4, NOTICE=5, INFORMATIONAL=6, DEBUG=7, - INFO=6, info=6 -} - --- 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={}, 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) - idx = row - local t = {} - while (idx <= #rules_lines) do - t[#t + 1] = rules_lines[idx] - idx = idx + 1 - - if idx > #rules_lines or rules_lines[idx] == "" or string.sub(rules_lines[idx], 1, 1) == '-' then - break - end - end - t[#t + 1] = "" - local ret = "" - ret = table.concat(t, "\n") - 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(rules_mgr, source, 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 falco_rules.is_defined_field(rules_mgr, source, 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(rules_mgr, source, 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 falco_rules.is_defined_field(rules_mgr, source, 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 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['required_plugin_versions']) then - - for _, vobj in ipairs(v['required_plugin_versions']) do - if vobj['name'] == nil then - return false, build_error_with_context(v['context'], "required_plugin_versions item must have name property"), warnings - end - - if vobj['version'] == nil then - return false, build_error_with_context(v['context'], "required_plugin_versions item must have version property"), warnings - end - - -- In the rules yaml, it's a name + version. But it's - -- possible, although unlikely, that a single yaml blob - -- contains multiple docs, with each doc having its own - -- required_engine_version entry. So populate a map plugin - -- name -> list of required plugin versions. - if load_state.required_plugin_versions[vobj['name']] == nil then - load_state.required_plugin_versions[vobj['name']] = {} - end - - table.insert(load_state.required_plugin_versions[vobj['name']], vobj['version']) - 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 - - valid = falco_rules.is_source_valid(rules_mgr, v['source']) - - if valid == false then - msg = "Macro "..v['macro']..": warning (unknown-source): unknown source "..v['source']..", skipping" - warnings[#warnings + 1] = msg - goto next_object - 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 - - -- 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 - - valid = falco_rules.is_source_valid(rules_mgr, v['source']) - - if valid == false then - msg = "Rule "..v['rule']..": warning (unknown-source): unknown source "..v['source']..", skipping" - warnings[#warnings + 1] = msg - goto next_object - 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(rules_mgr, v['source'], eitem, v['context']) - else - valid, err = validate_exception_item_single_field(rules_mgr, v['source'], 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 (v['condition'] == nil and next(v['exceptions']) == nil) then - return false, build_error_with_context(v['context'], "Appended rule must have exceptions or condition property"), warnings - end - - if next(v['exceptions']) ~= nil 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 - - -- Separate case when a exception name is not found - -- This means that a new exception is being appended - - local new_exception = true - for _, rex_item in ipairs(state.rules_by_name[v['rule']]['exceptions']) do - if rex_item['name'] == eitem['name'] then - new_exception = false - break - end - end - - if new_exception then - local exceptions = state.rules_by_name[v['rule']]['exceptions'] - - if eitem['fields'] == nil then - return false, build_error_with_context(v['context'], "Rule exception new item "..eitem['name']..": must have fields property with a list of fields"), warnings - end - if eitem['values'] == nil then - return false, build_error_with_context(v['context'], "Rule exception new item "..eitem['name']..": must have values property with a list of values"), warnings - end - - local valid, err - if type(eitem['fields']) == "table" then - valid, err = validate_exception_item_multi_fields(rules_mgr, v['source'], eitem, v['context']) - else - valid, err = validate_exception_item_single_field(rules_mgr, v['source'], eitem, v['context']) - end - - if valid == false then - return valid, err, warnings - end - - -- Insert the complete exception object - exceptions[#exceptions+1] = eitem - else - -- Appends to existing exception here - -- You can't append exception fields or comps to an existing rule exception - if eitem['fields'] ~= nil then - return false, build_error_with_context(v['context'], "Can not append exception fields to existing rule, only values"), warnings - end - - if eitem['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 - 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 - local err = nil - for j, field in ipairs({'condition', 'output', 'desc', 'priority'}) do - if (err == nil and v[field] == nil) then - err = build_error_with_context(v['context'], "Rule must have property "..field) - end - end - - -- Handle spacial case where "enabled" flag is defined only - if (err ~= nil) then - if (v['enabled'] == nil) then - return false, err, warnings - else - if state.rules_by_name[v['rule']] == nil then - return false, build_error_with_context(v['context'], "Rule " ..v['rule'].. " has 'enabled' key only, but no rule by that name already exists"), warnings - end - state.rules_by_name[v['rule']]['enabled'] = v['enabled'] - end - else - -- 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'] = trim(v['output']) - - state.rules_by_name[v['rule']] = v - else - state.skipped_rules_by_name[v['rule']] = v - end - 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 - - ::next_object:: - 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 = {} - - icond[#icond + 1] = "(" - - local lcount = 0 - 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 lcount ~= 0 then - icond[#icond + 1] = " or " - end - lcount = lcount + 1 - - icond[#icond + 1] = "(" - - for k = 1, #fields do - if k > 1 then - icond[#icond + 1] = " 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 + 1] = fields[k] .. " " .. comps[k] .. " " .. istr - exfields[fields[k]] = true - end - - icond[#icond + 1] = ")" - end - - icond[#icond + 1] = ")" - - -- Don't return a trivially empty condition string - local ret = table.concat(icond) - if ret == "()" then - return "", nil - end - - return ret, 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 --- - required_plugin_versions. will be nil when load_result is false --- - List of Errors --- - List of Warnings -function load_rules(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, required_plugin_versions={}} - - 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, 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, 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, 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, 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, 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 = parse_macro(v['condition'], state.macros, state.lists) - - if status == false then - return false, nil, nil, build_error_with_context(v['context'], ast), warnings - end - - state.macros[v['macro']] = {["ast"] = ast, ["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, 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 = parse_rule(v['rule'], v['compile_condition'], - state.macros, state.lists) - - if status == false then - return false, nil, nil, build_error_with_context(v['context'], filter), warnings - end - - 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. - - if (v['tags'] == nil) then - v['tags'] = {} - end - - local ok, compiled_filter_or_err = filter_helper.compile_filter(rules_mgr, filter, v['source'], state.n_rules) - filter_helper.delete_ast(filter) - if (ok == false) then - local err = compiled_filter_or_err - -- If a rule has a property skip-if-unknown-filter: true, - -- and the error is about an undefined field, print a - -- message but continue. - if v['skip-if-unknown-filter'] == true and string.find(err, "filter_check called with nonexistent field") ~= nil then - local msg = "Rule "..v['rule']..": warning (unknown-field):" - warnings[#warnings + 1] = msg - else - local msg = "Rule "..v['rule']..": error "..err - return false, nil, nil, build_error_with_context(v['context'], msg), warnings - end - else - local compiled_filter = compiled_filter_or_err - local num_evttypes = falco_rules.add_filter(rules_mgr, compiled_filter, v['rule'], v['source'], v['tags']) - if v['source'] == "syscall" and (num_evttypes == 0 or num_evttypes > 100) then - if warn_evttypes == true then - local msg = "Rule "..v['rule']..": warning (no-evttype):\n".." matches too many evt.type values.\n".." This has a significant performance penalty." - warnings[#warnings + 1] = msg - end - end - 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 = falco_rules.is_format_valid(rules_mgr, v['source'], v['output']) - if err ~= nil then - return false, nil, nil, build_error_with_context(v['context'], err), 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 referred 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 referred to by any rule/macro/list" - warnings[#warnings + 1] = msg - end - end - - io.flush() - - return true, load_state.required_engine_version, load_state.required_plugin_versions, {}, 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 - - - diff --git a/userspace/engine/lua_filter_helper.cpp b/userspace/engine/lua_filter_helper.cpp deleted file mode 100644 index b1a2640b..00000000 --- a/userspace/engine/lua_filter_helper.cpp +++ /dev/null @@ -1,190 +0,0 @@ -/* -Copyright (C) 2022 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. -*/ - -#include -#include "lua_filter_helper.h" -#include "filter_macro_resolver.h" -#include "rules.h" - -using namespace std; -using namespace libsinsp::filter; - -// The code below implements the Lua wrapper. -// todo(jasondellaluce): remove this once Lua is removed from Falco -extern "C" { - #include "lua.h" - #include "lualib.h" - #include "lauxlib.h" -} - -const static struct luaL_Reg ll_filter_helper[] = -{ - {"compile_filter", &lua_filter_helper::compile_filter}, - {"parse_filter", &lua_filter_helper::parse_filter}, - {"expand_macro", &lua_filter_helper::expand_macro}, - {"find_unknown_macro", &lua_filter_helper::find_unknown_macro}, - {"clone_ast", &lua_filter_helper::clone_ast}, - {"delete_ast", &lua_filter_helper::delete_ast}, - {NULL, NULL} -}; - -void lua_filter_helper::init(lua_State *ls) -{ - luaL_openlib(ls, "filter_helper", ll_filter_helper, 0); -} - -int lua_filter_helper::parse_filter(lua_State *ls) -{ - if (! lua_isstring(ls, -1)) - { - lua_pushstring(ls, "invalid argument passed to parse_filter()"); - lua_error(ls); - } - - string filter_str = lua_tostring(ls, -1); - - parser p(filter_str); - p.set_max_depth(1000); - try - { - auto filter = p.parse(); - lua_pushboolean(ls, true); - lua_pushlightuserdata(ls, filter); - } - catch (const sinsp_exception& e) - { - string err = to_string(p.get_pos().col) + ": " + e.what(); - lua_pushboolean(ls, false); - lua_pushstring(ls, err.c_str()); - } - return 2; -} - -int lua_filter_helper::compile_filter(lua_State *ls) -{ - if (! lua_islightuserdata(ls, -4) || - ! lua_islightuserdata(ls, -3) || - ! lua_isstring(ls, -2) || - ! lua_isnumber(ls, -1)) - { - lua_pushstring(ls, "invalid argument passed to compile_filter()"); - lua_error(ls); - } - - falco_rules *rules = (falco_rules *) lua_topointer(ls, -4); - ast::expr* ast = (ast::expr*) lua_topointer(ls, -3); - std::string source = lua_tostring(ls, -2); - int32_t check_id = (int32_t) luaL_checkinteger(ls, -1); - - try - { - sinsp_filter_compiler compiler(rules->get_filter_factory(source), ast); - compiler.set_check_id(check_id); - gen_event_filter* filter = compiler.compile(); - lua_pushboolean(ls, true); - lua_pushlightuserdata(ls, filter); - } - catch (const sinsp_exception& e) - { - lua_pushboolean(ls, false); - lua_pushstring(ls, e.what()); - } - catch (const falco_exception& e) - { - lua_pushboolean(ls, false); - lua_pushstring(ls, e.what()); - } - return 2; -} - -int lua_filter_helper::expand_macro(lua_State *ls) -{ - if (! lua_islightuserdata(ls, -3) || // ast - ! lua_isstring(ls, -2) || // name - ! lua_islightuserdata(ls, -1)) // macro - { - lua_pushstring(ls, "invalid arguments passed to expand_macro()"); - lua_error(ls); - } - - ast::expr* ast = (ast::expr*) lua_topointer(ls, -3); - std::string name = lua_tostring(ls, -2); - ast::expr* macro = (ast::expr*) lua_topointer(ls, -1); - - // For now we need to clone the macro AST because the current Lua - // rule-loader implementation manages the pointer lifecycle manually, - // and it's not compatible with shared_ptr. - shared_ptr macro_clone(ast::clone(macro)); - filter_macro_resolver resolver; - resolver.set_macro(name, macro_clone); - bool resolved = resolver.run(ast); - lua_pushboolean(ls, resolved); - lua_pushlightuserdata(ls, ast); - return 2; -} - -int lua_filter_helper::find_unknown_macro(lua_State *ls) -{ - if (! lua_islightuserdata(ls, -1)) // ast - { - lua_pushstring(ls, "invalid arguments passed to find_unknown_macro()"); - lua_error(ls); - } - - ast::expr* ast = (ast::expr*) lua_topointer(ls, -1); - - // Running a macro resolver without defining any macro allows - // us to spot all the still-unresolved macros in an AST. - filter_macro_resolver resolver; - resolver.run(ast); - if (!resolver.get_unknown_macros().empty()) - { - lua_pushboolean(ls, true); - lua_pushstring(ls, resolver.get_unknown_macros().begin()->c_str()); - } - else - { - lua_pushboolean(ls, false); - lua_pushstring(ls, ""); - } - return 2; -} - -int lua_filter_helper::clone_ast(lua_State *ls) -{ - if (! lua_islightuserdata(ls, -1)) // ast - { - lua_pushstring(ls, "Invalid arguments passed to clone_ast()"); - lua_error(ls); - } - - ast::expr* ast = (ast::expr*) lua_topointer(ls, -1); - ast::expr* cloned_ast = ast::clone(ast); - lua_pushlightuserdata(ls, cloned_ast); - return 1; -} - -int lua_filter_helper::delete_ast(lua_State *ls) -{ - if (! lua_islightuserdata(ls, -1)) // ptr - { - lua_pushstring(ls, "Invalid arguments passed to delete_ast()"); - lua_error(ls); - } - - delete (ast::expr*) lua_topointer(ls, -1); - return 0; -} \ No newline at end of file diff --git a/userspace/engine/lua_filter_helper.h b/userspace/engine/lua_filter_helper.h deleted file mode 100644 index fc633dc7..00000000 --- a/userspace/engine/lua_filter_helper.h +++ /dev/null @@ -1,36 +0,0 @@ -/* -Copyright (C) 2022 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. -*/ - -#pragma once - -// todo(jasondellaluce): remove this once Lua is removed from Falco -typedef struct lua_State lua_State; - -/*! - \brief This is a Lua helper for filter-related operations - todo(jasondellaluce): remove this once Lua is removed from Falco -*/ -class lua_filter_helper -{ -public: - static void init(lua_State *ls); - static int compile_filter(lua_State *ls); - static int parse_filter(lua_State *ls); - static int expand_macro(lua_State *ls); - static int find_unknown_macro(lua_State *ls); - static int clone_ast(lua_State *ls); - static int delete_ast(lua_State *ls); -}; diff --git a/userspace/engine/rules.cpp b/userspace/engine/rules.cpp deleted file mode 100644 index 487be4fb..00000000 --- a/userspace/engine/rules.cpp +++ /dev/null @@ -1,471 +0,0 @@ -/* -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. -*/ - -#include - -#include "rules.h" - -extern "C" { -#include "lua.h" -#include "lualib.h" -#include "lauxlib.h" -} - -#include "falco_engine.h" -#include "banned.h" // This raises a compilation error when certain functions are used - -const static struct luaL_Reg ll_falco_rules[] = - { - {"clear_filters", &falco_rules::clear_filters}, - {"add_filter", &falco_rules::add_filter}, - {"enable_rule", &falco_rules::enable_rule}, - {"engine_version", &falco_rules::engine_version}, - {"is_source_valid", &falco_rules::is_source_valid}, - {"is_format_valid", &falco_rules::is_format_valid}, - {"is_defined_field", &falco_rules::is_defined_field}, - {NULL, NULL}}; - -falco_rules::falco_rules(falco_engine *engine, - lua_State *ls) - : m_engine(engine), - m_ls(ls) -{ -} - -void falco_rules::add_filter_factory(const std::string &source, - std::shared_ptr factory) -{ - m_filter_factories[source] = factory; -} - -void falco_rules::init(lua_State *ls) -{ - luaL_openlib(ls, "falco_rules", ll_falco_rules, 0); -} - -int falco_rules::clear_filters(lua_State *ls) -{ - if (! lua_islightuserdata(ls, -1)) - { - lua_pushstring(ls, "Invalid arguments passed to clear_filters()"); - lua_error(ls); - } - - falco_rules *rules = (falco_rules *) lua_topointer(ls, -1); - rules->clear_filters(); - - return 0; -} - -void falco_rules::clear_filters() -{ - m_engine->clear_filters(); -} - -std::shared_ptr falco_rules::get_filter_factory(const std::string &source) -{ - auto it = m_filter_factories.find(source); - if(it == m_filter_factories.end()) - { - throw falco_exception(string("unknown event source: ") + source); - } - return it->second; -} - -int falco_rules::add_filter(lua_State *ls) -{ - if (! lua_islightuserdata(ls, -5) || - ! lua_islightuserdata(ls, -4) || - ! lua_isstring(ls, -3) || - ! lua_isstring(ls, -2) || - ! lua_istable(ls, -1)) - { - lua_pushstring(ls, "Invalid arguments passed to add_filter()"); - lua_error(ls); - } - - falco_rules *rules = (falco_rules *) lua_topointer(ls, -5); - gen_event_filter *filter = (gen_event_filter*) lua_topointer(ls, -4); - std::string rule = lua_tostring(ls, -3); - std::string source = lua_tostring(ls, -2); - - set tags; - - lua_pushnil(ls); /* first key */ - while (lua_next(ls, -2) != 0) { - // key is at index -2, value is at index - // -1. We want the values. - tags.insert(lua_tostring(ls, -1)); - - // Remove value, keep key for next iteration - lua_pop(ls, 1); - } - - // todo(jasondellaluce,leogr,fededp): temp workaround, remove when fixed in libs - size_t num_evttypes = 1; // assume plugin - if(source == "syscall" || source == "k8s_audit") - { - num_evttypes = filter->evttypes().size(); - } - - try - { - std::shared_ptr filter_ptr(filter); - rules->add_filter(filter_ptr, rule, source, tags); - } - catch (exception &e) - { - std::string errstr = string("Could not add rule to falco engine: ") + e.what(); - lua_pushstring(ls, errstr.c_str()); - lua_error(ls); - } - - lua_pushnumber(ls, num_evttypes); - return 1; -} - -void falco_rules::add_filter(std::shared_ptr filter, string &rule, string &source, set &tags) -{ - m_engine->add_filter(filter, rule, source, tags); -} - -int falco_rules::enable_rule(lua_State *ls) -{ - if (! lua_islightuserdata(ls, -3) || - ! lua_isstring(ls, -2) || - ! lua_isnumber(ls, -1)) - { - lua_pushstring(ls, "Invalid arguments passed to enable_rule()"); - lua_error(ls); - } - - falco_rules *rules = (falco_rules *) lua_topointer(ls, -3); - const char *rulec = lua_tostring(ls, -2); - std::string rule = rulec; - bool enabled = (lua_tonumber(ls, -1) ? true : false); - - rules->enable_rule(rule, enabled); - - return 0; -} - -void falco_rules::enable_rule(string &rule, bool enabled) -{ - m_engine->enable_rule(rule, enabled); -} - -int falco_rules::engine_version(lua_State *ls) -{ - if (! lua_islightuserdata(ls, -1)) - { - lua_pushstring(ls, "Invalid arguments passed to engine_version()"); - lua_error(ls); - } - - falco_rules *rules = (falco_rules *) lua_topointer(ls, -1); - - lua_pushnumber(ls, rules->m_engine->engine_version()); - - return 1; -} - -bool falco_rules::is_source_valid(const std::string &source) -{ - return m_engine->is_source_valid(source); -} - -int falco_rules::is_source_valid(lua_State *ls) -{ - if (! lua_islightuserdata(ls, -2) || - ! lua_isstring(ls, -1)) - { - lua_pushstring(ls, "Invalid arguments passed to is_source_valid"); - lua_error(ls); - } - - falco_rules *rules = (falco_rules *) lua_topointer(ls, -2); - string source = luaL_checkstring(ls, -1); - - bool ret = rules->is_source_valid(source); - - lua_pushboolean(ls, (ret ? 1 : 0)); - - return 1; -} - -int falco_rules::is_format_valid(lua_State *ls) -{ - if (! lua_islightuserdata(ls, -3) || - ! lua_isstring(ls, -2) || - ! lua_isstring(ls, -1)) - { - lua_pushstring(ls, "Invalid arguments passed to is_format_valid"); - lua_error(ls); - } - - falco_rules *rules = (falco_rules *) lua_topointer(ls, -3); - string source = luaL_checkstring(ls, -2); - string format = luaL_checkstring(ls, -1); - string errstr; - - bool ret = rules->is_format_valid(source, format, errstr); - - if (!ret) - { - lua_pushstring(ls, errstr.c_str()); - } - else - { - lua_pushnil(ls); - } - - return 1; -} - -bool falco_rules::is_format_valid(const std::string &source, const std::string &format, std::string &errstr) -{ - bool ret = true; - - try - { - std::shared_ptr formatter; - - formatter = m_engine->create_formatter(source, format); - } - catch(exception &e) - { - std::ostringstream os; - - os << "Invalid output format '" - << format - << "': '" - << e.what() - << "'"; - - errstr = os.str(); - ret = false; - } - - return ret; -} - -int falco_rules::is_defined_field(lua_State *ls) -{ - if (! lua_islightuserdata(ls, -3) || - ! lua_isstring(ls, -2) || - ! lua_isstring(ls, -1)) - { - lua_pushstring(ls, "Invalid arguments passed to is_defined_field"); - lua_error(ls); - } - - falco_rules *rules = (falco_rules *) lua_topointer(ls, -3); - string source = luaL_checkstring(ls, -2); - string fldname = luaL_checkstring(ls, -1); - - bool ret = rules->is_defined_field(source, fldname); - - lua_pushboolean(ls, (ret ? 1 : 0)); - - return 1; -} - -bool falco_rules::is_defined_field(const std::string &source, const std::string &fldname) -{ - auto it = m_filter_factories.find(source); - - if(it == m_filter_factories.end()) - { - return false; - } - - auto *chk = it->second->new_filtercheck(fldname.c_str()); - - if (chk == NULL) - { - return false; - } - - delete(chk); - - return true; -} - -static std::list get_lua_table_values(lua_State *ls, int idx) -{ - std::list ret; - - if (lua_isnil(ls, idx)) { - return ret; - } - - lua_pushnil(ls); /* first key */ - while (lua_next(ls, idx-1) != 0) { - // key is at index -2, value is at index - // -1. We want the values. - if (! lua_isstring(ls, -1)) { - std::string err = "Non-string value in table of strings"; - throw falco_exception(err); - } - ret.push_back(string(lua_tostring(ls, -1))); - - // Remove value, keep key for next iteration - lua_pop(ls, 1); - } - - return ret; -} - -static void get_lua_table_list_values(lua_State *ls, - int idx, - std::map> &required_plugin_versions) -{ - if (lua_isnil(ls, idx)) { - return; - } - - lua_pushnil(ls); /* first key */ - while (lua_next(ls, idx-1) != 0) { - // key is at index -2, table of values is at index -1. - if (! lua_isstring(ls, -2)) { - std::string err = "Non-string key in table of strings"; - throw falco_exception(err); - } - - std::string key = string(lua_tostring(ls, -2)); - std::list vals = get_lua_table_values(ls, -1); - - if (required_plugin_versions.find(key) == required_plugin_versions.end()) - { - required_plugin_versions[key] = vals; - } - else - { - required_plugin_versions[key].insert(required_plugin_versions[key].end(), - vals.begin(), - vals.end()); - } - - // Remove value, keep key for next iteration - lua_pop(ls, 1); - } -} - - -void falco_rules::load_rules(const string &rules_content, - bool verbose, bool all_events, - string &extra, bool replace_container_info, - falco_common::priority_type min_priority, - uint64_t &required_engine_version, - std::map> &required_plugin_versions) -{ - lua_getglobal(m_ls, m_lua_load_rules.c_str()); - if(lua_isfunction(m_ls, -1)) - { - lua_pushstring(m_ls, rules_content.c_str()); - lua_pushlightuserdata(m_ls, this); - lua_pushboolean(m_ls, (verbose ? 1 : 0)); - lua_pushboolean(m_ls, (all_events ? 1 : 0)); - lua_pushstring(m_ls, extra.c_str()); - lua_pushboolean(m_ls, (replace_container_info ? 1 : 0)); - lua_pushnumber(m_ls, min_priority); - if(lua_pcall(m_ls, 7, 5, 0) != 0) - { - const char* lerr = lua_tostring(m_ls, -1); - - string err = "Error loading rules: " + string(lerr); - - throw falco_exception(err); - } - - // Returns: - // Load result: bool - // required engine version: will be nil when load result is false - // required_plugin_versions: will be nil when load result is false - // array of errors - // array of warnings - bool successful = lua_toboolean(m_ls, -5); - required_engine_version = lua_tonumber(m_ls, -4); - get_lua_table_list_values(m_ls, -3, required_plugin_versions); - std::list errors = get_lua_table_values(m_ls, -2); - std::list warnings = get_lua_table_values(m_ls, -1); - - // Concatenate errors/warnings - std::ostringstream os; - if (errors.size() > 0) - { - os << errors.size() << " errors:" << std::endl; - for(auto err : errors) - { - os << err << std::endl; - } - } - - if (warnings.size() > 0) - { - os << warnings.size() << " warnings:" << std::endl; - for(auto warn : warnings) - { - os << warn << std::endl; - } - } - - if(!successful) - { - throw falco_exception(os.str()); - } - - if (verbose && os.str() != "") { - // We don't really have a logging callback - // from the falco engine, but this would be a - // good place to use it. - fprintf(stderr, "When reading rules content: %s", os.str().c_str()); - } - - lua_pop(m_ls, 4); - - } else { - throw falco_exception("No function " + m_lua_load_rules + " found in lua rule module"); - } -} - -void falco_rules::describe_rule(std::string *rule) -{ - lua_getglobal(m_ls, m_lua_describe_rule.c_str()); - if(lua_isfunction(m_ls, -1)) - { - if (rule == NULL) - { - lua_pushnil(m_ls); - } else { - lua_pushstring(m_ls, rule->c_str()); - } - - if(lua_pcall(m_ls, 1, 0, 0) != 0) - { - const char* lerr = lua_tostring(m_ls, -1); - string err = "Could not describe " + (rule == NULL ? "all rules" : "rule " + *rule) + ": " + string(lerr); - throw falco_exception(err); - } - } else { - throw falco_exception("No function " + m_lua_describe_rule + " found in lua rule module"); - } -} - - -falco_rules::~falco_rules() -{ -} diff --git a/userspace/engine/rules.h b/userspace/engine/rules.h deleted file mode 100644 index f5448b80..00000000 --- a/userspace/engine/rules.h +++ /dev/null @@ -1,85 +0,0 @@ -/* -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. -*/ - -#pragma once - -#include -#include - -#include "sinsp.h" -#include "filter.h" - -#include "json_evt.h" -#include "falco_common.h" - -typedef struct lua_State lua_State; - -class falco_engine; - -class falco_rules -{ - public: - falco_rules(falco_engine *engine, - lua_State *ls); - ~falco_rules(); - - void add_filter_factory(const std::string &source, - std::shared_ptr factory); - - std::shared_ptr get_filter_factory(const std::string &source); - - void load_rules(const string &rules_content, bool verbose, bool all_events, - std::string &extra, bool replace_container_info, - falco_common::priority_type min_priority, - uint64_t &required_engine_version, - std::map> &required_plugin_versions); - void describe_rule(string *rule); - - bool is_source_valid(const std::string &source); - - bool is_format_valid(const std::string &source, const std::string &format, std::string &errstr); - - bool is_defined_field(const std::string &source, const std::string &field); - - static void init(lua_State *ls); - static int clear_filters(lua_State *ls); - static int add_filter(lua_State *ls); - static int enable_rule(lua_State *ls); - static int engine_version(lua_State *ls); - - static int is_source_valid(lua_State *ls); - - // err = falco_rules.is_format_valid(source, format_string) - static int is_format_valid(lua_State *ls); - - // err = falco_rules.is_defined_field(source, field) - static int is_defined_field(lua_State *ls); - - private: - void clear_filters(); - void add_filter(std::shared_ptr filter, string &rule, string &source, std::set &tags); - void enable_rule(string &rule, bool enabled); - - falco_engine *m_engine; - lua_State* m_ls; - - // Maps from event source to an object that can create rules - // for that event source. - std::map> m_filter_factories; - - string m_lua_load_rules = "load_rules"; - string m_lua_describe_rule = "describe_rule"; -};