diff --git a/.gitignore b/.gitignore index f392843bf7e..5b202663d24 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,9 @@ /build* +*~ +test/falco_test.pyc +test/falco_tests.yaml +test/traces-negative +test/traces-positive userspace/falco/lua/re.lua userspace/falco/lua/lpeg.so diff --git a/.travis.yml b/.travis.yml index d49533715c4..fe37c22f52b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,6 +9,14 @@ install: - sudo apt-get --force-yes install g++-4.8 - sudo apt-get install rpm linux-headers-$(uname -r) - git clone https://github.com/draios/sysdig.git ../sysdig + - sudo apt-get install -y python-pip libvirt-dev jq + - cd .. + - curl -Lo avocado-36.0-tar.gz https://github.com/avocado-framework/avocado/archive/36.0lts.tar.gz + - tar -zxvf avocado-36.0-tar.gz + - cd avocado-36.0lts + - sudo pip install -r requirements-travis.txt + - sudo python setup.py install + - cd ../falco before_script: - export KERNELDIR=/lib/modules/$(ls /lib/modules | sort | head -1)/build script: @@ -28,7 +36,7 @@ script: - make VERBOSE=1 - make package - cd .. - - sudo test/falco_trace_regression.sh build/userspace/falco/falco + - sudo test/run_regression_tests.sh notifications: webhooks: urls: diff --git a/rules/falco_rules.yaml b/rules/falco_rules.yaml index b0cdb0ab5e7..1d451af991e 100644 --- a/rules/falco_rules.yaml +++ b/rules/falco_rules.yaml @@ -38,12 +38,16 @@ - macro: modify condition: rename or remove -- macro: spawn_process - condition: syscall.type = execve and evt.dir=< +- macro: spawned_process + condition: evt.type = execve and evt.dir=< # File categories - macro: terminal_file_fd condition: fd.name=/dev/ptmx or fd.directory=/dev/pts + +# This really should be testing that the directory begins with these +# prefixes but sysdig's filter doesn't have a "starts with" operator +# (yet). - macro: bin_dir condition: fd.directory in (/bin, /sbin, /usr/bin, /usr/sbin) @@ -52,6 +56,8 @@ - macro: bin_dir_rename condition: evt.arg[1] contains /bin/ or evt.arg[1] contains /sbin/ or evt.arg[1] contains /usr/bin/ or evt.arg[1] contains /usr/sbin/ +# This really should be testing that the directory begins with /etc, +# but sysdig's filter doesn't have a "starts with" operator (yet). - macro: etc_dir condition: fd.directory contains /etc @@ -74,25 +80,31 @@ tac, link, chroot, vdir, chown, touch, ls, dd, uname, true, pwd, date, chgrp, chmod, mktemp, cat, mknod, sync, ln, false, rm, mv, cp, echo, readlink, sleep, stty, mkdir, df, dir, rmdir, touch) -- macro: adduser_binaries - condition: proc.name in (adduser, deluser, addgroup, delgroup) + +# dpkg -L login | grep bin | xargs ls -ld | grep -v '^d' | awk '{print $9}' | xargs -L 1 basename | tr "\\n" "," - macro: login_binaries - condition: proc.name in (bin, login, su, sbin, nologin, bin, faillog, lastlog, newgrp, sg) + condition: proc.name in (login, systemd-logind, su, nologin, faillog, lastlog, newgrp, sg) -# dpkg -L passwd | grep bin | xargs -L 1 basename | tr "\\n" "," +# dpkg -L passwd | grep bin | xargs ls -ld | grep -v '^d' | awk '{print $9}' | xargs -L 1 basename | tr "\\n" "," - macro: passwd_binaries condition: > - proc.name in (sbin, shadowconfig, sbin, grpck, pwunconv, grpconv, pwck, + proc.name in (shadowconfig, grpck, pwunconv, grpconv, pwck, groupmod, vipw, pwconv, useradd, newusers, cppw, chpasswd, usermod, - groupadd, groupdel, grpunconv, chgpasswd, userdel, bin, chage, chsh, + groupadd, groupdel, grpunconv, chgpasswd, userdel, chage, chsh, gpasswd, chfn, expiry, passwd, vigr, cpgr) -# repoquery -l shadow-utils | grep bin | xargs -L 1 basename | tr "\\n" "," +# repoquery -l shadow-utils | grep bin | xargs ls -ld | grep -v '^d' | awk '{print $9}' | xargs -L 1 basename | tr "\\n" "," - macro: shadowutils_binaries condition: > - proc.name in (chage, gpasswd, lastlog, newgrp, sg, adduser, chpasswd, - groupadd, groupdel, groupmems, groupmod, grpck, grpconv, grpunconv, - newusers, pwck, pwconv, pwunconv, useradd, userdel, usermod, vigr, vipw) + proc.name in (chage, gpasswd, lastlog, newgrp, sg, adduser, deluser, chpasswd, + groupadd, groupdel, addgroup, delgroup, groupmems, groupmod, grpck, grpconv, grpunconv, + newusers, pwck, pwconv, pwunconv, useradd, userdel, usermod, vigr, vipw, unix_chkpwd) + +- macro: sysdigcloud_binaries + condition: proc.name in (setup-backend, dragent) + +- macro: sysdigcloud_binaries_parent + condition: proc.pname in (setup-backend, dragent) - macro: docker_binaries condition: proc.name in (docker, exe) @@ -103,25 +115,33 @@ - macro: db_server_binaries condition: proc.name in (mysqld) +- macro: db_server_binaries_parent + condition: proc.pname in (mysqld) + - macro: server_binaries - condition: http_server_binaries or db_server_binaries or docker_binaries or proc.name in (sshd) + condition: (http_server_binaries or db_server_binaries or docker_binaries or proc.name in (sshd)) +# The truncated dpkg-preconfigu is intentional, process names are +# truncated at the sysdig level. - macro: package_mgmt_binaries - condition: proc.name in (dpkg, rpm) + condition: proc.name in (dpkg, dpkg-preconfigu, rpm, yum) # A canonical set of processes that run other programs with different # privileges or as a different user. - macro: userexec_binaries condition: proc.name in (sudo, su) +- macro: user_mgmt_binaries + condition: (login_binaries or passwd_binaries or shadowutils_binaries) + - macro: system_binaries - condition: coreutils_binaries or adduser_binaries or login_binaries or passwd_binaries or shadowutils_binaries + condition: (coreutils_binaries or user_mgmt_binaries) - macro: mail_binaries - condition: proc.name in (sendmail, postfix, procmail) + condition: proc.name in (sendmail, sendmail-msp, postfix, procmail) - macro: sensitive_files - condition: fd.name contains /etc/shadow or fd.name = /etc/sudoers or fd.directory = /etc/sudoers.d or fd.directory = /etc/pam.d or fd.name = /etc/pam.conf + condition: (fd.name contains /etc/shadow or fd.name = /etc/sudoers or fd.directory = /etc/sudoers.d or fd.directory = /etc/pam.d or fd.name = /etc/pam.conf) # Indicates that the process is new. Currently detected using time # since process was started, using a threshold of 5 seconds. @@ -130,7 +150,7 @@ # Network - macro: inbound - condition: (syscall.type=listen and evt.dir=>) or (syscall.type=accept and evt.dir=<) + condition: ((syscall.type=listen and evt.dir=>) or (syscall.type=accept and evt.dir=<)) # Currently sendto is an ignored syscall, otherwise this could also check for (syscall.type=sendto and evt.dir=>) - macro: outbound @@ -141,7 +161,7 @@ # Ssh - macro: ssh_error_message - condition: evt.arg.data contains "Invalid user" or evt.arg.data contains "preauth" + condition: (evt.arg.data contains "Invalid user" or evt.arg.data contains "preauth" or evt.arg.data contains "Failed password") # System - macro: modules @@ -149,9 +169,9 @@ - macro: container condition: container.id != host - macro: interactive - condition: (proc.aname=sshd and proc.name != sshd) or proc.name=systemd-logind + condition: ((proc.aname=sshd and proc.name != sshd) or proc.name=systemd-logind) - macro: syslog - condition: fd.name = /dev/log + condition: fd.name in (/dev/log, /run/systemd/journal/syslog) - macro: cron condition: proc.name in (cron, crond) - macro: parent_cron @@ -169,32 +189,32 @@ - rule: write_binary_dir desc: an attempt to write to any file below a set of binary directories - condition: evt.dir = > and open_write and bin_dir + condition: evt.dir = < and open_write and not package_mgmt_binaries and bin_dir output: "File below a known binary directory opened for writing (user=%user.name command=%proc.cmdline file=%fd.name)" priority: WARNING - rule: write_etc desc: an attempt to write to any file below /etc - condition: evt.dir = > and open_write and etc_dir + condition: evt.dir = < and open_write and not shadowutils_binaries and not sysdigcloud_binaries_parent and not package_mgmt_binaries and etc_dir output: "File below /etc opened for writing (user=%user.name command=%proc.cmdline file=%fd.name)" priority: WARNING - rule: read_sensitive_file_untrusted desc: an attempt to read any sensitive file (e.g. files containing user/password/authentication information). Exceptions are made for known trusted programs. - condition: open_read and not server_binaries and not userexec_binaries and not proc.name in (iptables, ps, systemd-logind, lsb_release, check-new-relea, dumpe2fs, accounts-daemon, bash) and not cron and sensitive_files + condition: open_read and not user_mgmt_binaries and not userexec_binaries and not proc.name in (iptables, ps, lsb_release, check-new-relea, dumpe2fs, accounts-daemon, bash, sshd) and not cron and sensitive_files output: "Sensitive file opened for reading by non-trusted program (user=%user.name command=%proc.cmdline file=%fd.name)" priority: WARNING - rule: read_sensitive_file_trusted_after_startup desc: an attempt to read any sensitive file (e.g. files containing user/password/authentication information) by a trusted program after startup. Trusted programs might read these files at startup to load initial state, but not afterwards. - condition: open_read and server_binaries and not proc_is_new and sensitive_files + condition: open_read and server_binaries and not proc_is_new and sensitive_files and proc.name!="sshd" output: "Sensitive file opened for reading by trusted program after startup (user=%user.name command=%proc.cmdline file=%fd.name)" priority: WARNING -- rule: db_program_spawn_process - desc: a database-server related program spawning a new process after startup. This shouldn\'t occur and is a follow on from some SQL injection attacks. - condition: db_server_binaries and not proc_is_new and spawn_process - output: "Database-related program spawned new process after startup (user=%user.name command=%proc.cmdline)" +- rule: db_program_spawned_process + desc: a database-server related program spawned a new process other than itself. This shouldn\'t occur and is a follow on from some SQL injection attacks. + condition: db_server_binaries_parent and not db_server_binaries and spawned_process + output: "Database-related program spawned process other than itself (user=%user.name program=%proc.cmdline parent=%proc.pname)" priority: WARNING - rule: modify_binary_dirs @@ -218,11 +238,12 @@ # output: "Loaded .so from unexpected dir (%user.name %proc.name %evt.dir %evt.type %evt.args %fd.name)" # priority: WARNING -- rule: syscall_returns_eaccess - desc: any system call that returns EACCESS. This is not always a strong indication of a problem, hence the INFO priority. - condition: evt.res = EACCESS - output: "System call returned EACCESS (user=%user.name command=%proc.cmdline syscall=%evt.type args=%evt.args)" - priority: INFO +# Temporarily disabling this rule as it's tripping over https://github.com/draios/sysdig/issues/598 +# - rule: syscall_returns_eaccess +# desc: any system call that returns EACCESS. This is not always a strong indication of a problem, hence the INFO priority. +# condition: evt.res = EACCESS +# output: "System call returned EACCESS (user=%user.name command=%proc.cmdline syscall=%evt.type args=%evt.args)" +# priority: INFO - rule: change_thread_namespace desc: an attempt to change a program/thread\'s namespace (commonly done as a part of creating a container) by calling setns. @@ -232,7 +253,7 @@ - rule: run_shell_untrusted desc: an attempt to spawn a shell by a non-shell program. Exceptions are made for trusted binaries. - condition: proc.name = bash and evt.dir=< and evt.type=execve and proc.pname exists and not parent_cron and not proc.pname in (bash, sshd, sudo, docker, su, tmux, screen, emacs, systemd, flock, fs-bash, nginx, monit, supervisord) + condition: not container and proc.name = bash and spawned_process and proc.pname exists and not parent_cron and not proc.pname in (bash, sshd, sudo, docker, su, tmux, screen, emacs, systemd, login, flock, fbash, nginx, monit, supervisord, dragent) output: "Shell spawned by untrusted binary (user=%user.name shell=%proc.name parent=%proc.pname cmdline=%proc.cmdline)" priority: WARNING @@ -243,13 +264,13 @@ - rule: system_user_interactive desc: an attempt to run interactive commands by a system (i.e. non-login) user - condition: spawn_process and system_users and interactive + condition: spawned_process and system_users and interactive output: "System user ran an interactive command (user=%user.name command=%proc.cmdline)" priority: WARNING - rule: run_shell_in_container - desc: an attempt to spawn a shell by a non-shell program in a container. Container entrypoints are excluded. - condition: container and proc.name = bash and evt.dir=< and evt.type=execve and proc.pname exists and not proc.pname in (bash, docker) + desc: a shell was spawned by a non-shell program in a container. Container entrypoints are excluded. + condition: container and proc.name = bash and spawned_process and proc.pname exists and not proc.pname in (bash, docker) output: "Shell spawned in a container other than entrypoint (user=%user.name container_id=%container.id container_name=%container.name shell=%proc.name parent=%proc.pname cmdline=%proc.cmdline)" priority: WARNING @@ -260,22 +281,26 @@ output: "Known system binary sent/received network traffic (user=%user.name command=%proc.cmdline connection=%fd.name)" priority: WARNING -- rule: ssh_error_syslog - desc: any ssh errors (failed logins, disconnects, ...) sent to syslog - condition: syslog and ssh_error_message and evt.dir = < - output: "sshd sent error message to syslog (error=%evt.buffer)" - priority: WARNING +# With the current restriction on system calls handled by falco +# (e.g. excluding read/write/sendto/recvfrom/etc, this rule won't +# trigger). +# - rule: ssh_error_syslog +# desc: any ssh errors (failed logins, disconnects, ...) sent to syslog +# condition: syslog and ssh_error_message and evt.dir = < +# output: "sshd sent error message to syslog (error=%evt.buffer)" +# priority: WARNING +# sshd, sendmail-msp, sendmail attempt to setuid to root even when running as non-root. Excluding here to avoid meaningless FPs - rule: non_sudo_setuid desc: an attempt to change users by calling setuid. sudo/su are excluded. user "root" is also excluded, as setuid calls typically involve dropping privileges. - condition: evt.type=setuid and evt.dir=> and not user.name=root and not userexec_binaries + condition: evt.type=setuid and evt.dir=> and not user.name=root and not userexec_binaries and not proc.name in (sshd, sendmail-msp, sendmail) output: "Unexpected setuid call by non-sudo, non-root program (user=%user.name command=%proc.cmdline uid=%evt.arg.uid)" priority: WARNING - rule: user_mgmt_binaries desc: activity by any programs that can manage users, passwords, or permissions. sudo and su are excluded. Activity in containers is also excluded--some containers create custom users on top of a base linux distribution at startup. - condition: spawn_process and not proc.name in (su, sudo) and not container and (adduser_binaries or login_binaries or passwd_binaries or shadowutils_binaries) - output: "User management binary command run outside of container (user=%user.name command=%proc.cmdline)" + condition: spawned_process and not proc.name in (su, sudo) and not container and user_mgmt_binaries and not parent_cron and not proc.pname in (systemd, run-parts) + output: "User management binary command run outside of container (user=%user.name command=%proc.cmdline parent=%proc.pname)" priority: WARNING # (we may need to add additional checks against false positives, see: https://bugs.launchpad.net/ubuntu/+source/rkhunter/+bug/86153) @@ -285,17 +310,17 @@ output: "File created below /dev by untrusted program (user=%user.name command=%proc.cmdline file=%fd.name)" priority: WARNING -# fs-bash is a restricted version of bash suitable for use in curl | sh installers. +# fbash is a small shell script that runs bash, and is suitable for use in curl | fbash installers. - rule: installer_bash_starts_network_server - desc: an attempt by any program that is a child of fs-bash to start listening for network connections - condition: evt.type=listen and proc.aname=fs-bash - output: "Unexpected listen call by a child process of fs-bash (command=%proc.cmdline)" + desc: an attempt by any program that is a child of fbash to start listening for network connections + condition: evt.type=listen and proc.aname=fbash + output: "Unexpected listen call by a child process of fbash (command=%proc.cmdline)" priority: WARNING - rule: installer_bash_starts_session - desc: an attempt by any program that is a child of fs-bash to start a new session (process group) - condition: evt.type=setsid and proc.aname=fs-bash - output: "Unexpected setsid call by a child process of fs-bash (command=%proc.cmdline)" + desc: an attempt by any program that is a child of fbash to start a new session (process group) + condition: evt.type=setsid and proc.aname=fbash + output: "Unexpected setsid call by a child process of fbash (command=%proc.cmdline)" priority: WARNING ########################### diff --git a/test/falco_test.py b/test/falco_test.py new file mode 100644 index 00000000000..72875c1c063 --- /dev/null +++ b/test/falco_test.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python + +import os +import re + +from avocado import Test +from avocado.utils import process +from avocado.utils import linux_modules + +class FalcoTest(Test): + + def setUp(self): + """ + Load the sysdig kernel module if not already loaded. + """ + self.falcodir = self.params.get('falcodir', '/', default=os.path.join(self.basedir, '../build')) + + self.should_detect = self.params.get('detect', '*') + self.trace_file = self.params.get('trace_file', '*') + + # Doing this in 2 steps instead of simply using + # module_is_loaded to avoid logging lsmod output to the log. + lsmod_output = process.system_output("lsmod", verbose=False) + + if linux_modules.parse_lsmod_for_module(lsmod_output, 'sysdig_probe') == {}: + self.log.debug("Loading sysdig kernel module") + process.run('sudo insmod {}/driver/sysdig-probe.ko'.format(self.falcodir)) + + self.str_variant = self.trace_file + + def test(self): + self.log.info("Trace file %s", self.trace_file) + + # Run the provided trace file though falco + cmd = '{}/userspace/falco/falco -r {}/../rules/falco_rules.yaml -c {}/../falco.yaml -e {}'.format( + self.falcodir, self.falcodir, self.falcodir, self.trace_file) + + self.falco_proc = process.SubProcess(cmd) + + res = self.falco_proc.run(timeout=60, sig=9) + + if res.exit_status != 0: + self.error("Falco command \"{}\" exited with non-zero return value {}".format( + cmd, res.exit_status)) + + # Get the number of events detected. + res = re.search('Events detected: (\d+)', res.stdout) + if res is None: + self.fail("Could not find a line 'Events detected: ' in falco output") + + events_detected = int(res.group(1)) + + if not self.should_detect and events_detected > 0: + self.fail("Detected {} events when should have detected none".format(events_detected)) + + if self.should_detect and events_detected == 0: + self.fail("Detected {} events when should have detected > 0".format(events_detected)) + + pass + + +if __name__ == "__main__": + main() diff --git a/test/falco_trace_regression.sh b/test/falco_trace_regression.sh deleted file mode 100755 index a4b3498c7b2..00000000000 --- a/test/falco_trace_regression.sh +++ /dev/null @@ -1,30 +0,0 @@ -#!/bin/bash -set -eu - -SCRIPT=$(readlink -f $0) -BASEDIR=$(dirname $SCRIPT) - -FALCO=$1 -BUILDDIR=$(dirname $FALCO) - -# Load the built kernel module by hand -insmod $BUILDDIR/../../driver/sysdig-probe.ko - -# For now, simply ensure that falco can run without errors. -FALCO_CMDLINE="$FALCO -c $BASEDIR/../falco.yaml -r $BASEDIR/../rules/falco_rules.yaml" -echo "Running falco: $FALCO_CMDLINE" -$FALCO_CMDLINE > $BASEDIR/falco.log 2>&1 & -FALCO_PID=$! -echo "Falco started, pid $FALCO_PID" -sleep 10 -if kill -0 $FALCO_PID > /dev/null 2>&1; then - echo "Falco ran successfully" - kill $FALCO_PID - ret=0 -else - echo "Falco did not start successfully. Full program output:" - cat $BASEDIR/falco.log - ret=1 -fi - -exit $ret diff --git a/test/run_regression_tests.sh b/test/run_regression_tests.sh new file mode 100755 index 00000000000..9f6b2a28863 --- /dev/null +++ b/test/run_regression_tests.sh @@ -0,0 +1,62 @@ +#!/bin/bash + +SCRIPT=$(readlink -f $0) +SCRIPTDIR=$(dirname $SCRIPT) +MULT_FILE=$SCRIPTDIR/falco_tests.yaml + +function download_trace_files() { + for TRACE in traces-positive traces-negative ; do + curl -so $SCRIPTDIR/$TRACE.zip https://s3.amazonaws.com/download.draios.com/falco-tests/$TRACE.zip && + unzip -d $SCRIPTDIR $SCRIPTDIR/$TRACE.zip && + rm -rf $SCRIPTDIR/$TRACE.zip + done +} + +function prepare_multiplex_file() { + echo "trace_files: !mux" > $MULT_FILE + + for trace in $SCRIPTDIR/traces-positive/*.scap ; do + [ -e "$trace" ] || continue + NAME=`basename $trace .scap` + cat << EOF >> $MULT_FILE + $NAME: + detect: True + trace_file: $trace +EOF + done + + for trace in $SCRIPTDIR/traces-negative/*.scap ; do + [ -e "$trace" ] || continue + NAME=`basename $trace .scap` + cat << EOF >> $MULT_FILE + $NAME: + detect: False + trace_file: $trace +EOF + done + + echo "Contents of $MULT_FILE:" + cat $MULT_FILE +} + +function run_tests() { + CMD="avocado run --multiplex $MULT_FILE --job-results-dir $SCRIPTDIR/job-results -- $SCRIPTDIR/falco_test.py" + echo "Running: $CMD" + $CMD + TEST_RC=$? +} + + +function print_test_failure_details() { + echo "Showing full job logs for any tests that failed:" + jq '.tests[] | select(.status != "PASS") | .logfile' $SCRIPTDIR/job-results/latest/results.json | xargs cat +} + +download_trace_files +prepare_multiplex_file +run_tests +if [ $TEST_RC -ne 0 ]; then + print_test_failure_details +fi + +exit $TEST_RC diff --git a/test/utils/run_sysdig.sh b/test/utils/run_sysdig.sh new file mode 100644 index 00000000000..9a1a611fde4 --- /dev/null +++ b/test/utils/run_sysdig.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +# Run sysdig excluding all events that aren't used by falco and also +# excluding other high-volume events that aren't essential. This +# results in smaller trace files. + +# The remaining arguments are taken from the command line. + +exec sudo sysdig not evt.type in '(mprotect,brk,mq_timedreceive,mq_receive,mq_timedsend,mq_send,getrusage,procinfo,rt_sigprocmask,rt_sigaction,ioctl,clock_getres,clock_gettime,clock_nanosleep,clock_settime,close,epoll_create,epoll_create1,epoll_ctl,epoll_pwait,epoll_wait,eventfd,fcntl,fcntl64,fstat,fstat64,fstatat64,fstatfs,fstatfs64,futex,getitimer,gettimeofday,ioprio_get,ioprio_set,llseek,lseek,lstat,lstat64,mmap,mmap2,munmap,nanosleep,poll,ppoll,pread,pread64,preadv,procinfo,pselect6,pwrite,pwrite64,pwritev,read,readv,recv,recvfrom,recvmmsg,recvmsg,sched_yield,select,send,sendfile,sendfile64,sendmmsg,sendmsg,sendto,setitimer,settimeofday,shutdown,splice,stat,stat64,statfs,statfs64,switch,tee,timer_create,timer_delete,timerfd_create,timerfd_gettime,timerfd_settime,timer_getoverrun,timer_gettime,timer_settime,wait4,write,writev) and user.name!=ec2-user' $@ diff --git a/userspace/falco/falco.cpp b/userspace/falco/falco.cpp index 6b92058fadb..01b3019c664 100644 --- a/userspace/falco/falco.cpp +++ b/userspace/falco/falco.cpp @@ -28,6 +28,14 @@ extern "C" { #include "utils.h" #include +bool g_terminate = false; +// +// Helper functions +// +static void signal_callback(int signal) +{ + g_terminate = true; +} // // Program help @@ -67,6 +75,7 @@ static void display_fatal_err(const string &msg, bool daemon) string lua_on_event = "on_event"; string lua_add_output = "add_output"; +string lua_print_stats = "print_stats"; // Splitting into key=value or key.subkey=value will be handled by configuration class. std::list cmdline_options; @@ -90,7 +99,11 @@ void do_inspect(sinsp* inspector, res = inspector->next(&ev); - if(res == SCAP_TIMEOUT) + if (g_terminate) + { + break; + } + else if(res == SCAP_TIMEOUT) { continue; } @@ -199,6 +212,26 @@ void add_output(lua_State *ls, output_config oc) } +// Print statistics on the the rules that triggered +void print_stats(lua_State *ls) +{ + lua_getglobal(ls, lua_print_stats.c_str()); + + if(lua_isfunction(ls, -1)) + { + if(lua_pcall(ls, 0, 0, 0) != 0) + { + const char* lerr = lua_tostring(ls, -1); + string err = "Error invoking function print_stats: " + string(lerr); + throw sinsp_exception(err); + } + } + else + { + throw sinsp_exception("No function " + lua_print_stats + " found in lua rule loader module"); + } + +} // // ARGUMENT PARSING AND PROGRAM SETUP @@ -398,6 +431,20 @@ int falco_init(int argc, char **argv) add_output(ls, *it); } + if(signal(SIGINT, signal_callback) == SIG_ERR) + { + fprintf(stderr, "An error occurred while setting SIGINT signal handler.\n"); + result = EXIT_FAILURE; + goto exit; + } + + if(signal(SIGTERM, signal_callback) == SIG_ERR) + { + fprintf(stderr, "An error occurred while setting SIGTERM signal handler.\n"); + result = EXIT_FAILURE; + goto exit; + } + if (scap_filename.size()) { inspector->open(scap_filename); @@ -406,7 +453,7 @@ int falco_init(int argc, char **argv) { try { - inspector->open(); + inspector->open(200); } catch(sinsp_exception e) { @@ -478,6 +525,8 @@ int falco_init(int argc, char **argv) ls); inspector->close(); + + print_stats(ls); } catch(sinsp_exception& e) { diff --git a/userspace/falco/lua/output.lua b/userspace/falco/lua/output.lua index 78573b947c4..0bef1712ab3 100644 --- a/userspace/falco/lua/output.lua +++ b/userspace/falco/lua/output.lua @@ -2,6 +2,8 @@ local mod = {} levels = {"Emergency", "Alert", "Critical", "Error", "Warning", "Notice", "Informational", "Debug"} +mod.levels = levels + local outputs = {} function mod.stdout(evt, level, format) diff --git a/userspace/falco/lua/rule_loader.lua b/userspace/falco/lua/rule_loader.lua index f5cc888264d..7a9774a7666 100644 --- a/userspace/falco/lua/rule_loader.lua +++ b/userspace/falco/lua/rule_loader.lua @@ -230,12 +230,51 @@ function describe_rule(name) end end +local rule_output_counts = {total=0, by_level={}, by_name={}} + +for idx, level in ipairs(output.levels) do + rule_output_counts[level] = 0 +end + 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 - output.event(evt_, state.rules_by_idx[rule_id].level, state.rules_by_idx[rule_id].output) + rule_output_counts.total = rule_output_counts.total + 1 + local rule = state.rules_by_idx[rule_id] + + if rule_output_counts.by_level[rule.level] == nil then + rule_output_counts.by_level[rule.level] = 1 + else + rule_output_counts.by_level[rule.level] = rule_output_counts.by_level[rule.level] + 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 + + output.event(evt_, rule.level, rule.output) end +function print_stats() + print("Events detected: "..rule_output_counts.total) + print("Rule counts by severity:") + for idx, level in ipairs(output.levels) do + -- To keep the output concise, we only print 0 counts for error, warning, and info levels + if rule_output_counts[level] > 0 or level == "Error" or level == "Warning" or level == "Informational" then + print (" "..level..": "..rule_output_counts[level]) + end + end + + print("Triggered rules by rule name:") + for name, count in pairs(rule_output_counts.by_name) do + print (" "..name..": "..count) + end +end + + +