Move falco engine to its own library.

Move the c++ and lua code implementing falco engine/falco common to its
own directory userspace/engine. It's compiled as a static library
libfalco_engine.a, and has its own CMakeLists.txt so it can be included
by other projects.

The engine's CMakeLists.txt has a add_subdirectory for the falco rules
directory, so including the engine also builds the rules.

The variables you need to set to use the engine's CMakeLists.txt are:

- CMAKE_INSTALL_PREFIX: the root directory below which everything is
  installed.
- FALCO_ETC_DIR: where to install the rules file.
- FALCO_SHARE_DIR: where to install lua code, relative to the
- install/package root.
- LUAJIT_INCLUDE: where to find header files for lua.
- FALCO_SINSP_LIBRARY: the library containing sinsp code. It will be
- considered a dependency of the engine.
- LPEG_LIB/LYAML_LIB/LIBYAML_LIB: locations for third-party libraries.
- FALCO_COMPONENT: if set, will be included as a part of any install()
  commands.

Instead of specifying /usr/share/falco in config_falco_*.h.in, use
CMAKE_INSTALL_PREFIX and FALCO_SHARE_DIR.

The lua code for the engine has also moved, so the two lua source
directories (userspace/engine/lua and userspace/falco/lua) need to be
available separately via falco_common, so make it an argument to
falco_common::init.

As a part of making it easy to include in another project, also clean up
LPEG build/defs. Modify build-lpeg to add a PREFIX argument to allow for
object files/libraries being in an alternate location, and when building
lpeg, put object files in a build/ subdirectory.
This commit is contained in:
Mark Stemm
2016-07-20 15:31:34 -07:00
parent 917d66e9e8
commit f547dc97ab
23 changed files with 94 additions and 31 deletions

View File

@@ -0,0 +1,31 @@
include_directories("${PROJECT_SOURCE_DIR}/../sysdig/userspace/libsinsp/third-party/jsoncpp")
include_directories("${PROJECT_SOURCE_DIR}/../sysdig/userspace/libscap")
include_directories("${PROJECT_SOURCE_DIR}/../sysdig/userspace/libsinsp")
include_directories("${PROJECT_BINARY_DIR}/userspace/engine")
include_directories("${LUAJIT_INCLUDE}")
add_library(falco_engine STATIC rules.cpp falco_common.cpp falco_engine.cpp)
target_include_directories(falco_engine PUBLIC
"${LUAJIT_INCLUDE}")
target_link_libraries(falco_engine
"${FALCO_SINSP_LIBRARY}"
"${LPEG_LIB}"
"${LYAML_LIB}"
"${LIBYAML_LIB}")
configure_file(config_falco_engine.h.in config_falco_engine.h)
if(DEFINED FALCO_COMPONENT)
install(DIRECTORY lua
DESTINATION "${FALCO_SHARE_DIR}"
COMPONENT "${FALCO_COMPONENT}"
FILES_MATCHING PATTERN *.lua)
else()
install(DIRECTORY lua
DESTINATION "${FALCO_SHARE_DIR}"
FILES_MATCHING PATTERN *.lua)
endif()
add_subdirectory("${PROJECT_SOURCE_DIR}/../falco/rules" "${PROJECT_BINARY_DIR}/rules")

View File

@@ -0,0 +1,4 @@
#pragma once
#define FALCO_ENGINE_LUA_DIR "${CMAKE_INSTALL_PREFIX}/${FALCO_SHARE_DIR}/lua/"
#define FALCO_ENGINE_SOURCE_LUA_DIR "${PROJECT_SOURCE_DIR}/../falco/userspace/engine/lua/"

View File

@@ -0,0 +1,90 @@
#include <fstream>
#include "config_falco_engine.h"
#include "falco_common.h"
falco_common::falco_common()
{
m_ls = lua_open();
luaL_openlibs(m_ls);
}
falco_common::~falco_common()
{
if(m_ls)
{
lua_close(m_ls);
}
}
void falco_common::set_inspector(sinsp *inspector)
{
m_inspector = inspector;
}
void falco_common::init(const char *lua_main_filename, const char *source_dir)
{
ifstream is;
string lua_dir = FALCO_ENGINE_LUA_DIR;
string lua_main_path = lua_dir + lua_main_filename;
is.open(lua_main_path);
if (!is.is_open())
{
lua_dir = source_dir;
lua_main_path = lua_dir + lua_main_filename;
is.open(lua_main_path);
if (!is.is_open())
{
throw falco_exception("Could not find Falco Lua entrypoint (tried " +
string(FALCO_ENGINE_LUA_DIR) + lua_main_filename + ", " +
string(source_dir) + lua_main_filename + ")");
}
}
// Initialize Lua interpreter
add_lua_path(lua_dir);
// Load the main program, which defines all the available functions.
string scriptstr((istreambuf_iterator<char>(is)),
istreambuf_iterator<char>());
if(luaL_loadstring(m_ls, scriptstr.c_str()) || lua_pcall(m_ls, 0, 0, 0))
{
throw falco_exception("Failed to load script " +
lua_main_path + ": " + lua_tostring(m_ls, -1));
}
}
void falco_common::add_lua_path(string &path)
{
string cpath = string(path);
path += "?.lua";
cpath += "?.so";
lua_getglobal(m_ls, "package");
lua_getfield(m_ls, -1, "path");
string cur_path = lua_tostring(m_ls, -1 );
cur_path += ';';
lua_pop(m_ls, 1);
cur_path.append(path.c_str());
lua_pushstring(m_ls, cur_path.c_str());
lua_setfield(m_ls, -2, "path");
lua_getfield(m_ls, -1, "cpath");
string cur_cpath = lua_tostring(m_ls, -1 );
cur_cpath += ';';
lua_pop(m_ls, 1);
cur_cpath.append(cpath.c_str());
lua_pushstring(m_ls, cur_cpath.c_str());
lua_setfield(m_ls, -2, "cpath");
lua_pop(m_ls, 1);
}

View File

@@ -0,0 +1,69 @@
#pragma once
#include <string>
#include <exception>
extern "C" {
#include "lua.h"
#include "lualib.h"
#include "lauxlib.h"
}
#include <sinsp.h>
//
// Most falco_* classes can throw exceptions. Unless directly related
// to low-level failures like inability to open file, etc, they will
// be of this type.
//
struct falco_exception : std::exception
{
falco_exception()
{
}
virtual ~falco_exception() throw()
{
}
falco_exception(std::string error_str)
{
m_error_str = error_str;
}
char const* what() const throw()
{
return m_error_str.c_str();
}
std::string m_error_str;
};
//
// This is the base class of falco_engine/falco_output. It is
// responsible for managing a lua state and associated inspector and
// loading a single "main" lua file into that state.
//
class falco_common
{
public:
falco_common();
virtual ~falco_common();
void init(const char *lua_main_filename, const char *source_dir);
void set_inspector(sinsp *inspector);
protected:
lua_State *m_ls;
sinsp *m_inspector;
private:
void add_lua_path(std::string &path);
};

View File

@@ -0,0 +1,145 @@
#include <string>
#include <fstream>
#include "falco_engine.h"
#include "config_falco_engine.h"
extern "C" {
#include "lpeg.h"
#include "lyaml.h"
}
#include "utils.h"
string lua_on_event = "on_event";
string lua_print_stats = "print_stats";
using namespace std;
falco_engine::falco_engine()
: m_rules(NULL)
{
luaopen_lpeg(m_ls);
luaopen_yaml(m_ls);
falco_common::init(m_lua_main_filename.c_str(), FALCO_ENGINE_SOURCE_LUA_DIR);
falco_rules::init(m_ls);
}
falco_engine::~falco_engine()
{
if (m_rules)
{
delete m_rules;
}
}
void falco_engine::load_rules(const string &rules_content, bool verbose, bool all_events)
{
// The engine must have been given an inspector by now.
if(! m_inspector)
{
throw falco_exception("No inspector provided");
}
if(!m_rules)
{
m_rules = new falco_rules(m_inspector, this, m_ls);
}
m_rules->load_rules(rules_content, verbose, all_events);
}
void falco_engine::load_rules_file(const string &rules_filename, bool verbose, bool all_events)
{
ifstream is;
is.open(rules_filename);
if (!is.is_open())
{
throw falco_exception("Could not open rules filename " +
rules_filename + " " +
"for reading");
}
string rules_content((istreambuf_iterator<char>(is)),
istreambuf_iterator<char>());
load_rules(rules_content, verbose, all_events);
}
void falco_engine::enable_rule(string &pattern, bool enabled)
{
m_evttype_filter.enable(pattern, enabled);
}
falco_engine::rule_result *falco_engine::process_event(sinsp_evt *ev)
{
if(!m_evttype_filter.run(ev))
{
return NULL;
}
struct rule_result *res = new rule_result();
lua_getglobal(m_ls, lua_on_event.c_str());
if(lua_isfunction(m_ls, -1))
{
lua_pushlightuserdata(m_ls, ev);
lua_pushnumber(m_ls, ev->get_check_id());
if(lua_pcall(m_ls, 2, 3, 0) != 0)
{
const char* lerr = lua_tostring(m_ls, -1);
string err = "Error invoking function output: " + string(lerr);
throw falco_exception(err);
}
res->evt = ev;
const char *p = lua_tostring(m_ls, -3);
res->rule = p;
res->priority = lua_tostring(m_ls, -2);
res->format = lua_tostring(m_ls, -1);
}
else
{
throw falco_exception("No function " + lua_on_event + " found in lua compiler module");
}
return res;
}
void falco_engine::describe_rule(string *rule)
{
return m_rules->describe_rule(rule);
}
// Print statistics on the the rules that triggered
void falco_engine::print_stats()
{
lua_getglobal(m_ls, lua_print_stats.c_str());
if(lua_isfunction(m_ls, -1))
{
if(lua_pcall(m_ls, 0, 0, 0) != 0)
{
const char* lerr = lua_tostring(m_ls, -1);
string err = "Error invoking function print_stats: " + string(lerr);
throw falco_exception(err);
}
}
else
{
throw falco_exception("No function " + lua_print_stats + " found in lua rule loader module");
}
}
void falco_engine::add_evttype_filter(string &rule,
list<uint32_t> &evttypes,
sinsp_filter* filter)
{
m_evttype_filter.add(rule, evttypes, filter);
}

View File

@@ -0,0 +1,75 @@
#pragma once
#include <string>
#include "sinsp.h"
#include "filter.h"
#include "rules.h"
#include "falco_common.h"
//
// This class acts as the primary interface between a program and the
// falco rules engine. Falco outputs (writing to files/syslog/etc) are
// handled in a separate class falco_outputs.
//
class falco_engine : public falco_common
{
public:
falco_engine();
virtual ~falco_engine();
//
// Load rules either directly or from a filename.
//
void load_rules_file(const std::string &rules_filename, bool verbose, bool all_events);
void load_rules(const std::string &rules_content, bool verbose, bool all_events);
//
// Enable/Disable any rules matching the provided pattern (regex).
//
void enable_rule(std::string &pattern, bool enabled);
struct rule_result {
sinsp_evt *evt;
std::string rule;
std::string priority;
std::string format;
};
//
// Given an event, check it against the set of rules in the
// engine and if a matching rule is found, return details on
// the rule that matched. If no rule matched, returns NULL.
//
// the reutrned rule_result is allocated and must be delete()d.
rule_result *process_event(sinsp_evt *ev);
//
// Print details on the given rule. If rule is NULL, print
// details on all rules.
//
void describe_rule(std::string *rule);
//
// Print statistics on how many events matched each rule.
//
void print_stats();
//
// Add a filter, which is related to the specified list of
// event types, to the engine.
//
void add_evttype_filter(std::string &rule,
list<uint32_t> &evttypes,
sinsp_filter* filter);
private:
falco_rules *m_rules;
sinsp_evttype_filter m_evttype_filter;
std::string m_lua_main_filename = "rule_loader.lua";
};

6
userspace/engine/lpeg.h Normal file
View File

@@ -0,0 +1,6 @@
#pragma once
#include "lua.h"
int luaopen_lpeg (lua_State *L);

View File

@@ -0,0 +1,6 @@
Installation
------------
The sysdig grammar uses the `lpeg` parser. For now install it using luarocks:
`luarocks install lpeg`.

View File

@@ -0,0 +1,327 @@
local parser = require("parser")
local compiler = {}
compiler.verbose = false
compiler.all_events = false
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 expand_macros(ast, defs, changed)
function copy(obj)
if type(obj) ~= 'table' then return obj end
local res = {}
for k, v in pairs(obj) do res[copy(k)] = copy(v) end
return res
end
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
ast.value = copy(defs[ast.value.value])
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
ast.left = copy(defs[ast.left.value])
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
ast.right = copy(defs[ast.right.value])
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
ast.argument = copy(defs[ast.argument.value])
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" 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 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. (Also, a warning is printed).
--
function get_evttypes(name, ast, source)
local evttypes = {}
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" then
for i, v in ipairs(node.right.elements) do
if v.type == "BareString" then
evtnames[v.value] = 1
for id in string.gmatch(events[v.value], "%S+") do
evttypes[id] = 1
end
end
end
else
if node.right.type == "BareString" then
evtnames[node.right.value] = 1
for id in string.gmatch(events[node.right.value], "%S+") do
evttypes[id] = 1
end
end
end
end
end
end
parser.traverse_ast(ast.filter.value, {BinaryRelOp=1, UnaryBoolOp=1} , cb)
if not found_event 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")
evttypes = {}
evtnames = {}
end
if found_event_after_not 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")
evttypes = {}
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 for rule "..name..": "..table.concat(evtnames_only, ",").."\n")
end
return evttypes
end
function compiler.compile_macro(line, list_defs)
for name, items in pairs(list_defs) do
line = string.gsub(line, name, table.concat(items, ", "))
end
local ast, error_msg = parser.parse_filter(line)
if (error_msg) then
print ("Compilation error: ", error_msg)
error(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
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)
for name, items in pairs(list_defs) do
source = string.gsub(source, name, table.concat(items, ", "))
end
local ast, error_msg = parser.parse_filter(source)
if (error_msg) then
print ("Compilation error: ", error_msg)
error(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 = get_evttypes(name, ast, source)
return ast, evttypes
end
return compiler

View File

@@ -0,0 +1,70 @@
#!/bin/bash
function error_exit_good
{
echo "Error: '$1' did not compiler" 1>&2
exit 1
}
function error_exit_bad
{
echo "Error: incorrect filter '$1' compiler ok" 1>&2
exit 1
}
function good
{
lua5.1 test.lua "$1" 2> /dev/null || error_exit_good "$1"
}
function bad
{
lua5.1 test.lua "$1" 2> /dev/null && error_exit_bad "$1"
}
# Filters
good " a"
good "a and b"
good "#a and b; a and b"
good "#a and b; # ; ; a and b"
good "(a)"
good "(a and b)"
good "(a.a exists and b)"
good "(a.a exists) and (b)"
good "a.a exists and b"
good "a.a=1 or b.b=2 and c"
good "not (a)"
good "not (not (a))"
good "not (a.b=1)"
good "not (a.a exists)"
good "not a"
good "a.b = 1 and not a"
good "not not a"
good "(not not a)"
good "not a.b=1"
good "not a.a exists"
good "notz and a and b"
good "a.b = bla"
good "a.b = 'bla'"
good "a.b = not"
good "a.b contains bla"
good "a.b icontains 'bla'"
good "a.g in (1, 'a', b)"
good "a.g in ( 1 ,, , b)"
good "evt.dir=> and fd.name=*.log"
good "evt.dir=> and fd.name=/var/log/httpd.log"
good "a.g in (1, 'a', b.c)"
good "a.b = a.a"
good "evt.arg[0] contains /bin"
bad "evt.arg[a] contains /bin"
bad "evt.arg[] contains /bin"
bad "a.b = b = 1"
bad "(a.b = 1"
echo
echo "All tests passed."
exit 0

View File

@@ -0,0 +1,343 @@
--[[
Falco grammar and parser.
Much of the scaffolding and helpers was derived from Andre Murbach Maidl's Lua parser (https://github.com/andremm/lua-parser).
Parses regular filters following the existing sysdig filter syntax (*), extended to support "macro" terms, which are just identifiers.
(*) There is currently one known difference with the syntax implemented in libsinsp: In libsinsp, field names cannot start with 'a', 'o', or 'n'. With this parser they can.
--]]
local parser = {}
parser.verbose = false
function parser.set_verbose(verbose)
parser.verbose = verbose
end
local lpeg = require "lpeg"
lpeg.locale(lpeg)
local P, S, V = lpeg.P, lpeg.S, lpeg.V
local C, Carg, Cb, Cc = lpeg.C, lpeg.Carg, lpeg.Cb, lpeg.Cc
local Cf, Cg, Cmt, Cp, Ct = lpeg.Cf, lpeg.Cg, lpeg.Cmt, lpeg.Cp, lpeg.Ct
local alpha, digit, alnum = lpeg.alpha, lpeg.digit, lpeg.alnum
local xdigit = lpeg.xdigit
local space = lpeg.space
-- error message auxiliary functions
-- 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)
end
-- gets the farthest failure position
local function getffp (s, 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
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
end
-- reports a syntactic error
local function report_error ()
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
end
local function updateffp (name)
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)
end
local function symb (str)
return token (P(str), str)
end
local function kw (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
end
--http://lua-users.org/wiki/StringTrim
function trim(s)
if (type(s) ~= "string") then return s end
return (s:gsub("^%s*(.-)%s*$", "%1"))
end
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) return { type = tag, value = trim(tok)} end
end
local function unaryboolop (op, e)
return { type = "UnaryBoolOp", operator = op, argument = e }
end
local function unaryrelop (e, op)
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
end
local function bool (pat, sep)
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
end
local function fix_str (str)
str = string.gsub(str, "\\a", "\a")
str = string.gsub(str, "\\b", "\b")
str = string.gsub(str, "\\f", "\f")
str = string.gsub(str, "\\n", "\n")
str = string.gsub(str, "\\r", "\r")
str = string.gsub(str, "\\t", "\t")
str = string.gsub(str, "\\v", "\v")
str = string.gsub(str, "\\\n", "\n")
str = string.gsub(str, "\\\r", "\n")
str = string.gsub(str, "\\'", "'")
str = string.gsub(str, '\\"', '"')
str = string.gsub(str, '\\\\', '\\')
return str
end
-- grammar
local function filter(e)
return {type = "Filter", value=e}
end
local function rule(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"InOp", 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".";
FieldName = V"Identifier" * (P"." + V"Identifier")^1 * (P"[" * V"Int" * 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");
Int = digit^1;
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"'") / function (s) return fix_str(s) end;
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("startswith") / "startswith";
InOp = kw("in") / "in";
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
end
function print_ast(ast, level)
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
end
parser.print_ast = print_ast
-- Traverse the provided ast and call the provided callback function
-- for any nodes of the specified type. The callback function should
-- have the signature:
-- cb(ast_node, ctx)
-- ctx is optional.
function traverse_ast(ast, node_types, cb, ctx)
local t = ast.type
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
end
parser.traverse_ast = traverse_ast
return parser

View File

@@ -0,0 +1,288 @@
--[[
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 compiler = require "compiler"
local yaml = require"lyaml"
--[[
Traverse AST, adding the passed-in 'index' to each node that contains a relational expression
--]]
local function mark_relational_nodes(ast, index)
local t = ast.type
if t == "BinaryBoolOp" then
mark_relational_nodes(ast.left, index)
mark_relational_nodes(ast.right, index)
elseif t == "UnaryBoolOp" then
mark_relational_nodes(ast.argument, index)
elseif t == "BinaryRelOp" then
ast.index = index
elseif t == "UnaryRelOp" then
ast.index = index
else
error ("Unexpected type in mark_relational_nodes: "..t)
end
end
function map(f, arr)
local res = {}
for i,v in ipairs(arr) do
res[i] = f(v)
end
return res
end
--[[
Take a filter AST and set it up in the libsinsp runtime, using the filter API.
--]]
local function install_filter(node, parent_bool_op)
local t = node.type
if t == "BinaryBoolOp" then
-- "nesting" (the runtime equivalent of placing parens in syntax) is
-- never necessary when we have identical successive operators. so we
-- avoid it as a runtime performance optimization.
if (not(node.operator == parent_bool_op)) then
filter.nest() -- io.write("(")
end
install_filter(node.left, node.operator)
filter.bool_op(node.operator) -- io.write(" "..node.operator.." ")
install_filter(node.right, node.operator)
if (not (node.operator == parent_bool_op)) then
filter.unnest() -- io.write(")")
end
elseif t == "UnaryBoolOp" then
filter.nest() --io.write("(")
filter.bool_op(node.operator) -- io.write(" "..node.operator.." ")
install_filter(node.argument)
filter.unnest() -- io.write(")")
elseif t == "BinaryRelOp" then
if (node.operator == "in") then
elements = map(function (el) return el.value end, node.right.elements)
filter.rel_expr(node.left.value, node.operator, elements, node.index)
else
filter.rel_expr(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.rel_expr(node.argument.value, node.operator, node.index)
--io.write(node.argument.value.." "..node.operator)
else
error ("Unexpected type in install_filter: "..t)
end
end
function set_output(output_format, state)
if(output_ast.type == "OutputFormat") then
local format
else
error ("Unexpected type in set_output: ".. output_ast.type)
end
end
-- 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={}, n_rules=0, rules_by_idx={}}
function load_rules(rules_content, rules_mgr, verbose, all_events)
compiler.set_verbose(verbose)
compiler.set_all_events(all_events)
local rules = yaml.load(rules_content)
if rules == nil then
-- An empty rules file is acceptable
return
end
for i,v in ipairs(rules) do -- iterate over yaml list
if (not (type(v) == "table")) then
error ("Unexpected element of type " ..type(v)..". Each element should be a yaml associative array.")
end
if (v['macro']) then
local ast = compiler.compile_macro(v['condition'], state.lists)
state.macros[v['macro']] = ast.filter.value
elseif (v['list']) then
-- 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]) do
items[#items+1] = exp_item
end
end
end
state.lists[v['list']] = items
else -- rule
if (v['rule'] == nil) then
error ("Missing name in rule")
end
for i, field in ipairs({'condition', 'output', 'desc', 'priority'}) do
if (v[field] == nil) then
error ("Missing "..field.." in rule with name "..v['rule'])
end
end
state.rules_by_name[v['rule']] = v
local filter_ast, evttypes = compiler.compile_filter(v['rule'], v['condition'],
state.macros, state.lists)
if (filter_ast.type == "Rule") then
state.n_rules = state.n_rules + 1
state.rules_by_idx[state.n_rules] = v
-- Store the index of this formatter in each relational expression that
-- this rule contains.
-- This index will eventually be stamped in events passing this rule, and
-- we'll use it later to determine which output to display when we get an
-- event.
mark_relational_nodes(filter_ast.filter.value, state.n_rules)
install_filter(filter_ast.filter.value)
-- Pass the filter and event types back up
falco_rules.add_filter(rules_mgr, v['rule'], evttypes)
-- Rule ASTs are merged together into one big AST, with "OR" between each
-- rule.
if (state.filter_ast == nil) then
state.filter_ast = filter_ast.filter.value
else
state.filter_ast = { type = "BinaryBoolOp", operator = "or", left = state.filter_ast, right = filter_ast.filter.value }
end
else
error ("Unexpected type in load_rule: "..filter_ast.type)
end
end
end
io.flush()
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(evt_, 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
return rule.rule, rule.priority, rule.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("Triggered rules by rule name:")
for name, count in pairs(rule_output_counts.by_name) do
print (" "..name..": "..count)
end
end

6
userspace/engine/lyaml.h Normal file
View File

@@ -0,0 +1,6 @@
#pragma once
#include "lua.h"
int luaopen_yaml (lua_State *L);

181
userspace/engine/rules.cpp Normal file
View File

@@ -0,0 +1,181 @@
#include "rules.h"
#include "logger.h"
extern "C" {
#include "lua.h"
#include "lualib.h"
#include "lauxlib.h"
}
#include "falco_engine.h"
const static struct luaL_reg ll_falco_rules [] =
{
{"add_filter", &falco_rules::add_filter},
{NULL,NULL}
};
falco_rules::falco_rules(sinsp* inspector, falco_engine *engine, lua_State *ls)
: m_inspector(inspector), m_engine(engine), m_ls(ls)
{
m_lua_parser = new lua_parser(inspector, m_ls);
}
void falco_rules::init(lua_State *ls)
{
luaL_openlib(ls, "falco_rules", ll_falco_rules, 0);
}
int falco_rules::add_filter(lua_State *ls)
{
if (! lua_islightuserdata(ls, -3) ||
! lua_isstring(ls, -2) ||
! lua_istable(ls, -1))
{
throw falco_exception("Invalid arguments passed to add_filter()\n");
}
falco_rules *rules = (falco_rules *) lua_topointer(ls, -3);
const char *rulec = lua_tostring(ls, -2);
list<uint32_t> evttypes;
lua_pushnil(ls); /* first key */
while (lua_next(ls, -2) != 0) {
// key is at index -2, value is at index
// -1. We want the keys.
evttypes.push_back(luaL_checknumber(ls, -2));
// Remove value, keep key for next iteration
lua_pop(ls, 1);
}
std::string rule = rulec;
rules->add_filter(rule, evttypes);
return 0;
}
void falco_rules::add_filter(string &rule, list<uint32_t> &evttypes)
{
// While the current rule was being parsed, a sinsp_filter
// object was being populated by lua_parser. Grab that filter
// and pass it to the engine.
sinsp_filter *filter = m_lua_parser->get_filter(true);
m_engine->add_evttype_filter(rule, evttypes, filter);
}
void falco_rules::load_rules(const string &rules_content, bool verbose, bool all_events)
{
lua_getglobal(m_ls, m_lua_load_rules.c_str());
if(lua_isfunction(m_ls, -1))
{
// Create a table containing all events, so they can
// be mapped to event ids.
sinsp_evttables* einfo = m_inspector->get_event_info_tables();
const struct ppm_event_info* etable = einfo->m_event_info;
const struct ppm_syscall_desc* stable = einfo->m_syscall_info_table;
map<string,string> events_by_name;
for(uint32_t j = 0; j < PPM_EVENT_MAX; j++)
{
auto it = events_by_name.find(etable[j].name);
if (it == events_by_name.end()) {
events_by_name[etable[j].name] = to_string(j);
} else {
string cur = it->second;
cur += " ";
cur += to_string(j);
events_by_name[etable[j].name] = cur;
}
}
lua_newtable(m_ls);
for( auto kv : events_by_name)
{
lua_pushstring(m_ls, kv.first.c_str());
lua_pushstring(m_ls, kv.second.c_str());
lua_settable(m_ls, -3);
}
lua_setglobal(m_ls, m_lua_events.c_str());
// Create a table containing the syscalls/events that
// are ignored by the kernel module. load_rules will
// return an error if any rule references one of these
// syscalls/events.
lua_newtable(m_ls);
for(uint32_t j = 0; j < PPM_EVENT_MAX; j++)
{
if(etable[j].flags & EF_DROP_FALCO)
{
lua_pushstring(m_ls, etable[j].name);
lua_pushnumber(m_ls, 1);
lua_settable(m_ls, -3);
}
}
lua_setglobal(m_ls, m_lua_ignored_events.c_str());
lua_newtable(m_ls);
for(uint32_t j = 0; j < PPM_SC_MAX; j++)
{
if(stable[j].flags & EF_DROP_FALCO)
{
lua_pushstring(m_ls, stable[j].name);
lua_pushnumber(m_ls, 1);
lua_settable(m_ls, -3);
}
}
lua_setglobal(m_ls, m_lua_ignored_syscalls.c_str());
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));
if(lua_pcall(m_ls, 4, 0, 0) != 0)
{
const char* lerr = lua_tostring(m_ls, -1);
string err = "Error loading rules:" + string(lerr);
throw falco_exception(err);
}
} 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()
{
delete m_lua_parser;
}

35
userspace/engine/rules.h Normal file
View File

@@ -0,0 +1,35 @@
#pragma once
#include <list>
#include "sinsp.h"
#include "lua_parser.h"
class falco_engine;
class falco_rules
{
public:
falco_rules(sinsp* inspector, falco_engine *engine, lua_State *ls);
~falco_rules();
void load_rules(const string &rules_content, bool verbose, bool all_events);
void describe_rule(string *rule);
static void init(lua_State *ls);
static int add_filter(lua_State *ls);
private:
void add_filter(string &rule, list<uint32_t> &evttypes);
lua_parser* m_lua_parser;
sinsp* m_inspector;
falco_engine *m_engine;
lua_State* m_ls;
string m_lua_load_rules = "load_rules";
string m_lua_ignored_syscalls = "ignored_syscalls";
string m_lua_ignored_events = "ignored_events";
string m_lua_events = "events";
string m_lua_describe_rule = "describe_rule";
};