crowdsec/test/bats/07_setup.bats

816 lines
24 KiB
Bash

#!/usr/bin/env bats
# vim: ft=bats:list:ts=8:sts=4:sw=4:et:ai:si:
set -u
setup_file() {
load "../lib/setup_file.sh"
./instance-data load
HUB_DIR=$(config_get '.config_paths.hub_dir')
export HUB_DIR
DETECT_YAML="${HUB_DIR}/detect.yaml"
export DETECT_YAML
# shellcheck disable=SC2154
TESTDATA="${BATS_TEST_DIRNAME}/testdata/07_setup"
export TESTDATA
export CROWDSEC_FEATURE_CSCLI_SETUP="true"
}
teardown_file() {
load "../lib/teardown_file.sh"
}
setup() {
load "../lib/setup.sh"
load "../lib/bats-file/load.bash"
load "../lib/bats-mock/load.bash"
./instance-data load
}
teardown() {
./instance-crowdsec stop
}
#----------
#shellcheck disable=SC2154
@test "cscli setup" {
rune -0 cscli help
assert_line --regexp '^ +setup +Tools to configure crowdsec$'
rune -0 cscli setup --help
assert_line 'Usage:'
assert_line ' cscli setup [command]'
assert_line 'Manage hub configuration and service detection'
assert_line --partial "detect detect running services, generate a setup file"
assert_line --partial "datasources generate datasource (acquisition) configuration from a setup file"
assert_line --partial "install-hub install items from a setup file"
assert_line --partial "validate validate a setup file"
# cobra should return error for non-existing sub-subcommands, but doesn't
rune -0 cscli setup blahblah
assert_line 'Usage:'
}
@test "cscli setup detect --help; --detect-config" {
rune -0 cscli setup detect --help
assert_line --regexp "detect running services, generate a setup file"
assert_line 'Usage:'
assert_line ' cscli setup detect [flags]'
assert_line --partial "--detect-config string path to service detection configuration (default \"${HUB_DIR}/detect.yaml\")"
assert_line --partial "--force-process strings force detection of a running process (can be repeated)"
assert_line --partial "--force-unit strings force detection of a systemd unit (can be repeated)"
assert_line --partial "--list-supported-services do not detect; only print supported services"
assert_line --partial "--force-os-family string override OS.Family: one of linux, freebsd, windows or darwin"
assert_line --partial "--force-os-id string override OS.ID=[debian | ubuntu | , redhat...]"
assert_line --partial "--force-os-version string override OS.RawVersion (of OS or Linux distribution)"
assert_line --partial "--skip-service strings ignore a service, don't recommend hub/datasources (can be repeated)"
rune -1 cscli setup detect --detect-config /path/does/not/exist
assert_stderr --partial "detecting services: while reading file: open /path/does/not/exist: no such file or directory"
# rm -f "${HUB_DIR}/detect.yaml"
}
@test "cscli setup detect (linux), --skip-service" {
[[ ${OSTYPE} =~ linux.* ]] || skip
tempfile=$(TMPDIR="$BATS_TEST_TMPDIR" mktemp)
cat <<-EOT >"${tempfile}"
version: 1.0
detect:
linux:
when:
- OS.Family == "linux"
install:
collections:
- crowdsecurity/linux
thewiz:
when:
- OS.Family != "linux"
foobarbaz:
EOT
rune -0 cscli setup detect --detect-config "$tempfile"
assert_json '{setup:[{detected_service:"foobarbaz"},{detected_service:"linux",install:{collections:["crowdsecurity/linux"]}}]}'
rune -0 cscli setup detect --detect-config "$tempfile" --skip-service linux
assert_json '{setup:[{detected_service:"foobarbaz"}]}'
}
@test "cscli setup detect --force-os-*" {
rune -0 cscli setup detect --force-os-family linux --detect-config "${TESTDATA}/detect.yaml"
rune -0 jq -cS '.setup[] | select(.detected_service=="linux")' <(output)
assert_json '{detected_service:"linux",install:{collections:["crowdsecurity/linux"]},datasource:{source:"file",labels:{type:"syslog"},filenames:["/var/log/syslog","/var/log/kern.log","/var/log/messages"]}}'
rune -0 cscli setup detect --force-os-family freebsd --detect-config "${TESTDATA}/detect.yaml"
rune -0 jq -cS '.setup[] | select(.detected_service=="freebsd")' <(output)
assert_json '{detected_service:"freebsd",install:{collections:["crowdsecurity/freebsd"]}}'
rune -0 cscli setup detect --force-os-family windows --detect-config "${TESTDATA}/detect.yaml"
rune -0 jq -cS '.setup[] | select(.detected_service=="windows")' <(output)
assert_json '{detected_service:"windows",install:{collections:["crowdsecurity/windows"]}}'
rune -0 cscli setup detect --force-os-family darwin --detect-config "${TESTDATA}/detect.yaml"
# XXX do we want do disallow unknown family?
# assert_stderr --partial "detecting services: OS 'darwin' not supported"
# XXX TODO force-os-id, force-os-version
}
@test "cscli setup detect --list-supported-services" {
tempfile=$(TMPDIR="$BATS_TEST_TMPDIR" mktemp)
cat <<-EOT >"${tempfile}"
version: 1.0
detect:
thewiz:
foobarbaz:
apache2:
EOT
rune -0 cscli setup detect --list-supported-services --detect-config "$tempfile"
# the service list is sorted
assert_output - <<-EOT
apache2
foobarbaz
thewiz
EOT
cat <<-EOT >"${tempfile}"
thisisajoke
EOT
rune -1 cscli setup detect --list-supported-services --detect-config "$tempfile"
assert_stderr --partial "while parsing ${tempfile}: yaml: unmarshal errors:"
rm -f "$tempfile"
}
@test "cscli setup detect (systemctl)" {
cat <<-EOT >"${DETECT_YAML}"
version: 1.0
detect:
apache2:
when:
- UnitFound("mock-apache2.service")
datasource:
source: file
filename: dummy.log
labels:
type: apache2
EOT
# transparently mock systemctl. It's easier if you can tell the application
# under test which executable to call (in which case just call $mock) but
# here we do the symlink and $PATH dance as an example
mocked_command="systemctl"
# mock setup
mock="$(mock_create)"
mock_path="${mock%/*}"
mock_file="${mock##*/}"
ln -sf "${mock_path}/${mock_file}" "${mock_path}/${mocked_command}"
#shellcheck disable=SC2030
PATH="${mock_path}:${PATH}"
mock_set_output "$mock" \
'UNIT FILE STATE VENDOR PRESET
snap-bare-5.mount enabled enabled
snap-core-13308.mount enabled enabled
snap-firefox-1635.mount enabled enabled
snap-fx-158.mount enabled enabled
snap-gimp-393.mount enabled enabled
snap-gtk\x2dcommon\x2dthemes-1535.mount enabled enabled
snap-kubectl-2537.mount enabled enabled
snap-rustup-1027.mount enabled enabled
cups.path enabled enabled
console-setup.service enabled enabled
dmesg.service enabled enabled
getty@.service enabled enabled
grub-initrd-fallback.service enabled enabled
irqbalance.service enabled enabled
keyboard-setup.service enabled enabled
mock-apache2.service enabled enabled
networkd-dispatcher.service enabled enabled
ua-timer.timer enabled enabled
update-notifier-download.timer enabled enabled
update-notifier-motd.timer enabled enabled
20 unit files listed.'
mock_set_status "$mock" 1 2
rune -0 cscli setup detect
rune -0 jq -c '.setup' <(output)
# If a call to UnitFoundwas part of the expression and it returned true,
# there is a default journalctl_filter derived from the unit's name.
assert_json '[{datasource:{source:"file",filename:"dummy.log",labels:{type:"apache2"}},detected_service:"apache2"}]'
# the command was called exactly once
[[ $(mock_get_call_num "$mock") -eq 1 ]]
# the command was called with the expected parameters
[[ $(mock_get_call_args "$mock" 1) == "list-unit-files --state=enabled,generated,static" ]]
rune -1 systemctl
# mock teardown
unlink "${mock_path}/${mocked_command}"
PATH="${PATH/${mock_path}:/}"
}
# XXX this is the same boilerplate as the previous test, can be simplified
@test "cscli setup detect (snub systemd)" {
cat <<-EOT >"${DETECT_YAML}"
version: 1.0
detect:
apache2:
when:
- UnitFound("mock-apache2.service")
datasource:
source: file
filename: dummy.log
labels:
type: apache2
EOT
# transparently mock systemctl. It's easier if you can tell the application
# under test which executable to call (in which case just call $mock) but
# here we do the symlink and $PATH dance as an example
mocked_command="systemctl"
# mock setup
mock="$(mock_create)"
mock_path="${mock%/*}"
mock_file="${mock##*/}"
ln -sf "${mock_path}/${mock_file}" "${mock_path}/${mocked_command}"
#shellcheck disable=SC2031
PATH="${mock_path}:${PATH}"
# we don't really care about the output, it's not used anyway
mock_set_output "$mock" ""
mock_set_status "$mock" 1 2
rune -0 cscli setup detect --snub-systemd
# setup must not be 'null', but an empty list
assert_json '{setup:[]}'
# the command was never called
[[ $(mock_get_call_num "$mock") -eq 0 ]]
rune -0 systemctl
# mock teardown
unlink "${mock_path}/${mocked_command}"
PATH="${PATH/${mock_path}:/}"
}
@test "cscli setup detect --force-unit" {
cat <<-EOT >"${DETECT_YAML}"
version: 1.0
detect:
apache2:
when:
- UnitFound("force-apache2")
datasource:
source: file
filename: dummy.log
labels:
type: apache2
apache3:
when:
- UnitFound("force-apache3")
datasource:
source: file
filename: dummy.log
labels:
type: apache3
EOT
rune -0 cscli setup detect --force-unit force-apache2
rune -0 jq -cS '.setup' <(output)
assert_json '[{datasource:{source:"file",filename:"dummy.log",labels:{"type":"apache2"}},detected_service:"apache2"}]'
rune -0 cscli setup detect --force-unit force-apache2,force-apache3
rune -0 jq -cS '.setup' <(output)
assert_json '[{datasource:{source:"file",filename:"dummy.log",labels:{type:"apache2"}},detected_service:"apache2"},{datasource:{source:"file",filename:"dummy.log",labels:{"type":"apache3"}},detected_service:"apache3"}]'
# force-unit can be specified multiple times, the order does not matter
rune -0 cscli setup detect --force-unit force-apache3 --force-unit force-apache2
rune -0 jq -cS '.setup' <(output)
assert_json '[{datasource:{source:"file",filename:"dummy.log",labels:{type:"apache2"}},detected_service:"apache2"},{datasource:{source:"file",filename:"dummy.log",labels:{type:"apache3"}},detected_service:"apache3"}]'
rune -1 cscli setup detect --force-unit mock-doesnotexist
assert_stderr --partial "detecting services: unit(s) forced but not supported: [mock-doesnotexist]"
}
@test "cscli setup detect (process)" {
# This is harder to mock, because gopsutil requires proc/ to be a mount
# point. So we pick a process that exists for sure.
expected_process=$(basename "$SHELL")
cat <<-EOT >"${DETECT_YAML}"
version: 1.0
detect:
apache2:
when:
- ProcessRunning("${expected_process}")
apache3:
when:
- ProcessRunning("this-does-not-exist")
EOT
rune -0 cscli setup detect
rune -0 jq -cS '.setup' <(output)
assert_json '[{detected_service:"apache2"}]'
}
@test "cscli setup detect --force-process" {
cat <<-EOT >"${DETECT_YAML}"
version: 1.0
detect:
apache2:
when:
- ProcessRunning("force-apache2")
apache3:
when:
- ProcessRunning("this-does-not-exist")
EOT
rune -0 cscli setup detect --force-process force-apache2
rune -0 jq -cS '.setup' <(output)
assert_json '[{detected_service:"apache2"}]'
}
@test "cscli setup detect (acquisition only, no hub items)" {
cat <<-EOT >"${DETECT_YAML}"
version: 1.0
detect:
apache2:
when:
- UnitFound("force-apache2")
datasource:
source: file
filename: dummy.log
labels:
type: apache2
EOT
rune -0 cscli setup detect --force-unit force-apache2
rune -0 jq -cS '.setup' <(output)
assert_json '[{datasource:{source:"file",filename:"dummy.log",labels:{type:"apache2"}},detected_service:"apache2"}]'
rune -0 cscli setup detect --force-unit force-apache2 --yaml
assert_output - <<-EOT
setup:
- detected_service: apache2
datasource:
filename: dummy.log
labels:
type: apache2
source: file
EOT
}
@test "cscli setup detect (full acquisition section)" {
skip "not supported yet"
cat <<-EOT >"${DETECT_YAML}"
version: 1.0
detect:
foobar:
datasource:
filenames:
- /path/to/log/*.log
exclude_regexps:
- ^/path/to/log/excludeme\.log$
force_inotify: true
mode: tail
labels:
type: foolog
EOT
rune -0 cscli setup detect --yaml
assert_output - <<-EOT
setup:
- detected_service: foobar
datasource:
filenames:
- /path/to/log/*.log
exclude_regexps:
- ^/path/to/log/excludeme.log$
force_inotify: true
mode: tail
labels:
type: foolog
EOT
}
@test "cscli setup detect + acquis + install (no acquisition, no hub items)" {
# no-op edge case, to make sure we don't crash
cat <<-EOT >"${DETECT_YAML}"
version: 1.0
detect:
always:
EOT
rune -0 cscli setup detect
assert_json '{setup:[{detected_service:"always"}]}'
setup=$output
rune -0 cscli setup datasources /dev/stdin <<<"$setup"
rune -0 cscli setup install-hub /dev/stdin <<<"$setup"
}
@test "cscli setup detect (with collections)" {
cat <<-EOT >"${DETECT_YAML}"
version: 1.0
detect:
foobar:
when:
- ProcessRunning("force-foobar")
install:
collections:
- crowdsecurity/foobar
qox:
when:
- ProcessRunning("test-qox")
install:
collections:
- crowdsecurity/foobar
apache2:
when:
- ProcessRunning("force-apache2")
install:
collections:
- crowdsecurity/apache2
EOT
rune -0 cscli setup detect --force-process force-apache2,force-foobar
rune -0 jq -Sc '.setup | sort' <(output)
assert_json '[{install:{collections:["crowdsecurity/apache2"]},detected_service:"apache2"},{install:{collections:["crowdsecurity/foobar"]},detected_service:"foobar"}]'
}
@test "cscli setup detect (with acquisition)" {
cat <<-EOT >"${DETECT_YAML}"
version: 1.0
detect:
foobar:
when:
- ProcessRunning("force-foobar")
datasource:
source: file
labels:
type: foobar
filenames:
- /var/log/apache2/*.log
- /var/log/*http*/*.log
EOT
rune -0 cscli setup detect --force-process force-foobar
rune -0 yq -op '.setup | sort_keys(..)' <(output)
assert_output - <<-EOT
0.datasource.filenames.0 = /var/log/apache2/*.log
0.datasource.filenames.1 = /var/log/*http*/*.log
0.datasource.labels.type = foobar
0.datasource.source = file
0.detected_service = foobar
EOT
rune -1 cscli setup detect --force-process mock-doesnotexist
assert_stderr --partial "detecting services: process(es) forced but not supported: [mock-doesnotexist]"
}
@test "cscli setup detect (datasource validation)" {
cat <<-EOT >"${DETECT_YAML}"
version: 1.0
detect:
foobar:
datasource:
labels:
type: something
EOT
rune -1 cscli setup detect
assert_stderr --partial "detecting services: invalid datasource for foobar: source is empty"
# more datasource-specific tests are in detect_test.go
}
@test "cscli setup install-hub (dry run)" {
# it's not installed
rune -0 cscli collections list -o json
rune -0 jq -r '.collections[].name' <(output)
refute_line "crowdsecurity/apache2"
# we install it
rune -0 cscli setup install-hub /dev/stdin --dry-run <<< '{"setup":[{"install":{"collections":["crowdsecurity/apache2"]}}]}'
assert_output 'dry-run: would install collection crowdsecurity/apache2'
# still not installed
rune -0 cscli collections list -o json
rune -0 jq -r '.collections[].name' <(output)
refute_line "crowdsecurity/apache2"
}
@test "cscli setup install-hub (dry run: install multiple collections)" {
# it's not installed
rune -0 cscli collections list -o json
rune -0 jq -r '.collections[].name' <(output)
refute_line "crowdsecurity/apache2"
# we install it
rune -0 cscli setup install-hub /dev/stdin --dry-run <<< '{"setup":[{"install":{"collections":["crowdsecurity/apache2"]}}]}'
assert_output 'dry-run: would install collection crowdsecurity/apache2'
# still not installed
rune -0 cscli collections list -o json
rune -0 jq -r '.collections[].name' <(output)
refute_line "crowdsecurity/apache2"
}
@test "cscli setup install-hub (dry run: install multiple collections, parsers, scenarios, postoverflows)" {
rune -0 cscli setup install-hub /dev/stdin --dry-run <<< '{"setup":[{"install":{"collections":["crowdsecurity/foo","johndoe/bar"],"parsers":["crowdsecurity/fooparser","johndoe/barparser"],"scenarios":["crowdsecurity/fooscenario","johndoe/barscenario"],"postoverflows":["crowdsecurity/foopo","johndoe/barpo"]}}]}'
assert_line 'dry-run: would install collection crowdsecurity/foo'
assert_line 'dry-run: would install collection johndoe/bar'
assert_line 'dry-run: would install parser crowdsecurity/fooparser'
assert_line 'dry-run: would install parser johndoe/barparser'
assert_line 'dry-run: would install scenario crowdsecurity/fooscenario'
assert_line 'dry-run: would install scenario johndoe/barscenario'
assert_line 'dry-run: would install postoverflow crowdsecurity/foopo'
assert_line 'dry-run: would install postoverflow johndoe/barpo'
}
@test "cscli setup datasources" {
rune -0 cscli setup datasources --help
assert_line --partial "--to-dir string write the configuration to a directory, in multiple files"
# single item
rune -0 cscli setup datasources /dev/stdin <<-EOT
setup:
- datasource:
source: file
labels:
type: syslog
filenames:
- /var/log/apache2/*.log
- /var/log/*http*/*.log
- /var/log/httpd/*.log
EOT
# remove diclaimer
rune -0 yq '. head_comment=""' <(output)
assert_output - <<-EOT
filenames:
- /var/log/apache2/*.log
- /var/log/*http*/*.log
- /var/log/httpd/*.log
labels:
type: syslog
source: file
EOT
# multiple items
rune -0 cscli setup datasources /dev/stdin <<-EOT
setup:
- datasource:
labels:
type: syslog
filenames:
- /var/log/apache2/*.log
- /var/log/*http*/*.log
- /var/log/httpd/*.log
- datasource:
labels:
type: foobar
filenames:
- /var/log/foobar/*.log
- datasource:
labels:
type: barbaz
filenames:
- /path/to/barbaz.log
EOT
rune -0 yq '. head_comment=""' <(output)
assert_output - <<-EOT
filenames:
- /var/log/apache2/*.log
- /var/log/*http*/*.log
- /var/log/httpd/*.log
labels:
type: syslog
---
filenames:
- /var/log/foobar/*.log
labels:
type: foobar
---
filenames:
- /path/to/barbaz.log
labels:
type: barbaz
EOT
# multiple items, to a directory
# avoid the BATS_TEST_TMPDIR variable, it can have a double //
acquisdir=$(TMPDIR="$BATS_FILE_TMPDIR" mktemp -u)
mkdir "$acquisdir"
rune -0 cscli setup datasources /dev/stdin --to-dir "$acquisdir" <<-EOT
setup:
- detected_service: apache2
datasource:
labels:
type: syslog
filenames:
- /var/log/apache2/*.log
- /var/log/*http*/*.log
- /var/log/httpd/*.log
- detected_service: foobar
datasource:
labels:
type: foobar
filenames:
- /var/log/foobar/*.log
- detected_service: barbaz
datasource:
labels:
type: barbaz
filenames:
- /path/to/barbaz.log
EOT
# XXX what if detected_service is missing?
rune -0 cat "${acquisdir}/setup.apache2.yaml"
rune -0 yq '. head_comment=""' <(output)
assert_output - <<-EOT
filenames:
- /var/log/apache2/*.log
- /var/log/*http*/*.log
- /var/log/httpd/*.log
labels:
type: syslog
EOT
rune -0 cat "${acquisdir}/setup.foobar.yaml"
rune -0 yq '. head_comment=""' <(output)
assert_output - <<-EOT
filenames:
- /var/log/foobar/*.log
labels:
type: foobar
EOT
rune -0 cat "${acquisdir}/setup.barbaz.yaml"
rune -0 yq '. head_comment=""' <(output)
assert_output - <<-EOT
filenames:
- /path/to/barbaz.log
labels:
type: barbaz
EOT
rm -rf -- "${acquisdir:?}"
mkdir "$acquisdir"
# having both filenames and journalctl does not generate two files: the datasource is copied as-is, even if incorrect
rune -0 cscli setup datasources /dev/stdin --to-dir "$acquisdir" <<-EOT
setup:
- detected_service: apache2
install:
collections:
- crowdsecurity/apache2
datasource:
labels:
type: apache2
filenames:
- /var/log/apache2/*.log
- /var/log/*http*/*.log
- /var/log/httpd/*.log
journalctl_filter:
- _SYSTEMD_UNIT=apache2.service
EOT
rune -0 cat "${acquisdir}/setup.apache2.yaml"
rune -0 yq '. head_comment=""' <(output)
assert_output - <<-EOT
filenames:
- /var/log/apache2/*.log
- /var/log/*http*/*.log
- /var/log/httpd/*.log
journalctl_filter:
- _SYSTEMD_UNIT=apache2.service
labels:
type: apache2
EOT
# the directory must exist
rune -1 cscli setup datasources /dev/stdin --to-dir /path/does/not/exist <<< '{}'
assert_stderr --partial "directory /path/does/not/exist does not exist"
# of course it must be a directory
touch "${acquisdir}/notadir"
rune -1 cscli setup datasources /dev/stdin --to-dir "${acquisdir}/notadir" <<-EOT
setup:
- detected_service: apache2
datasource:
filenames:
- /var/log/apache2/*.log
EOT
assert_stderr --partial "open ${acquisdir}/notadir/setup.apache2.yaml: not a directory"
rm -rf -- "${acquisdir:?}"
}
@test "cscli setup datasources (disclaimer)" {
disclaimer="This file was automatically generated"
rune -0 cscli setup datasources /dev/stdin <<<"setup:"
rune -0 yq 'head_comment' <(output)
assert_output --partial "$disclaimer"
rune -0 cscli setup datasources /dev/stdin <<-EOT
setup:
- detected_service: something
datasource:
labels:
type: syslog
filenames:
- /var/log/something.log
EOT
rune -0 yq 'head_comment' <(output)
assert_output --partial "$disclaimer"
}
@test "cscli setup (custom journalctl filter)" {
tempfile=$(TMPDIR="$BATS_TEST_TMPDIR" mktemp)
cat <<-EOT >"${tempfile}"
version: 1.0
detect:
thewiz:
when:
- UnitFound("thewiz.service")
datasource:
source: journalctl
labels:
type: thewiz
journalctl_filter:
- "SYSLOG_IDENTIFIER=TheWiz"
EOT
rune -0 cscli setup detect --detect-config "$tempfile" --force-unit thewiz.service
rune -0 jq -cS '.' <(output)
assert_json '{setup:[{datasource:{source:"journalctl",journalctl_filter:["SYSLOG_IDENTIFIER=TheWiz"],labels:{type:"thewiz"}},detected_service:"thewiz"}]}'
rune -0 cscli setup datasources <(output)
rune -0 yq '. head_comment=""' <(output)
assert_output - <<-EOT
journalctl_filter:
- SYSLOG_IDENTIFIER=TheWiz
labels:
type: thewiz
source: journalctl
EOT
rm -f "$tempfile"
}
@test "cscli setup validate" {
# an empty file is not enough
rune -1 cscli setup validate /dev/null
assert_output "EOF"
assert_stderr --partial "invalid setup file"
# this is ok; install nothing
rune -0 cscli setup validate /dev/stdin <<-EOT
setup:
EOT
refute_output
rune -1 cscli setup validate /dev/stdin <<-EOT
se tup:
EOT
assert_output - <<-EOT
[1:1] unknown field "se tup"
> 1 | se tup:
^
EOT
assert_stderr --partial "invalid setup file"
rune -1 cscli setup validate /dev/stdin <<-EOT
setup:
alsdk al; sdf
EOT
assert_output "while unmarshaling setup file: yaml: line 2: could not find expected ':'"
assert_stderr --partial "invalid setup file"
}