Skip to content

Commit

Permalink
Add automated tests for packages/driver installs
Browse files Browse the repository at this point in the history
Add automated tests for running falco from a package and container. As a
result, this will also test building the kernel module as well as
runnning falco-probe-loader as a backup.

In travis.yml, switch to the docker-enabled vm and install dkms. This
changed the environment slightly, so change how avocado's python
dependencies are installed. After building falco, copy the .deb package
to docker/local and build a local docker image based on that package.

Add the following new tests:

 - docker_package: this uses "docker run" to run the image created in
   travis.yml. This includes using dkms to build the kernel module and
   load it. In addition, the conf directory is mounted to /host/conf, the
   rules directory is mounted to /host/rules, and the traces directory is
   mounted to /host/traces.
 - docker_package_local_driver: this disables dkms via a volume mount
   that maps /dev/null to /usr/sbin/dkms and copies the kernel module by
   hand into the container to /root/.sysdig/falco-probe-....ko. As a
   result, falco-probe-loader will use the local kernel module instead
   of building one itself.
 - debian_package: this installs the .deb package and runs the installed
   version of falco.

Ideally, there'd also be a test for downloading the driver, but since
the driver depends on the kernel as well as the falco version string,
you can't put a single driver on download.draios.com that will work
long-term.

These tests depend on the following new test attributes:
  - package: if present, this points to the docker image/debian package
    to install.
  - addl_docker_run_args: if present, will be added to the docker run
    command.
  - copy_local_driver: if present, will copy the built kernel module to
    ~/.sysdig. ~/.sysdig/* is always cleared out before each test.
  - run_duration: maps to falco's -M <secs> flag
  - trace_file is now optional.

Also add some misc general test changes:
  - Clean up our use of process.run. By default it will fail a test if the
    run program returns non-zero, so we don't have to grab the exit
    status. In addition, get rid of sudo in the command lines and use the
    sudo attribute instead.

  - Fix some tests that were writing to files below /tmp/falco_outputs
    by creating the directory first. Useful when running avocado directly.
  • Loading branch information
mstemm committed Mar 24, 2017
1 parent 52b006e commit 5b94c3b
Show file tree
Hide file tree
Showing 3 changed files with 170 additions and 15 deletions.
12 changes: 9 additions & 3 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,22 @@ language: c
env:
- BUILD_TYPE=Debug
- BUILD_TYPE=Release
sudo: required
services:
- docker
before_install:
- sudo add-apt-repository -y ppa:ubuntu-toolchain-r/test
- sudo apt-get update
install:
- sudo apt-get --force-yes install g++-4.8
- sudo apt-get install rpm linux-headers-$(uname -r)
- git clone https://github.com/draios/sysdig.git ../sysdig
- sudo apt-get install -y python-pip libvirt-dev jq
- sudo apt-get install -y python-pip libvirt-dev jq dkms
- cd ..
- curl -Lo avocado-36.0-tar.gz https://github.com/avocado-framework/avocado/archive/36.0lts.tar.gz
- tar -zxvf avocado-36.0-tar.gz
- cd avocado-36.0lts
- sudo pip install -r requirements-travis.txt
- sudo -H pip install -r requirements.txt
- sudo python setup.py install
- cd ../falco
before_script:
Expand All @@ -35,7 +38,10 @@ script:
- cmake .. -DCMAKE_BUILD_TYPE=$BUILD_TYPE -DDRAIOS_DEBUG_FLAGS="-D_DEBUG -DNDEBUG"
- make VERBOSE=1
- make package
- cd ..
- cp falco*.deb ../docker/local
- cd ../docker/local
- docker build -t sysdig/falco:test .
- cd ../..
- sudo test/run_regression_tests.sh $TRAVIS_BRANCH
notifications:
webhooks:
Expand Down
139 changes: 127 additions & 12 deletions test/falco_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
import re
import json
import sets
import glob
import shutil
import subprocess

from avocado import Test
from avocado.utils import process
Expand All @@ -21,9 +24,9 @@ def setUp(self):
self.stderr_contains = self.params.get('stderr_contains', '*', default='')
self.exit_status = self.params.get('exit_status', '*', default=0)
self.should_detect = self.params.get('detect', '*', default=False)
self.trace_file = self.params.get('trace_file', '*')
self.trace_file = self.params.get('trace_file', '*', default='')

if not os.path.isabs(self.trace_file):
if self.trace_file and not os.path.isabs(self.trace_file):
self.trace_file = os.path.join(self.basedir, self.trace_file)

self.json_output = self.params.get('json_output', '*', default=False)
Expand All @@ -43,6 +46,8 @@ def setUp(self):
if not os.path.isabs(self.conf_file):
self.conf_file = os.path.join(self.basedir, self.conf_file)

self.run_duration = self.params.get('run_duration', '*', default='')

self.disabled_rules = self.params.get('disabled_rules', '*', default='')

if self.disabled_rules == '':
Expand Down Expand Up @@ -89,15 +94,23 @@ def setUp(self):
if not isinstance(self.detect_level, list):
self.detect_level = [self.detect_level]

# Doing this in 2 steps instead of simply using
# module_is_loaded to avoid logging lsmod output to the log.
lsmod_output = process.system_output("lsmod", verbose=False)
self.package = self.params.get('package', '*', default='None')

if self.package == 'None':
# Doing this in 2 steps instead of simply using
# module_is_loaded to avoid logging lsmod output to the log.
lsmod_output = process.system_output("lsmod", verbose=False)

if linux_modules.parse_lsmod_for_module(lsmod_output, 'falco_probe') == {}:
self.log.debug("Loading falco kernel module")
process.run('insmod {}/driver/falco-probe.ko'.format(self.falcodir), sudo=True)

if linux_modules.parse_lsmod_for_module(lsmod_output, 'falco_probe') == {}:
self.log.debug("Loading falco kernel module")
process.run('sudo insmod {}/driver/falco-probe.ko'.format(self.falcodir))
self.addl_docker_run_args = self.params.get('addl_docker_run_args', '*', default='')

self.str_variant = self.trace_file
self.copy_local_driver = self.params.get('copy_local_driver', '*', default=False)

# Used by possibly_copy_local_driver as well as docker run
self.module_dir = os.path.expanduser("~/.sysdig")

self.outputs = self.params.get('outputs', '*', default='')

Expand All @@ -111,6 +124,10 @@ def setUp(self):
output['file'] = item2[0]
output['line'] = item2[1]
outputs.append(output)
filedir = os.path.dirname(output['file'])
# Create the parent directory for the trace file if it doesn't exist.
if not os.path.isdir(filedir):
os.makedirs(filedir)
self.outputs = outputs

self.disable_tags = self.params.get('disable_tags', '*', default='')
Expand All @@ -123,6 +140,10 @@ def setUp(self):
if self.run_tags == '':
self.run_tags=[]

def tearDown(self):
if self.package != 'None':
self.uninstall_package()

def check_rules_warnings(self, res):

found_warning = sets.Set()
Expand Down Expand Up @@ -231,19 +252,113 @@ def check_json_output(self, res):
if not attr in obj:
self.fail("Falco JSON object {} does not contain property \"{}\"".format(line, attr))

def install_package(self):

if self.package.startswith("docker:"):

image = self.package.split(":", 1)[1]
# Remove an existing falco-test container first. Note we don't check the output--docker rm
# doesn't have an -i equivalent.
res = process.run("docker rm falco-test", ignore_status=True)
rules_dir = os.path.abspath(os.path.join(self.basedir, "./rules"))
conf_dir = os.path.abspath(os.path.join(self.basedir, "../"))
traces_dir = os.path.abspath(os.path.join(self.basedir, "./trace_files"))
self.falco_binary_path = "docker run -i -t --name falco-test --privileged " \
"-v {}:/host/rules -v {}:/host/conf -v {}:/host/traces " \
"-v /var/run/docker.sock:/host/var/run/docker.sock " \
"-v /dev:/host/dev -v /proc:/host/proc:ro -v /boot:/host/boot:ro " \
"-v /lib/modules:/host/lib/modules:ro -v {}:/root/.sysdig:ro -v " \
"/usr:/host/usr:ro {} {} falco".format(
rules_dir, conf_dir, traces_dir,
self.module_dir, self.addl_docker_run_args, image)

elif self.package.endswith(".deb"):
self.falco_binary_path = '/usr/bin/falco';

package_glob = "{}/{}".format(self.falcodir, self.package)

matches = glob.glob(package_glob)

if len(matches) != 1:
self.fail("Package path {} did not match exactly 1 file. Instead it matched: {}", package_glob, ",".join(matches))

package_path = matches[0]

cmdline = "dpkg -i {}".format(package_path)
self.log.debug("Installing debian package via \"{}\"".format(cmdline))
res = process.run(cmdline, timeout=120, sudo=True)

def uninstall_package(self):

if self.package.startswith("docker:"):
# Remove the falco-test image. Here we *do* check the return value
res = process.run("docker rm falco-test")

elif self.package.endswith(".deb"):
cmdline = "dpkg -r falco"
self.log.debug("Uninstalling debian package via \"{}\"".format(cmdline))
res = process.run(cmdline, timeout=120, sudo=True)

def possibly_copy_driver(self):
# Remove the contents of ~/.sysdig regardless of
# copy_local_driver.
self.log.debug("Checking for module dir {}".format(self.module_dir))
if os.path.isdir(self.module_dir):
self.log.info("Removing files below directory {}".format(self.module_dir))
for rmfile in glob.glob(self.module_dir + "/*"):
self.log.debug("Removing file {}".format(rmfile))
os.remove(rmfile)

if self.copy_local_driver:
verstr = subprocess.check_output([self.falco_binary_path, "--version"]).rstrip()
self.log.info("verstr {}".format(verstr))
falco_version = verstr.split(" ")[2]
self.log.info("falco_version {}".format(falco_version))
arch = subprocess.check_output(["uname", "-m"]).rstrip()
self.log.info("arch {}".format(arch))
kernel_release = subprocess.check_output(["uname", "-r"]).rstrip()
self.log.info("kernel release {}".format(kernel_release))

# sysdig-probe-loader has a more comprehensive set of ways to
# find the config hash. We only look at /boot/config-<kernel release>
md5_output = subprocess.check_output(["md5sum", "/boot/config-{}".format(kernel_release)]).rstrip()
config_hash = md5_output.split(" ")[0]

probe_filename = "falco-probe-{}-{}-{}-{}.ko".format(falco_version, arch, kernel_release, config_hash)
driver_path = os.path.join(self.falcodir, "driver", "falco-probe.ko")
module_path = os.path.join(self.module_dir, probe_filename)
self.log.debug("Copying {} to {}".format(driver_path, module_path))
shutil.copyfile(driver_path, module_path)

def test(self):
self.log.info("Trace file %s", self.trace_file)

# Run the provided trace file though falco
cmd = '{}/userspace/falco/falco {} {} -c {} -e {} -o json_output={} -v'.format(
self.falcodir, self.rules_args, self.disabled_args, self.conf_file, self.trace_file, self.json_output)
self.falco_binary_path = '{}/userspace/falco/falco'.format(self.falcodir)

self.possibly_copy_driver()

if self.package != 'None':
# This sets falco_binary_path as a side-effect.
self.install_package()

trace_arg = self.trace_file

if self.trace_file:
trace_arg = "-e {}".format(self.trace_file)

# Run falco
cmd = '{} {} {} -c {} {} -o json_output={} -v'.format(
self.falco_binary_path, self.rules_args, self.disabled_args, self.conf_file, trace_arg, self.json_output)

for tag in self.disable_tags:
cmd += ' -T {}'.format(tag)

for tag in self.run_tags:
cmd += ' -t {}'.format(tag)

if self.run_duration:
cmd += ' -M {}'.format(self.run_duration)

self.falco_proc = process.SubProcess(cmd)

res = self.falco_proc.run(timeout=180, sig=9)
Expand Down
34 changes: 34 additions & 0 deletions test/falco_tests.yaml.in
Original file line number Diff line number Diff line change
@@ -1,4 +1,38 @@
trace_files: !mux

docker_package:
package: docker:sysdig/falco:test
detect: True
detect_level: WARNING
rules_file: /host/rules/rule_names_with_spaces.yaml
trace_file: /host/traces/cat_write.scap
conf_file: /host/conf/falco.yaml

# This uses a volume mount to overwrite and prevent /usr/sbin/dkms
# from being run. As a result, it will force falco-probe-loader to
# fall back to loading the driver from ~/.sysdig. Setting
# copy_local_driver to True copied the driver to ~/.sysdig, so it
# will be available. In this case, we're running live for 5 seconds
# just to see if falco can load the driver.

docker_package_local_driver:
package: docker:sysdig/falco:test
addl_docker_run_args: -v /dev/null:/usr/sbin/dkms
copy_local_driver: True
detect: False
detect_level: WARNING
rules_file: /host/rules/rule_names_with_spaces.yaml
conf_file: /host/conf/falco.yaml
run_duration: 5

debian_package:
package: falco*.deb
detect: True
detect_level: WARNING
rules_file:
- rules/rule_names_with_spaces.yaml
trace_file: trace_files/cat_write.scap

builtin_rules_no_warnings:
detect: False
trace_file: trace_files/empty.scap
Expand Down

0 comments on commit 5b94c3b

Please sign in to comment.