Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

new(app_actions)!: adjust base_syscalls option, add base_syscalls.repair #2457

Merged
merged 10 commits into from Mar 30, 2023
4 changes: 2 additions & 2 deletions cmake/modules/driver.cmake
Expand Up @@ -26,8 +26,8 @@ else()
# In case you want to test against another driver version (or branch, or commit) just pass the variable -
# ie., `cmake -DDRIVER_VERSION=dev ..`
if(NOT DRIVER_VERSION)
set(DRIVER_VERSION "e006bf38219d392cd5aeb3c1109e4bdaa9ef1617")
set(DRIVER_CHECKSUM "SHA256=185963f6d658e8963f218ec1d45056b7f6e391d05244f6499aec74807d2190fb")
set(DRIVER_VERSION "2024af2e264e1cd76ec8bc43924f1857937848a8")
set(DRIVER_CHECKSUM "SHA256=af98c4c505882a899eab38a7f7b7cc92cba634d81a110a18c42243214f9ffc5f")
endif()

# cd /path/to/build && cmake /path/to/source
Expand Down
4 changes: 2 additions & 2 deletions cmake/modules/falcosecurity-libs.cmake
Expand Up @@ -27,8 +27,8 @@ else()
# In case you want to test against another falcosecurity/libs version (or branch, or commit) just pass the variable -
# ie., `cmake -DFALCOSECURITY_LIBS_VERSION=dev ..`
if(NOT FALCOSECURITY_LIBS_VERSION)
set(FALCOSECURITY_LIBS_VERSION "e006bf38219d392cd5aeb3c1109e4bdaa9ef1617")
set(FALCOSECURITY_LIBS_CHECKSUM "SHA256=185963f6d658e8963f218ec1d45056b7f6e391d05244f6499aec74807d2190fb")
set(FALCOSECURITY_LIBS_VERSION "2024af2e264e1cd76ec8bc43924f1857937848a8")
set(FALCOSECURITY_LIBS_CHECKSUM "SHA256=af98c4c505882a899eab38a7f7b7cc92cba634d81a110a18c42243214f9ffc5f")
endif()

# cd /path/to/build && cmake /path/to/source
Expand Down
81 changes: 51 additions & 30 deletions falco.yaml
Expand Up @@ -451,66 +451,87 @@ metadata_download:
watch_freq_sec: 1


# base_syscalls ! Use with caution !
# base_syscalls ! Use with caution, read carefully !
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tried to make the description easier to follow, we can plan for some final touch up closer to Falco 0.35 release.

#
# --- [Description]
#
# With this option you are in full control of the total set of syscalls that
# With this option you are in full control of the set of syscalls that
# Falco will enable in the kernel for active tracing.

# All syscalls and events from each enabled Falco rule will automatically be activated
# even when choosing this option. This option provides full end user control to specifically
# define a static set of base syscalls that will be activated in addition to the
# All syscalls and events from each enabled Falco rule are activated
# even when choosing this option. This option allows you to define a
# set of base syscalls that will be activated in addition to the
# syscalls defined in the rules.
#
# When using this option, Falco does not add any other syscalls that may be needed for
# Falco's state engine. The union of all syscalls from the rules (including resolved macros)
# and the ones specified here compose the final set of syscalls that are traced in the kernel.
# This puts the end user in the driver seat, but if not used correctly Falco logs may be
# incomplete or wrong. This option however can be very useful to lower CPU utilization and
# allowing you to tailor Falco to specific environments according to your
# organization's threat model and security posture as well as cost budget.

# !!! When NOT using this option, Falco defaults to adding a static set of syscalls in addition
# to the rules system calls you need for Falco's state engine build-up and life-cycle management.
# You may ask yourself why do we need to activate syscalls in addition to the rules?
#
# Falco requires a set of syscalls to build up state in userspace. This is because for
# example when spawning a new process or creating a network connection more than one syscall
# is involved. Furthermore, properties of a process during its life time can be modified
# by syscalls. Falco takes care of this by activating more syscalls than the ones defined
# in the rules and by managing a smart process cache table in userspace.
# Processes are purged when a process exits.
#
# Looking back to what this option does, it activates all syscalls from the rules
# (including resolved macros) and the ones specified here.
#
# This puts the end user in the driver seat to tell Falco what it needs, but if not used correctly
# Falco logs may be incomplete or wrong or Falco won't work at all. This option however can be
# very useful to lower CPU utilization and allowing you to tailor Falco to specific environments
# according to your organization's threat model and cost budget.
#
# !!! When NOT using this option, Falco defaults to adding a static (more verbose) set of syscalls
# in addition to the rules system calls Falco needs for its state engine build-up and life-cycle management.
#
# `base_syscalls.repair` is an experimental alternative to Falco's default state engine enforcement.
# `base_syscalls.repair` is designed to be the most resourceful option to ensure Falco runs correctly
# while activating a most minimal set of additional syscalls. The recommendations listed in the suggestions
# section is effectively what `base_syscalls.repair` is doing in an automated manner. `base_syscalls.repair`
# can be used with an empty custom set.
#
# --- [Usage]
#
# List of system calls names (<syscall-name>) plus negative ("!<syscall-name>") notation supported.
#
# base_syscalls: [<syscall-name>, <syscall-name>, "!<syscall-name>"]
# base_syscalls.repair: <bool>
# base_syscalls.custom_set: [<syscall-name>, <syscall-name>, "!<syscall-name>"]
#
#
# --- [Suggestions]
#
# Here are a few recommendations that may help you to use the full power of this option:
# Here are a few recommendations that may help you.
# Setting `base_syscalls.repair: true` automates these recommendations for you.
#
# Consider to at minimum add the following syscalls regardless of the syscalls used in the rules.
#
# [clone, clone3, fork, vfork, execve, execveat, close]
#
# This is because some Falco fields you may output for an execve* system call are retrieved
# from the associated "clone", "clone3", "fork", "vfork" syscall when spawning a new process.
# The "close" system call is used to purge file descriptors from Falco's internal
# thread / process cache table and therefore should always be added when you have rules around fds
# This is because some Falco fields for an execve* system call are retrieved
# from the associated `clone`, `clone3`, `fork`, `vfork` syscall when spawning a
# new process. The `close` system call is used to purge file descriptors from Falco's
# internal thread / process cache table and should always be added when you have
# rules around file descriptors.
# (e.g. open, openat, openat2, socket, connect, accept, accept4 ... and many more)
#
# When network syscalls are used in rules we recommend to at minimum set
#
# [clone, clone3, fork, vfork, execve, execveat, close, socket, bind, getsockopt]
#
# It turns out that while you absolutely can log connect or accept* syscalls without the socket
# system call, the log however would not contain the ip tuples.
# For listen and accept* system calls you also need the bind system call.
# It turns out that while you can log `connect` or `accept*` syscalls without the
# socket system call, the log however would not contain the ip tuples.
# For `listen` and `accept*` system calls you also need the `bind` system call.
#
# Lastly, if you care about the correct uid, gid or sid, pgid of a process when that process then
# opens a file or makes a network connection or any other action, consider also
# adding the following syscalls:
# Lastly, if you care about the correct `uid`, `gid` or `sid`, `pgid` of a process when the
# running process opens a file or makes a network connection, consider adding the following syscalls:
#
# setresuid, setsid, setuid, setgid, setpgid, setresgid, setsid, capset, chdir, chroot, fchdir
#
# Only exclude syscalls, e.g. "!mprotect" if you need a fast deployment update (overriding rules),
# else rather remove unwanted or not needed syscalls from the Falco rules.
# We recommend to only exclude syscalls, e.g. "!mprotect" if you need a fast deployment update
# (overriding rules), else remove unwanted syscalls from the Falco rules.
#
# Passing `-o "log_level=debug"` to Falco's cmd args during a dry-run will print the
# final set of syscalls to STDOUT.

base_syscalls: []
base_syscalls:
repair: false
custom_set: []
88 changes: 63 additions & 25 deletions unit_tests/falco/app/actions/test_configure_interesting_sets.cpp
Expand Up @@ -23,7 +23,7 @@ limitations under the License.
#include <gtest/gtest.h>

#define ASSERT_NAMES_EQ(a, b) { \
ASSERT_EQ(_order(a).size(), _order(b).size()); \
EXPECT_EQ(_order(a).size(), _order(b).size()); \
ASSERT_EQ(_order(a), _order(b)); \
}

Expand All @@ -47,7 +47,7 @@ static std::string s_sample_ruleset = "sample-ruleset";
static std::string s_sample_source = falco_common::syscall_source;

static strset_t s_sample_filters = {
"evt.type=connect or evt.type=accept",
"evt.type=connect or evt.type=accept or evt.type=accept4 or evt.type=umount2",
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small touch up on tests to include some of the overlapping syscalls already. Noticed during testing that init_module was troublesome ... but it may be a test setup issue, as when I ran the Falco binary with debug log level init_module was correctly included everywhere.

As we refactor the ppm sc API one more time with a subsequent libs bump we should include a generic syscalls in the test mock rules as well as this critical test coverage is currently missing. CC @jasondellaluce

"evt.type in (open, ptrace, mmap, execve, read, container)",
"evt.type in (open, execve, mprotect) and not evt.type=mprotect"};

Expand Down Expand Up @@ -99,15 +99,15 @@ TEST(ConfigureInterestingSets, engine_codes_syscalls_set)
auto rules_event_set = engine->event_codes_for_ruleset(s_sample_source);
auto rules_event_names = libsinsp::events::event_set_to_names(rules_event_set);
ASSERT_NAMES_EQ(rules_event_names, strset_t({
"connect", "accept", "open", "ptrace", "mmap", "execve", "read", "container"}));
"connect", "accept", "accept4", "umount2", "open", "ptrace", "mmap", "execve", "read", "container"}));

// test if sc code names were extracted from each rule in test ruleset.
// note, this is not supposed to contain "container", as that's an event
// not mapped through the ppm_sc_code enumerative.
auto rules_sc_set = engine->sc_codes_for_ruleset(s_sample_source);
auto rules_sc_names = libsinsp::events::sc_set_to_names(rules_sc_set);
ASSERT_NAMES_EQ(rules_sc_names, strset_t({
"connect", "accept", "accept4", "open", "ptrace", "mmap", "execve", "read"}));
"connect", "accept", "accept4", "umount2", "open", "ptrace", "mmap", "execve", "read"}));
}

TEST(ConfigureInterestingSets, preconditions_postconditions)
Expand Down Expand Up @@ -158,15 +158,15 @@ TEST(ConfigureInterestingSets, engine_codes_nonsyscalls_set)
// This is a good example of information loss from ppm_event_code <-> ppm_sc_code.
auto generic_names = libsinsp::events::event_set_to_names({ppm_event_code::PPME_GENERIC_E});
auto expected_names = strset_t({
"connect", "accept", "open", "ptrace", "mmap", "execve", "read", "container", // ruleset
"connect", "accept", "accept4", "umount2", "open", "ptrace", "mmap", "execve", "read", "container", // ruleset
"procexit", "switch", "pluginevent"}); // from non-syscall event filters
expected_names.insert(generic_names.begin(), generic_names.end());
ASSERT_NAMES_EQ(rules_event_names, expected_names);

auto rules_sc_set = engine->sc_codes_for_ruleset(s_sample_source);
auto rules_sc_names = libsinsp::events::sc_set_to_names(rules_sc_set);
ASSERT_NAMES_EQ(rules_sc_names, strset_t({
"connect", "accept", "accept4", "open", "ptrace", "mmap", "execve", "read",
"connect", "accept", "accept4", "umount2", "open", "ptrace", "mmap", "execve", "read",
"syncfs", "fanotify_init", // from generic event filters
}));
}
Expand All @@ -185,11 +185,11 @@ TEST(ConfigureInterestingSets, selection_not_allevents)
// also check if a warning has been printed in stderr

// check that the final selected set is the one expected
ASSERT_NE(s.selected_sc_set.size(), 0);
ASSERT_GT(s.selected_sc_set.size(), 1);
auto selected_sc_names = libsinsp::events::sc_set_to_names(s.selected_sc_set);
auto expected_sc_names = strset_t({
// note: we expect the "read" syscall to have been erased
"connect", "accept", "open", "ptrace", "mmap", "execve", // from ruleset
"connect", "accept", "accept4", "umount2", "open", "ptrace", "mmap", "execve", // from ruleset
"clone", "clone3", "fork", "vfork", // from sinsp state set (spawned_process)
"socket", "bind", "close" // from sinsp state set (network, files)
});
Expand Down Expand Up @@ -228,11 +228,11 @@ TEST(ConfigureInterestingSets, selection_allevents)
// also check if a warning has not been printed in stderr

// check that the final selected set is the one expected
ASSERT_NE(s.selected_sc_set.size(), 0);
ASSERT_GT(s.selected_sc_set.size(), 1);
auto selected_sc_names = libsinsp::events::sc_set_to_names(s.selected_sc_set);
auto expected_sc_names = strset_t({
// note: we expect the "read" syscall to not be erased
"connect", "accept", "open", "ptrace", "mmap", "execve", "read", // from ruleset
"connect", "accept", "accept4", "umount2", "open", "ptrace", "mmap", "execve", "read", // from ruleset
"clone", "clone3", "fork", "vfork", // from sinsp state set (spawned_process)
"socket", "bind", "close" // from sinsp state set (network, files)
});
Expand All @@ -251,6 +251,7 @@ TEST(ConfigureInterestingSets, selection_generic_evts)
{
// run app action with fake engine and without the `-A` option
falco::app::state s;
s.options.all_events = false;
auto filters = s_sample_filters;
filters.insert(s_sample_generic_filters.begin(), s_sample_generic_filters.end());
s.engine = mock_engine_from_filters(filters);
Expand All @@ -259,16 +260,18 @@ TEST(ConfigureInterestingSets, selection_generic_evts)
ASSERT_EQ(result.errstr, "");

// check that the final selected set is the one expected
ASSERT_NE(s.selected_sc_set.size(), 0);
ASSERT_GT(s.selected_sc_set.size(), 1);
auto selected_sc_names = libsinsp::events::sc_set_to_names(s.selected_sc_set);
auto expected_sc_names = strset_t({
// note: we expect the "read" syscall to not be erased
"connect", "accept", "open", "ptrace", "mmap", "execve", // from ruleset
"connect", "accept", "accept4", "umount2", "open", "ptrace", "mmap", "execve", // from ruleset
"syncfs", "fanotify_init", // from ruleset (generic events)
"clone", "clone3", "fork", "vfork", // from sinsp state set (spawned_process)
"socket", "bind", "close" // from sinsp state set (network, files)
});
ASSERT_NAMES_CONTAIN(selected_sc_names, expected_sc_names);
auto unexpected_sc_names = libsinsp::events::sc_set_to_names(libsinsp::events::io_sc_set());
ASSERT_NAMES_NOCONTAIN(selected_sc_names, unexpected_sc_names);
}

// expected combinations precedence:
Expand All @@ -285,7 +288,8 @@ TEST(ConfigureInterestingSets, selection_custom_base_set)
auto default_base_set = libsinsp::events::sinsp_state_sc_set();

// non-empty custom base set (both positive and negative)
s.config->m_base_syscalls = {"syncfs", "!accept"};
s.config->m_base_syscalls_repair = false;
s.config->m_base_syscalls_custom_set = {"syncfs", "!accept"};
auto result = falco::app::actions::configure_interesting_sets(s);
ASSERT_TRUE(result.success);
ASSERT_EQ(result.errstr, "");
Expand All @@ -297,56 +301,90 @@ TEST(ConfigureInterestingSets, selection_custom_base_set)
// note: `accept` is not included even though it is matched by the rules,
// which means that the custom negation base set has precedence over the
// final selection set as a whole
"connect", "open", "ptrace", "mmap", "execve", "read", "syncfs"
// todo(jasondellaluce): add "accept4" once names_to_sc_set is polished on the libs side
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When disabling accept, Falco also disables accept4. That's still expected because I still have to clean up names_to_sc_set on libsinsp.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, yes!

"connect", "umount2", "open", "ptrace", "mmap", "execve", "read", "syncfs", "sched_process_exit"
});
ASSERT_NAMES_CONTAIN(selected_sc_names, expected_sc_names);
ASSERT_NAMES_EQ(selected_sc_names, expected_sc_names);

// non-empty custom base set (both positive and negative with collision)
s.config->m_base_syscalls = {"syncfs", "accept", "!accept"};
s.config->m_base_syscalls_repair = false;
s.config->m_base_syscalls_custom_set = {"syncfs", "accept", "!accept"};
result = falco::app::actions::configure_interesting_sets(s);
ASSERT_TRUE(result.success);
ASSERT_EQ(result.errstr, "");
selected_sc_names = libsinsp::events::sc_set_to_names(s.selected_sc_set);
// note: in case of collision, negation has priority, so the expected
// names are the same as the case above
ASSERT_NAMES_CONTAIN(selected_sc_names, expected_sc_names);
ASSERT_NAMES_EQ(selected_sc_names, expected_sc_names);

// non-empty custom base set (only positive)
s.config->m_base_syscalls = {"syncfs"};
s.config->m_base_syscalls_custom_set = {"syncfs"};
result = falco::app::actions::configure_interesting_sets(s);
ASSERT_TRUE(result.success);
ASSERT_EQ(result.errstr, "");
selected_sc_names = libsinsp::events::sc_set_to_names(s.selected_sc_set);
expected_sc_names = strset_t({
// note: accept is not negated anymore
"connect", "accept", "open", "ptrace", "mmap", "execve", "read", "syncfs"
"connect", "accept", "accept4", "umount2", "open", "ptrace", "mmap", "execve", "read", "syncfs", "sched_process_exit"
});
ASSERT_NAMES_CONTAIN(selected_sc_names, expected_sc_names);
ASSERT_NAMES_EQ(selected_sc_names, expected_sc_names);

// non-empty custom base set (only negative)
s.config->m_base_syscalls = {"!accept"};
s.config->m_base_syscalls_custom_set = {"!accept"};
result = falco::app::actions::configure_interesting_sets(s);
ASSERT_TRUE(result.success);
ASSERT_EQ(result.errstr, "");
selected_sc_names = libsinsp::events::sc_set_to_names(s.selected_sc_set);
expected_sc_names = unordered_set_union(
libsinsp::events::sc_set_to_names(default_base_set),
strset_t({ "connect", "open", "ptrace", "mmap", "execve", "read"}));
strset_t({ "connect", "umount2", "open", "ptrace", "mmap", "execve", "read"}));
expected_sc_names.erase("accept");
// todo(jasondellaluce): add "accept4" once names_to_sc_set is polished on the libs side
expected_sc_names.erase("accept4");
ASSERT_NAMES_CONTAIN(selected_sc_names, expected_sc_names);
ASSERT_NAMES_EQ(selected_sc_names, expected_sc_names);

// non-empty custom base set (positive, without -A)
s.options.all_events = false;
s.config->m_base_syscalls = {"read"};
s.config->m_base_syscalls_custom_set = {"read"};
result = falco::app::actions::configure_interesting_sets(s);
ASSERT_TRUE(result.success);
ASSERT_EQ(result.errstr, "");
selected_sc_names = libsinsp::events::sc_set_to_names(s.selected_sc_set);
expected_sc_names = strset_t({
// note: read is both part of the custom base set and the rules set,
// but we expect the unset -A option to take precedence
"connect", "accept", "open", "ptrace", "mmap", "execve",
"connect", "accept", "accept4", "umount2", "open", "ptrace", "mmap", "execve", "sched_process_exit"
});
ASSERT_NAMES_EQ(selected_sc_names, expected_sc_names);
auto unexpected_sc_names = libsinsp::events::sc_set_to_names(libsinsp::events::io_sc_set());
ASSERT_NAMES_NOCONTAIN(selected_sc_names, unexpected_sc_names);
}

TEST(ConfigureInterestingSets, selection_custom_base_set_repair)
{
// run app action with fake engine and without the `-A` option
falco::app::state s;
s.options.all_events = false;
s.engine = mock_engine_from_filters(s_sample_filters);

// simulate empty custom set but repair option set.
// note: here we use file syscalls (e.g. open, openat) and have a custom
// positive set, so we expect syscalls such as "close" to be selected as
// repaired. Also, given that we use some network syscalls, we expect "bind"
// to be selected event if we negate it, because repairment should have
// take precedence.
s.config->m_base_syscalls_custom_set = {"openat", "!bind"};
s.config->m_base_syscalls_repair = true;
auto result = falco::app::actions::configure_interesting_sets(s);
ASSERT_TRUE(result.success);
ASSERT_EQ(result.errstr, "");
auto selected_sc_names = libsinsp::events::sc_set_to_names(s.selected_sc_set);
auto expected_sc_names = strset_t({
// note: expecting syscalls from mock rules and `sinsp_repair_state_sc_set` enforced syscalls
"connect", "accept", "accept4", "umount2", "open", "ptrace", "mmap", "execve", "sched_process_exit", \
"bind", "socket", "clone3", "close", "setuid"
});
ASSERT_NAMES_CONTAIN(selected_sc_names, expected_sc_names);
auto unexpected_sc_names = libsinsp::events::sc_set_to_names(libsinsp::events::io_sc_set());
ASSERT_NAMES_NOCONTAIN(selected_sc_names, unexpected_sc_names);
}