mirror of
https://github.com/falcosecurity/falco.git
synced 2025-10-14 07:09:21 +00:00
* Use correct copyright years. Also include the start year. * Improve copyright notices. Use the proper start year instead of just 2018. Add the right owner Draios dba Sysdig. Add copyright notices to some files that were missing them.
432 lines
12 KiB
Lua
432 lines
12 KiB
Lua
-- Copyright (C) 2016-2018 Draios Inc dba Sysdig.
|
|
--
|
|
-- This file is part of falco.
|
|
--
|
|
-- 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.
|
|
|
|
local parser = require("parser")
|
|
local compiler = {}
|
|
|
|
compiler.verbose = false
|
|
compiler.all_events = false
|
|
compiler.trim = parser.trim
|
|
|
|
function compiler.set_verbose(verbose)
|
|
compiler.verbose = verbose
|
|
parser.set_verbose(verbose)
|
|
end
|
|
|
|
function compiler.set_all_events(all_events)
|
|
compiler.all_events = all_events
|
|
end
|
|
|
|
function map(f, arr)
|
|
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
|
|
end
|
|
|
|
--[[
|
|
|
|
Given a map of macro definitions, traverse AST and replace macro references
|
|
with their definitions.
|
|
|
|
The AST is changed in-place.
|
|
|
|
The return value is a boolean which is true if any macro was
|
|
substitued. This allows a caller to re-traverse until no more macros are
|
|
found, a simple strategy for recursive resoltuions (e.g. when a macro
|
|
definition uses another macro).
|
|
|
|
--]]
|
|
|
|
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
|
|
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
|
|
error("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 changed
|
|
end
|
|
return expand_macros(ast.value, defs, changed)
|
|
|
|
elseif ast.type == "BinaryBoolOp" then
|
|
|
|
if (ast.left.type == "Macro") then
|
|
if (defs[ast.left.value] == nil) then
|
|
error("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
|
|
error("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 changed_left = expand_macros(ast.left, defs, false)
|
|
local changed_right = expand_macros(ast.right, defs, false)
|
|
return 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
|
|
error("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 changed
|
|
end
|
|
|
|
function get_macros(ast, set)
|
|
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 == "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
|
|
|
|
return set
|
|
end
|
|
if ast.type == "UnaryBoolOp" then
|
|
return get_macros(ast.argument, set)
|
|
end
|
|
return set
|
|
end
|
|
|
|
function 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
|
|
|
|
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
|
|
|
|
if node.operator == "in" 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)
|
|
end
|
|
|
|
-- Examine the ast and find the event types/syscalls for which the
|
|
-- rule should run. All evt.type references are added as event types
|
|
-- up until the first "!=" binary operator or unary not operator. If
|
|
-- no event type checks are found afterward in the rule, the rule is
|
|
-- considered optimized and is associated with the event type(s).
|
|
--
|
|
-- Otherwise, the rule is associated with a 'catchall' category and is
|
|
-- run for all event types/syscalls. (Also, a warning is printed).
|
|
--
|
|
|
|
function get_evttypes_syscalls(name, ast, source, warn_evttypes)
|
|
|
|
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 == "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
|
|
|
|
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
|
|
|
|
-- 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
|
|
|
|
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)
|
|
|
|
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
|
|
|
|
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
|
|
|
|
table.sort(evtnames_only)
|
|
|
|
if compiler.verbose then
|
|
io.stderr:write("Event types/Syscalls for rule "..name..": "..table.concat(evtnames_only, ",").."\n")
|
|
end
|
|
|
|
return evttypes, syscallnums
|
|
end
|
|
|
|
function get_filters(ast)
|
|
|
|
local filters = {}
|
|
|
|
function cb(node)
|
|
if node.type == "FieldName" then
|
|
filters[node.value] = 1
|
|
end
|
|
end
|
|
|
|
parser.traverse_ast(ast.filter.value, {FieldName=1} , cb)
|
|
|
|
return filters
|
|
end
|
|
|
|
function compiler.expand_lists_in(source, list_defs)
|
|
|
|
for name, def in pairs(list_defs) do
|
|
local begin_name_pat = "^("..name..")([%s(),=])"
|
|
local mid_name_pat = "([%s(),=])("..name..")([%s(),=])"
|
|
local end_name_pat = "([%s(),=])("..name..")$"
|
|
|
|
source, subcount1 = string.gsub(source, begin_name_pat, table.concat(def.items, ", ").."%2")
|
|
source, subcount2 = string.gsub(source, mid_name_pat, "%1"..table.concat(def.items, ", ").."%3")
|
|
source, subcount3 = string.gsub(source, end_name_pat, "%1"..table.concat(def.items, ", "))
|
|
|
|
if (subcount1 + subcount2 + subcount3) > 0 then
|
|
def.used = true
|
|
end
|
|
end
|
|
|
|
return source
|
|
end
|
|
|
|
function compiler.compile_macro(line, macro_defs, list_defs)
|
|
|
|
line = compiler.expand_lists_in(line, list_defs)
|
|
|
|
local ast, error_msg = parser.parse_filter(line)
|
|
|
|
if (error_msg) then
|
|
msg = "Compilation error when compiling \""..line.."\": ".. error_msg
|
|
error(msg)
|
|
end
|
|
|
|
-- Traverse the ast looking for events/syscalls in the ignored
|
|
-- syscalls table. If any are found, return an error.
|
|
if not compiler.all_events then
|
|
check_for_ignored_syscalls_events(ast, 'macro', line)
|
|
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)
|
|
|
|
if (ast.type == "Rule") then
|
|
-- Line is a filter, so expand macro references
|
|
repeat
|
|
expanded = expand_macros(ast_copy, macro_defs, false)
|
|
until expanded == false
|
|
|
|
else
|
|
error("Unexpected top-level AST type: "..ast.type)
|
|
end
|
|
|
|
return ast
|
|
end
|
|
|
|
--[[
|
|
Parses a single filter, then expands macros using passed-in table of definitions. Returns resulting AST.
|
|
--]]
|
|
function compiler.compile_filter(name, source, macro_defs, list_defs, warn_evttypes)
|
|
|
|
source = compiler.expand_lists_in(source, list_defs)
|
|
|
|
local ast, error_msg = parser.parse_filter(source)
|
|
|
|
if (error_msg) then
|
|
msg = "Compilation error when compiling \""..source.."\": "..error_msg
|
|
error(msg)
|
|
end
|
|
|
|
-- Traverse the ast looking for events/syscalls in the ignored
|
|
-- syscalls table. If any are found, return an error.
|
|
if not compiler.all_events then
|
|
check_for_ignored_syscalls_events(ast, 'rule', source)
|
|
end
|
|
|
|
if (ast.type == "Rule") then
|
|
-- Line is a filter, so expand macro references
|
|
repeat
|
|
expanded = expand_macros(ast, macro_defs, false)
|
|
until expanded == false
|
|
|
|
else
|
|
error("Unexpected top-level AST type: "..ast.type)
|
|
end
|
|
|
|
evttypes, syscallnums = get_evttypes_syscalls(name, ast, source, warn_evttypes)
|
|
|
|
filters = get_filters(ast)
|
|
|
|
return ast, evttypes, syscallnums, filters
|
|
end
|
|
|
|
|
|
return compiler
|