From 10f821c5fe85b1586309606093ad26127d3999ae Mon Sep 17 00:00:00 2001 From: Mark Stemm Date: Wed, 1 Jun 2016 16:01:37 -0700 Subject: [PATCH] Add additional tests related to pipe installers. Add additional tests related to using pipe installers within a fbash session: - Modify write_etc to only trigger if *not* in a fbash session. There's a new rule write_etc_installer which has the same conditions when in a fbash session, logging at INFO severity. - A new rule write_rpm_database warns if any non package management program tries to write below /var/lib/rpm. - Add a new warning if any program below a fbash session tries to open an outbound network connection on ports other than http(s) and dns. - Add INFO level messages when programs in a fbash session try to run package management binaries (rpm,yum,etc) or service management (systemctl,chkconfig,etc) binaries. In order to test these new INFO level rules, make up a third class of trace files traces-info.zip containing trace files that should result in info-level messages. To differentiate warning and info level detection, add an attribute to the multiplex file "detect_level", which is "Warning" for the files in traces-positive and "Info" for the files in traces-info. Modify falco_test.py to look specifically for a non-zero count for the given detect_level. Doing this exposed a bug in the way the level-specific counts were being recorded--they were keeping counts by level name, not number. Fix that. --- rules/falco_rules.yaml | 54 +++++++++++++++++++++++++---- test/falco_test.py | 25 ++++++++++--- test/run_regression_tests.sh | 15 +++++++- userspace/falco/lua/rule_loader.lua | 15 ++++---- 4 files changed, 89 insertions(+), 20 deletions(-) diff --git a/rules/falco_rules.yaml b/rules/falco_rules.yaml index 8ba8452c17e..9b5c6097186 100644 --- a/rules/falco_rules.yaml +++ b/rules/falco_rules.yaml @@ -124,7 +124,7 @@ # The truncated dpkg-preconfigu is intentional, process names are # truncated at the sysdig level. - macro: package_mgmt_binaries - condition: proc.name in (dpkg, dpkg-preconfigu, rpm, yum) + condition: proc.name in (dpkg, dpkg-preconfigu, rpm, rpmkey, yum) # A canonical set of processes that run other programs with different # privileges or as a different user. @@ -141,7 +141,7 @@ 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 in (/etc/sudoers.d, /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. @@ -194,11 +194,18 @@ priority: WARNING - rule: write_etc - desc: an attempt to write to any file below /etc - condition: evt.dir = < and open_write and not shadowutils_binaries and not sysdigcloud_binaries_parent and not package_mgmt_binaries and etc_dir + desc: an attempt to write to any file below /etc, not in a pipe installer session + condition: evt.dir = < and open_write and not shadowutils_binaries and not sysdigcloud_binaries_parent and not package_mgmt_binaries and etc_dir and not proc.sname=fbash output: "File below /etc opened for writing (user=%user.name command=%proc.cmdline file=%fd.name)" priority: WARNING +# Within a fbash session, the severity is lowered to INFO +- rule: write_etc_installer + desc: an attempt to write to any file below /etc, in a pipe installer session + condition: evt.dir = < and open_write and not shadowutils_binaries and not sysdigcloud_binaries_parent and not package_mgmt_binaries and etc_dir and proc.sname=fbash + output: "File below /etc opened for writing (user=%user.name command=%proc.cmdline file=%fd.name) within pipe installer session" + priority: INFO + - 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 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 @@ -211,6 +218,13 @@ output: "Sensitive file opened for reading by trusted program after startup (user=%user.name command=%proc.cmdline file=%fd.name)" priority: WARNING +# Only let rpm-related programs write to the rpm database +- rule: write_rpm_database + desc: an attempt to write to the rpm database by any non-rpm related program + condition: open_write and not proc.name in (rpm,rpmkey,yum) and fd.directory=/var/lib/rpm + output: "Rpm database opened for writing by a non-rpm program (command=%proc.cmdline file=%fd.name)" + priority: WARNING + - 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 @@ -312,17 +326,45 @@ # 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 in a session led by fbash to start listening for network connections + desc: an attempt by a program in a pipe installer session to start listening for network connections condition: evt.type=listen and proc.sname=fbash output: "Unexpected listen call by a process in a fbash session (command=%proc.cmdline)" priority: WARNING - rule: installer_bash_starts_session - desc: an attempt by any program that is in a session led by fbash to start a new session + desc: an attempt by a program in a pipe installer session to start a new session condition: evt.type=setsid and proc.sname=fbash output: "Unexpected setsid call by a process in fbash session (command=%proc.cmdline)" priority: WARNING +- rule: installer_bash_non_https_connection + desc: an attempt by a program in a pipe installer session to make an outgoing connection on a non-http(s) port + condition: outbound and not fd.sport in (80, 443, 53) and proc.sname=fbash + output: "Outbound connection on non-http(s) port by a process in a fbash session (command=%proc.cmdline connection=%fd.name)" + priority: WARNING + +# It'd be nice if we could warn when processes in a fbash session try +# to download from any nonstandard location? This is probably blocked +# on https://github.com/draios/falco/issues/88 though. + +# Notice when processes try to run chkconfig/systemctl.... to install a service. +# Note: this is not a WARNING, as you'd expect some service management +# as a part of doing the installation. +- rule: installer_bash_manages_service + desc: an attempt by a program in a pipe installer session to manage a system service (systemd/chkconfig) + condition: evt.type=execve and proc.name in (chkconfig, systemctl) and proc.sname=fbash + output: "Service management program run by process in a fbash session (command=%proc.cmdline)" + priority: INFO + +# Notice when processes try to run any package management binary within a fbash session. +# Note: this is not a WARNING, as you'd expect some package management +# as a part of doing the installation +- rule: installer_bash_runs_pkgmgmt + desc: an attempt by a program in a pipe installer session to run a package management binary + condition: evt.type=execve and package_mgmt_binaries and proc.sname=fbash + output: "Package management program run by process in a fbash session (command=%proc.cmdline)" + priority: INFO + ########################### # Application-Related Rules ########################### diff --git a/test/falco_test.py b/test/falco_test.py index 72875c1c063..a2ff0847080 100644 --- a/test/falco_test.py +++ b/test/falco_test.py @@ -18,6 +18,9 @@ def setUp(self): self.should_detect = self.params.get('detect', '*') self.trace_file = self.params.get('trace_file', '*') + if self.should_detect: + self.detect_level = self.params.get('detect_level', '*') + # 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) @@ -44,17 +47,29 @@ def test(self): cmd, res.exit_status)) # Get the number of events detected. - res = re.search('Events detected: (\d+)', res.stdout) - if res is None: + match = re.search('Events detected: (\d+)', res.stdout) + if match is None: self.fail("Could not find a line 'Events detected: ' in falco output") - events_detected = int(res.group(1)) + events_detected = int(match.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)) + if self.should_detect: + if events_detected == 0: + self.fail("Detected {} events when should have detected > 0".format(events_detected)) + + level_line = '{}: (\d+)'.format(self.detect_level) + match = re.search(level_line, res.stdout) + + if match is None: + self.fail("Could not find a line '{}: ' in falco output".format(self.detect_level)) + + events_detected = int(match.group(1)) + + if not events_detected > 0: + self.fail("Detected {} events at level {} when should have detected > 0".format(events_detected, self.detect_level)) pass diff --git a/test/run_regression_tests.sh b/test/run_regression_tests.sh index 9f6b2a28863..1057ab61f1d 100755 --- a/test/run_regression_tests.sh +++ b/test/run_regression_tests.sh @@ -5,7 +5,8 @@ SCRIPTDIR=$(dirname $SCRIPT) MULT_FILE=$SCRIPTDIR/falco_tests.yaml function download_trace_files() { - for TRACE in traces-positive traces-negative ; do + for TRACE in traces-positive traces-negative traces-info ; do + rm -rf $SCRIPTDIR/$TRACE 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 @@ -21,6 +22,7 @@ function prepare_multiplex_file() { cat << EOF >> $MULT_FILE $NAME: detect: True + detect_level: Warning trace_file: $trace EOF done @@ -35,6 +37,17 @@ EOF EOF done + for trace in $SCRIPTDIR/traces-info/*.scap ; do + [ -e "$trace" ] || continue + NAME=`basename $trace .scap` + cat << EOF >> $MULT_FILE + $NAME: + detect: True + detect_level: Informational + trace_file: $trace +EOF + done + echo "Contents of $MULT_FILE:" cat $MULT_FILE } diff --git a/userspace/falco/lua/rule_loader.lua b/userspace/falco/lua/rule_loader.lua index 7a9774a7666..6f07e701a7d 100644 --- a/userspace/falco/lua/rule_loader.lua +++ b/userspace/falco/lua/rule_loader.lua @@ -102,14 +102,13 @@ function set_output(output_format, state) end local function priority(s) - valid_levels = {"emergency", "alert", "critical", "error", "warning", "notice", "informational", "debug"} s = string.lower(s) - for i,v in ipairs(valid_levels) do - if (string.find(v, "^"..s)) then + for i,v in ipairs(output.levels) do + if (string.find(string.lower(v), "^"..s)) then return i - 1 -- (syslog levels start at 0, lua indices start at 1) end end - error("Invalid severity level: "..level) + error("Invalid severity level: "..s) end -- Note that the rules_by_name and rules_by_idx refer to the same rule @@ -232,8 +231,8 @@ end local rule_output_counts = {total=0, by_level={}, by_name={}} -for idx, level in ipairs(output.levels) do - rule_output_counts[level] = 0 +for idx=0,table.getn(output.levels)-1,1 do + rule_output_counts.by_level[idx] = 0 end function on_event(evt_, rule_id) @@ -265,8 +264,8 @@ function print_stats() 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]) + if rule_output_counts.by_level[idx-1] > 0 or level == "Error" or level == "Warning" or level == "Informational" then + print (" "..level..": "..rule_output_counts.by_level[idx-1]) end end