replace 'timeout' helper with async python script; allow hub preload in func tests; improve item removal (#2591)

* replace 'timeout' helper with async python script; allow hub preload in func tests; improve item removal
* func tests: cscli hub update/upgrade
* docker test update
* Update docker entrypoint to disable items with --force

The --force flag was not transmitted to cscli, but is required after the hub refact
to disable items inside installed collections
This commit is contained in:
mmetc 2023-11-14 17:36:07 +01:00 committed by GitHub
parent f8c91d20b0
commit 4a6fd338e0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 511 additions and 166 deletions

View file

@ -101,19 +101,23 @@ register_bouncer() {
# $2 can be install, remove, upgrade
# $3 is a list of object names separated by space
cscli_if_clean() {
local itemtype="$1"
local action="$2"
local objs=$3
shift 3
# loop over all objects
for obj in $3; do
if cscli "$1" inspect "$obj" -o json | yq -e '.tainted // false' >/dev/null 2>&1; then
echo "Object $1/$obj is tainted, skipping"
for obj in $objs; do
if cscli "$itemtype" inspect "$obj" -o json | yq -e '.tainted // false' >/dev/null 2>&1; then
echo "Object $itemtype/$obj is tainted, skipping"
else
# # Too verbose? Only show errors if not in debug mode
# if [ "$DEBUG" != "true" ]; then
# error_only=--error
# fi
error_only=""
echo "Running: cscli $error_only $1 $2 \"$obj\""
echo "Running: cscli $error_only $itemtype $action \"$obj\" $*"
# shellcheck disable=SC2086
cscli $error_only "$1" "$2" "$obj"
cscli $error_only "$itemtype" "$action" "$obj" "$@"
fi
done
}

View file

@ -30,8 +30,8 @@ def test_install_two_collections(crowdsec, flavor):
cs.wait_for_log([
# f'*collections install "{it1}"*'
# f'*collections install "{it2}"*'
f'*Enabled collections : {it1}*',
f'*Enabled collections : {it2}*',
f'*Enabled collections: {it1}*',
f'*Enabled collections: {it2}*',
])
@ -72,7 +72,7 @@ def test_install_and_disable_collection(crowdsec, flavor):
assert it not in items
logs = cs.log_lines()
# check that there was no attempt to install
assert not any(f'Enabled collections : {it}' in line for line in logs)
assert not any(f'Enabled collections: {it}' in line for line in logs)
# already done in bats, prividing here as example of a somewhat complex test
@ -91,7 +91,7 @@ def test_taint_bubble_up(crowdsec, tmp_path_factory, flavor):
# implicit check for tainted=False
assert items[coll]['status'] == 'enabled'
cs.wait_for_log([
f'*Enabled collections : {coll}*',
f'*Enabled collections: {coll}*',
])
scenario = 'crowdsecurity/http-crawl-non_statics'

View file

@ -21,8 +21,8 @@ def test_install_two_scenarios(crowdsec, flavor):
}
with crowdsec(flavor=flavor, environment=env) as cs:
cs.wait_for_log([
f'*scenarios install "{it1}*"',
f'*scenarios install "{it2}*"',
f'*scenarios install "{it1}"*',
f'*scenarios install "{it2}"*',
"*Starting processing data*"
])
cs.wait_for_http(8080, '/health', want_status=HTTPStatus.OK)

View file

@ -76,10 +76,19 @@ func (i *Item) enable() error {
// purge removes the actual config file that was downloaded
func (i *Item) purge() error {
if !i.Downloaded {
log.Infof("removing %s: not downloaded -- no need to remove", i.Name)
return nil
}
itempath := i.hub.local.HubDir + "/" + i.RemotePath
// disable hub file
if err := os.Remove(itempath); err != nil {
if os.IsNotExist(err) {
log.Debugf("%s doesn't exist, no need to remove", itempath)
return nil
}
return fmt.Errorf("while removing file: %w", err)
}
@ -94,17 +103,6 @@ func (i *Item) disable(purge bool, force bool) error {
// XXX: should return the number of disabled/purged items to inform the upper layer whether to reload or not
var err error
// already disabled, noop unless purge
if !i.Installed {
if purge {
if err = i.purge(); err != nil {
return err
}
}
return nil
}
if i.IsLocal() {
return fmt.Errorf("%s isn't managed by hub. Please delete manually", i.Name)
}
@ -134,6 +132,11 @@ func (i *Item) disable(purge bool, force bool) error {
}
}
if !i.Installed && !purge {
log.Infof("removing %s: not installed -- no need to remove", i.Name)
return nil
}
syml, err := filepath.Abs(i.hub.local.InstallDir + "/" + i.Type + "/" + i.Stage + "/" + i.FileName)
if err != nil {
return err
@ -168,6 +171,10 @@ func (i *Item) disable(purge bool, force bool) error {
}
if err = os.Remove(syml); err != nil {
if os.IsNotExist(err) {
log.Debugf("%s doesn't exist, no need to remove", syml)
return nil
}
return fmt.Errorf("while removing symlink: %w", err)
}

View file

@ -55,16 +55,6 @@ func (i *Item) Install(force bool, downloadOnly bool) error {
func (i *Item) Remove(purge bool, forceAction bool) (bool, error) {
removed := false
if !i.Downloaded {
log.Infof("removing %s: not downloaded -- no removal required", i.Name)
return false, nil
}
if !i.Installed && !purge {
log.Infof("removing %s: already uninstalled", i.Name)
return false, nil
}
if err := i.disable(purge, forceAction); err != nil {
return false, fmt.Errorf("unable to disable %s: %w", i.Name, err)
}

View file

@ -298,6 +298,7 @@ func (h *Hub) GetAllItems(itemType string) ([]*Item, error) {
return ret, nil
}
// GetInstalledItems returns the list of installed items
func (h *Hub) GetInstalledItems(itemType string) ([]*Item, error) {
items, ok := h.Items[itemType]

View file

@ -0,0 +1,71 @@
#!/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"
}
setup() {
load "../lib/setup.sh"
}
@test "run a command and capture its stdout" {
run -0 wait-for seq 1 3
assert_output - <<-EOT
1
2
3
EOT
}
@test "run a command and capture its stderr" {
rune -0 wait-for sh -c 'seq 1 3 >&2'
assert_stderr - <<-EOT
1
2
3
EOT
}
@test "run a command until a pattern is found in stdout" {
run -0 wait-for --out "1[12]0" seq 1 200
assert_line --index 0 "1"
assert_line --index -1 "110"
refute_line "111"
}
@test "run a command until a pattern is found in stderr" {
rune -0 wait-for --err "10" sh -c 'seq 1 20 >&2'
assert_stderr - <<-EOT
1
2
3
4
5
6
7
8
9
10
EOT
}
@test "run a command with timeout (no match)" {
# when the process is terminated without a match, it returns
# 256 - 15 (SIGTERM) = 241
rune -241 wait-for --timeout 0.1 --out "10" sh -c 'echo 1; sleep 3; echo 2'
assert_line 1
# there may be more, but we don't care
}
@test "run a command with timeout (match)" {
# when the process is terminated with a match, return code is 128
rune -128 wait-for --timeout .4 --out "2" sh -c 'echo 1; sleep .1; echo 2; echo 3; echo 4; sleep 10'
assert_output - <<-EOT
1
2
EOT
}

View file

@ -24,28 +24,22 @@ teardown() {
#----------
@test "crowdsec (usage)" {
rune -0 timeout 2s "${CROWDSEC}" -h
assert_stderr_line --regexp "Usage of .*:"
rune -0 timeout 2s "${CROWDSEC}" --help
assert_stderr_line --regexp "Usage of .*:"
rune -0 wait-for --out "Usage of " "${CROWDSEC}" -h
rune -0 wait-for --out "Usage of " "${CROWDSEC}" --help
}
@test "crowdsec (unknown flag)" {
rune -2 timeout 2s "${CROWDSEC}" --foobar
assert_stderr_line "flag provided but not defined: -foobar"
assert_stderr_line --regexp "Usage of .*"
rune -0 wait-for --err "flag provided but not defined: -foobar" "$CROWDSEC" --foobar
}
@test "crowdsec (unknown argument)" {
rune -2 timeout 2s "${CROWDSEC}" trololo
assert_stderr_line "argument provided but not defined: trololo"
assert_stderr_line --regexp "Usage of .*"
rune -0 wait-for --err "argument provided but not defined: trololo" "${CROWDSEC}" trololo
}
@test "crowdsec (no api and no agent)" {
rune -1 timeout 2s "${CROWDSEC}" -no-api -no-cs
assert_stderr_line --partial "You must run at least the API Server or crowdsec"
rune -0 wait-for \
--err "You must run at least the API Server or crowdsec" \
"${CROWDSEC}" -no-api -no-cs
}
@test "crowdsec - print error on exit" {
@ -57,18 +51,20 @@ teardown() {
@test "crowdsec - default logging configuration (empty/missing common section)" {
config_set '.common={}'
rune -124 timeout 2s "${CROWDSEC}"
rune -0 wait-for \
--err "Starting processing data" \
"${CROWDSEC}"
refute_output
assert_stderr --partial "Starting processing data"
config_set 'del(.common)'
rune -124 timeout 2s "${CROWDSEC}"
rune -0 wait-for \
--err "Starting processing data" \
"${CROWDSEC}"
refute_output
assert_stderr --partial "Starting processing data"
}
@test "CS_LAPI_SECRET not strong enough" {
CS_LAPI_SECRET=foo rune -1 timeout 2s "${CROWDSEC}"
CS_LAPI_SECRET=foo rune -1 wait-for "${CROWDSEC}"
assert_stderr --partial "api server init: unable to run local API: controller init: CS_LAPI_SECRET not strong enough"
}
@ -138,8 +134,8 @@ teardown() {
ACQUIS_YAML=$(config_get '.crowdsec_service.acquisition_path')
rm -f "$ACQUIS_YAML"
rune -1 timeout 2s "${CROWDSEC}"
assert_stderr_line --partial "acquis.yaml: no such file or directory"
rune -1 wait-for "${CROWDSEC}"
assert_stderr --partial "acquis.yaml: no such file or directory"
}
@test "crowdsec (error if acquisition_path is not defined and acquisition_dir is empty)" {
@ -151,7 +147,7 @@ teardown() {
rm -f "$ACQUIS_DIR"
config_set '.common.log_media="stdout"'
rune -1 timeout 2s "${CROWDSEC}"
rune -1 wait-for "${CROWDSEC}"
# check warning
assert_stderr --partial "no acquisition file found"
assert_stderr --partial "crowdsec init: while loading acquisition config: no datasource enabled"
@ -167,13 +163,15 @@ teardown() {
config_set '.crowdsec_service.acquisition_dir=""'
config_set '.common.log_media="stdout"'
rune -1 timeout 2s "${CROWDSEC}"
rune -1 wait-for "${CROWDSEC}"
# check warning
assert_stderr --partial "no acquisition_path or acquisition_dir specified"
assert_stderr --partial "crowdsec init: while loading acquisition config: no datasource enabled"
}
@test "crowdsec (no error if acquisition_path is empty string but acquisition_dir is not empty)" {
config_set '.common.log_media="stdout"'
ACQUIS_YAML=$(config_get '.crowdsec_service.acquisition_path')
config_set '.crowdsec_service.acquisition_path=""'
@ -181,13 +179,15 @@ teardown() {
mkdir -p "$ACQUIS_DIR"
mv "$ACQUIS_YAML" "$ACQUIS_DIR"/foo.yaml
rune -124 timeout 2s "${CROWDSEC}"
rune -0 wait-for \
--err "Starting processing data" \
"${CROWDSEC}"
# now, if foo.yaml is empty instead, there won't be valid datasources.
cat /dev/null >"$ACQUIS_DIR"/foo.yaml
rune -1 timeout 2s "${CROWDSEC}"
rune -1 wait-for "${CROWDSEC}"
assert_stderr --partial "crowdsec init: while loading acquisition config: no datasource enabled"
}
@ -212,9 +212,10 @@ teardown() {
type: syslog
EOT
rune -124 timeout 2s env PATH='' "${CROWDSEC}"
#shellcheck disable=SC2016
assert_stderr --partial 'datasource '\''journalctl'\'' is not available: exec: "journalctl": executable file not found in $PATH'
rune -0 wait-for \
--err 'datasource '\''journalctl'\'' is not available: exec: "journalctl": executable file not found in ' \
env PATH='' "${CROWDSEC}"
# if all datasources are disabled, crowdsec should exit
@ -222,7 +223,7 @@ teardown() {
rm -f "$ACQUIS_YAML"
config_set '.crowdsec_service.acquisition_path=""'
rune -1 timeout 2s env PATH='' "${CROWDSEC}"
rune -1 wait-for env PATH='' "${CROWDSEC}"
assert_stderr --partial "crowdsec init: while loading acquisition config: no datasource enabled"
}

View file

@ -24,21 +24,23 @@ teardown() {
#----------
@test "test without -no-api flag" {
rune -124 timeout 2s "${CROWDSEC}"
# from `man timeout`: If the command times out, and --preserve-status is not set, then exit with status 124.
config_set '.common.log_media="stdout"'
rune -0 wait-for \
--err "CrowdSec Local API listening" \
"${CROWDSEC}"
}
@test "crowdsec should not run without LAPI (-no-api flag)" {
# really needs 4 secs on slow boxes
rune -1 timeout 4s "${CROWDSEC}" -no-api
config_set '.common.log_media="stdout"'
rune -1 wait-for "${CROWDSEC}" -no-api
}
@test "crowdsec should not run without LAPI (no api.server in configuration file)" {
config_disable_lapi
config_log_stderr
# really needs 4 secs on slow boxes
rune -1 timeout 4s "${CROWDSEC}"
assert_stderr --partial "crowdsec local API is disabled"
rune -0 wait-for \
--err "crowdsec local API is disabled" \
"${CROWDSEC}"
}
@test "capi status shouldn't be ok without api.server" {

View file

@ -23,20 +23,25 @@ teardown() {
#----------
@test "with agent: test without -no-cs flag" {
rune -124 timeout 2s "${CROWDSEC}"
# from `man timeout`: If the command times out, and --preserve-status is not set, then exit with status 124.
config_set '.common.log_media="stdout"'
rune -0 wait-for \
--err "Starting processing data" \
"${CROWDSEC}"
}
@test "no agent: crowdsec LAPI should run (-no-cs flag)" {
rune -124 timeout 2s "${CROWDSEC}" -no-cs
config_set '.common.log_media="stdout"'
rune -0 wait-for \
--err "CrowdSec Local API listening" \
"${CROWDSEC}" -no-cs
}
@test "no agent: crowdsec LAPI should run (no crowdsec_service in configuration file)" {
config_disable_agent
config_log_stderr
rune -124 timeout 2s "${CROWDSEC}"
assert_stderr --partial "crowdsec agent is disabled"
rune -0 wait-for \
--err "crowdsec agent is disabled" \
"${CROWDSEC}"
}
@test "no agent: cscli config show" {

View file

@ -25,16 +25,17 @@ teardown() {
@test "without capi: crowdsec LAPI should run without capi (-no-capi flag)" {
config_set '.common.log_media="stdout"'
rune -124 timeout 1s "${CROWDSEC}" -no-capi
assert_stderr --partial "Communication with CrowdSec Central API disabled from args"
rune -0 wait-for \
--err "Communication with CrowdSec Central API disabled from args" \
"${CROWDSEC}" -no-capi
}
@test "without capi: crowdsec LAPI should still work" {
config_disable_capi
config_set '.common.log_media="stdout"'
rune -124 timeout 1s "${CROWDSEC}"
# from `man timeout`: If the command times out, and --preserve-status is not set, then exit with status 124.
assert_stderr --partial "push and pull to Central API disabled"
rune -0 wait-for \
--err "push and pull to Central API disabled" \
"${CROWDSEC}"
}
@test "without capi: cscli capi status -> fail" {
@ -47,10 +48,7 @@ teardown() {
@test "no capi: cscli config show" {
config_disable_capi
rune -0 cscli config show -o human
assert_output --partial "Global:"
assert_output --partial "cscli:"
assert_output --partial "Crowdsec:"
assert_output --partial "Local API Server:"
assert_output --regexp "Global:.*Crowdsec.*cscli:.*Local API Server:"
}
@test "no agent: cscli config backup" {

View file

@ -56,28 +56,28 @@ teardown() {
# disable the agent or we'll need to patch api client credentials too
rune -0 config_disable_agent
./instance-crowdsec start
rune -0 ./bin/wait-for-port -q 8080
rune -0 wait-for-port -q 8080
./instance-crowdsec stop
rune -1 ./bin/wait-for-port -q 8080
rune -1 wait-for-port -q 8080
echo "{'api':{'server':{'listen_uri':127.0.0.1:8083}}}" >"${CONFIG_YAML}.local"
./instance-crowdsec start
rune -0 ./bin/wait-for-port -q 8083
rune -1 ./bin/wait-for-port -q 8080
rune -0 wait-for-port -q 8083
rune -1 wait-for-port -q 8080
./instance-crowdsec stop
rm -f "${CONFIG_YAML}.local"
./instance-crowdsec start
rune -1 ./bin/wait-for-port -q 8083
rune -0 ./bin/wait-for-port -q 8080
rune -1 wait-for-port -q 8083
rune -0 wait-for-port -q 8080
}
@test "local_api_credentials.yaml.local" {
rune -0 config_disable_agent
echo "{'api':{'server':{'listen_uri':127.0.0.1:8083}}}" >"${CONFIG_YAML}.local"
./instance-crowdsec start
rune -0 ./bin/wait-for-port -q 8083
rune -0 wait-for-port -q 8083
rune -1 cscli decisions list
echo "{'url':'http://127.0.0.1:8083'}" >"${LOCAL_API_CREDENTIALS}.local"

View file

@ -18,6 +18,7 @@ setup() {
load "../lib/setup.sh"
load "../lib/bats-file/load.bash"
./instance-data load
config_set '.common.log_media="stdout"'
config_set '.api.server.capi_whitelists_path=strenv(CAPI_WHITELISTS_YAML)'
}
@ -28,38 +29,51 @@ teardown() {
#----------
@test "capi_whitelists: file missing" {
rune -1 timeout 1s "${CROWDSEC}"
assert_stderr --partial "capi whitelist file '$CAPI_WHITELISTS_YAML' does not exist"
rune -0 wait-for \
--err "capi whitelist file '$CAPI_WHITELISTS_YAML' does not exist" \
"${CROWDSEC}"
}
@test "capi_whitelists: error on open" {
echo > "$CAPI_WHITELISTS_YAML"
chmod 000 "$CAPI_WHITELISTS_YAML"
rune -1 timeout 1s "${CROWDSEC}"
assert_stderr --partial "while opening capi whitelist file: open $CAPI_WHITELISTS_YAML: permission denied"
if is_package_testing; then
rune -0 wait-for \
--err "while parsing capi whitelist file .*: empty file" \
"${CROWDSEC}"
else
rune -0 wait-for \
--err "while opening capi whitelist file: open $CAPI_WHITELISTS_YAML: permission denied" \
"${CROWDSEC}"
fi
}
@test "capi_whitelists: empty file" {
echo > "$CAPI_WHITELISTS_YAML"
rune -1 timeout 1s "${CROWDSEC}"
assert_stderr --partial "while parsing capi whitelist file '$CAPI_WHITELISTS_YAML': empty file"
rune -0 wait-for \
--err "while parsing capi whitelist file '$CAPI_WHITELISTS_YAML': empty file" \
"${CROWDSEC}"
}
@test "capi_whitelists: empty lists" {
echo '{"ips": [], "cidrs": []}' > "$CAPI_WHITELISTS_YAML"
rune -124 timeout 1s "${CROWDSEC}"
rune -0 wait-for \
--err "Starting processing data" \
"${CROWDSEC}"
}
@test "capi_whitelists: bad ip" {
echo '{"ips": ["blahblah"], "cidrs": []}' > "$CAPI_WHITELISTS_YAML"
rune -1 timeout 1s "${CROWDSEC}"
assert_stderr --partial "while parsing capi whitelist file '$CAPI_WHITELISTS_YAML': invalid IP address: blahblah"
rune -0 wait-for \
--err "while parsing capi whitelist file '$CAPI_WHITELISTS_YAML': invalid IP address: blahblah" \
"${CROWDSEC}"
}
@test "capi_whitelists: bad cidr" {
echo '{"ips": [], "cidrs": ["blahblah"]}' > "$CAPI_WHITELISTS_YAML"
rune -1 timeout 1s "${CROWDSEC}"
assert_stderr --partial "while parsing capi whitelist file '$CAPI_WHITELISTS_YAML': invalid CIDR address: blahblah"
rune -0 wait-for \
--err "while parsing capi whitelist file '$CAPI_WHITELISTS_YAML': invalid CIDR address: blahblah" \
"${CROWDSEC}"
}
@test "capi_whitelists: file with ip and cidr values" {

View file

@ -72,16 +72,32 @@ teardown() {
}
@test "cscli hub update" {
#XXX: todo
:
rm -f "$INDEX_PATH"
rune -0 cscli hub update
assert_stderr --partial "Wrote index to $INDEX_PATH"
rune -0 cscli hub update
assert_stderr --partial "hub index is up to date"
}
@test "cscli hub upgrade" {
#XXX: todo
:
}
rune -0 cscli hub upgrade
assert_stderr --partial "Upgrading parsers"
assert_stderr --partial "Upgraded 0 parsers"
assert_stderr --partial "Upgrading postoverflows"
assert_stderr --partial "Upgraded 0 postoverflows"
assert_stderr --partial "Upgrading scenarios"
assert_stderr --partial "Upgraded 0 scenarios"
assert_stderr --partial "Upgrading collections"
assert_stderr --partial "Upgraded 0 collections"
@test "cscli hub upgrade --force" {
#XXX: todo
:
rune -0 cscli parsers install crowdsecurity/syslog-logs
rune -0 cscli hub upgrade
assert_stderr --partial "crowdsecurity/syslog-logs: up-to-date"
rune -0 cscli hub upgrade --force
assert_stderr --partial "crowdsecurity/syslog-logs: overwrite"
assert_stderr --partial "crowdsecurity/syslog-logs: updated"
assert_stderr --partial "Upgraded 1 parsers"
# this is used by the cron script to know if the hub was updated
assert_output --partial "updated crowdsecurity/syslog-logs"
}

View file

@ -187,7 +187,6 @@ teardown() {
# one item, json
rune -0 cscli collections inspect crowdsecurity/sshd -o json
rune -0 jq -c '[.type, .name, .author, .path, .installed]' <(output)
# XXX: .installed is missing -- not false
assert_json '["collections","crowdsecurity/sshd","crowdsecurity","collections/crowdsecurity/sshd.yaml",false]'
# one item, raw
@ -220,7 +219,7 @@ teardown() {
rune -0 cscli collections inspect crowdsecurity/sshd crowdsecurity/smb -o raw
assert_output --partial 'crowdsecurity/sshd'
assert_output --partial 'crowdsecurity/smb'
run -1 grep -c 'Current metrics:' <(output)
rune -1 grep -c 'Current metrics:' <(output)
assert_output "0"
}
@ -230,16 +229,23 @@ teardown() {
rune -1 cscli collections remove blahblah/blahblah
assert_stderr --partial "can't find 'blahblah/blahblah' in collections"
rune -0 cscli collections remove crowdsecurity/sshd --purge
rune -0 cscli collections remove crowdsecurity/sshd
assert_stderr --partial 'removing crowdsecurity/sshd: not downloaded -- no removal required'
rune -0 cscli collections install crowdsecurity/sshd --download-only
rune -0 cscli collections remove crowdsecurity/sshd
assert_stderr --partial 'removing crowdsecurity/sshd: already uninstalled'
assert_stderr --partial 'removing crowdsecurity/sshd: not installed -- no need to remove'
rune -0 cscli collections install crowdsecurity/sshd
rune -0 cscli collections remove crowdsecurity/sshd
assert_stderr --partial 'Removed crowdsecurity/sshd'
rune -0 cscli collections remove crowdsecurity/sshd --purge
assert_stderr --partial 'Removed source file [crowdsecurity/sshd]'
rune -0 cscli collections remove crowdsecurity/sshd
assert_stderr --partial 'removing crowdsecurity/sshd: not installed -- no need to remove'
rune -0 cscli collections remove crowdsecurity/sshd --purge
assert_stderr --partial 'removing crowdsecurity/sshd: not downloaded -- no need to remove'
# install, then remove, check files
rune -0 cscli collections install crowdsecurity/sshd
assert_file_exists "$CONFIG_DIR/collections/sshd.yaml"

View file

@ -70,6 +70,27 @@ teardown() {
rune -1 cscli collections inspect crowdsecurity/sshd --no-metrics -o json
# XXX: we are on the verbose side here...
rune -0 jq -r ".msg" <(stderr)
assert_output "failed to read Hub index: failed to sync items: failed to scan $CONFIG_DIR: while syncing collections sshd.yaml: 1.2.3.4: Invalid Semantic Version. Run 'sudo cscli hub update' to download the index again"
assert_output --regexp "failed to read Hub index: failed to sync items: failed to scan .*: while syncing collections sshd.yaml: 1.2.3.4: Invalid Semantic Version. Run 'sudo cscli hub update' to download the index again"
}
@test "removing or purging an item already removed by hand" {
rune -0 cscli parsers install crowdsecurity/syslog-logs
rune -0 cscli parsers inspect crowdsecurity/syslog-logs -o json
rune -0 jq -r '.local_path' <(output)
rune -0 rm "$(output)"
rune -0 cscli parsers remove crowdsecurity/syslog-logs --debug
assert_stderr --partial "Removed crowdsecurity/syslog-logs"
rune -0 cscli parsers inspect crowdsecurity/syslog-logs -o json
rune -0 jq -r '.path' <(output)
rune -0 rm "$HUB_DIR/$(output)"
rune -0 cscli parsers remove crowdsecurity/syslog-logs --purge
assert_stderr --partial "removing crowdsecurity/syslog-logs: not downloaded -- no need to remove"
rune -0 cscli parsers remove crowdsecurity/linux --all --error --purge --force
rune -0 cscli collections remove crowdsecurity/linux --all --error --purge --force
refute_output
refute_stderr
}

View file

@ -32,6 +32,8 @@ teardown() {
#----------
@test "cscli parsers list" {
hub_purge_all
# no items
rune -0 cscli parsers list
assert_output --partial "PARSERS"
@ -221,7 +223,7 @@ teardown() {
rune -0 cscli parsers inspect crowdsecurity/sshd-logs crowdsecurity/whitelists -o raw
assert_output --partial 'crowdsecurity/sshd-logs'
assert_output --partial 'crowdsecurity/whitelists'
run -1 grep -c 'Current metrics:' <(output)
rune -1 grep -c 'Current metrics:' <(output)
assert_output "0"
}
@ -231,16 +233,23 @@ teardown() {
rune -1 cscli parsers remove blahblah/blahblah
assert_stderr --partial "can't find 'blahblah/blahblah' in parsers"
rune -0 cscli parsers remove crowdsecurity/whitelists --purge
rune -0 cscli parsers remove crowdsecurity/whitelists
assert_stderr --partial 'removing crowdsecurity/whitelists: not downloaded -- no removal required'
rune -0 cscli parsers install crowdsecurity/whitelists --download-only
rune -0 cscli parsers remove crowdsecurity/whitelists
assert_stderr --partial 'removing crowdsecurity/whitelists: already uninstalled'
assert_stderr --partial "removing crowdsecurity/whitelists: not installed -- no need to remove"
rune -0 cscli parsers install crowdsecurity/whitelists
rune -0 cscli parsers remove crowdsecurity/whitelists
assert_stderr --partial "Removed crowdsecurity/whitelists"
rune -0 cscli parsers remove crowdsecurity/whitelists --purge
assert_stderr --partial 'Removed source file [crowdsecurity/whitelists]'
rune -0 cscli parsers remove crowdsecurity/whitelists
assert_stderr --partial "removing crowdsecurity/whitelists: not installed -- no need to remove"
rune -0 cscli parsers remove crowdsecurity/whitelists --purge
assert_stderr --partial 'removing crowdsecurity/whitelists: not downloaded -- no need to remove'
# install, then remove, check files
rune -0 cscli parsers install crowdsecurity/whitelists
assert_file_exists "$CONFIG_DIR/parsers/s02-enrich/whitelists.yaml"

View file

@ -191,7 +191,6 @@ teardown() {
# one item, json
rune -0 cscli postoverflows inspect crowdsecurity/rdns -o json
rune -0 jq -c '[.type, .stage, .name, .author, .path, .installed]' <(output)
# XXX: .installed is missing -- not false
assert_json '["postoverflows","s00-enrich","crowdsecurity/rdns","crowdsecurity","postoverflows/s00-enrich/crowdsecurity/rdns.yaml",false]'
# one item, raw
@ -235,16 +234,23 @@ teardown() {
rune -1 cscli postoverflows remove blahblah/blahblah
assert_stderr --partial "can't find 'blahblah/blahblah' in postoverflows"
rune -0 cscli postoverflows remove crowdsecurity/rdns --purge
rune -0 cscli postoverflows remove crowdsecurity/rdns
assert_stderr --partial 'removing crowdsecurity/rdns: not downloaded -- no removal required'
rune -0 cscli postoverflows install crowdsecurity/rdns --download-only
rune -0 cscli postoverflows remove crowdsecurity/rdns
assert_stderr --partial 'removing crowdsecurity/rdns: already uninstalled'
assert_stderr --partial "removing crowdsecurity/rdns: not installed -- no need to remove"
rune -0 cscli postoverflows install crowdsecurity/rdns
rune -0 cscli postoverflows remove crowdsecurity/rdns
assert_stderr --partial 'Removed crowdsecurity/rdns'
rune -0 cscli postoverflows remove crowdsecurity/rdns --purge
assert_stderr --partial 'Removed source file [crowdsecurity/rdns]'
rune -0 cscli postoverflows remove crowdsecurity/rdns
assert_stderr --partial 'removing crowdsecurity/rdns: not installed -- no need to remove'
rune -0 cscli postoverflows remove crowdsecurity/rdns --purge
assert_stderr --partial 'removing crowdsecurity/rdns: not downloaded -- no need to remove'
# install, then remove, check files
rune -0 cscli postoverflows install crowdsecurity/rdns
assert_file_exists "$CONFIG_DIR/postoverflows/s00-enrich/rdns.yaml"

View file

@ -232,16 +232,23 @@ teardown() {
rune -1 cscli scenarios remove blahblah/blahblah
assert_stderr --partial "can't find 'blahblah/blahblah' in scenarios"
rune -0 cscli scenarios remove crowdsecurity/ssh-bf --purge
rune -0 cscli scenarios remove crowdsecurity/ssh-bf
assert_stderr --partial 'removing crowdsecurity/ssh-bf: not downloaded -- no removal required'
rune -0 cscli scenarios install crowdsecurity/ssh-bf --download-only
rune -0 cscli scenarios remove crowdsecurity/ssh-bf
assert_stderr --partial 'removing crowdsecurity/ssh-bf: already uninstalled'
assert_stderr --partial "removing crowdsecurity/ssh-bf: not installed -- no need to remove"
rune -0 cscli scenarios install crowdsecurity/ssh-bf
rune -0 cscli scenarios remove crowdsecurity/ssh-bf
assert_stderr --partial "Removed crowdsecurity/ssh-bf"
rune -0 cscli scenarios remove crowdsecurity/ssh-bf --purge
assert_stderr --partial 'Removed source file [crowdsecurity/ssh-bf]'
rune -0 cscli scenarios remove crowdsecurity/ssh-bf
assert_stderr --partial "removing crowdsecurity/ssh-bf: not installed -- no need to remove"
rune -0 cscli scenarios remove crowdsecurity/ssh-bf --purge
assert_stderr --partial 'removing crowdsecurity/ssh-bf: not downloaded -- no need to remove'
# install, then remove, check files
rune -0 cscli scenarios install crowdsecurity/ssh-bf
assert_file_exists "$CONFIG_DIR/scenarios/ssh-bf.yaml"

View file

@ -78,15 +78,17 @@ teardown() {
@test "missing key_file" {
config_set '.api.server.tls.key_file=""'
rune -1 timeout 2s "${CROWDSEC}"
assert_stderr --partial "missing TLS key file"
rune -0 wait-for \
--err "missing TLS key file" \
"${CROWDSEC}"
}
@test "missing cert_file" {
config_set '.api.server.tls.cert_file=""'
rune -1 timeout 2s "${CROWDSEC}"
assert_stderr --partial "missing TLS cert file"
rune -0 wait-for \
--err "missing TLS cert file" \
"${CROWDSEC}"
}
@test "invalid OU for agent" {

View file

@ -27,7 +27,7 @@ setup() {
teardown() {
./instance-crowdsec stop
rm -f "${PLUGIN_DIR}"/badname
chmod go-w "${PLUGIN_DIR}"/notification-http
chmod go-w "${PLUGIN_DIR}"/notification-http || true
}
#----------
@ -35,36 +35,41 @@ teardown() {
@test "misconfigured plugin, only user is empty" {
config_set '.plugin_config.user="" | .plugin_config.group="nogroup"'
config_set "${PROFILES_PATH}" '.notifications=["http_default"]'
rune -1 timeout 2s "${CROWDSEC}"
assert_stderr --partial "api server init: unable to run plugin broker: while loading plugin: while getting process attributes: both plugin user and group must be set"
rune -0 wait-for \
--err "api server init: unable to run plugin broker: while loading plugin: while getting process attributes: both plugin user and group must be set" \
"${CROWDSEC}"
}
@test "misconfigured plugin, only group is empty" {
config_set '(.plugin_config.user="nobody") | (.plugin_config.group="")'
config_set "${PROFILES_PATH}" '.notifications=["http_default"]'
rune -1 timeout 2s "${CROWDSEC}"
assert_stderr --partial "api server init: unable to run plugin broker: while loading plugin: while getting process attributes: both plugin user and group must be set"
rune -0 wait-for \
--err "api server init: unable to run plugin broker: while loading plugin: while getting process attributes: both plugin user and group must be set" \
"${CROWDSEC}"
}
@test "misconfigured plugin, user does not exist" {
config_set '(.plugin_config.user="userdoesnotexist") | (.plugin_config.group="groupdoesnotexist")'
config_set "${PROFILES_PATH}" '.notifications=["http_default"]'
rune -1 timeout 2s "${CROWDSEC}"
assert_stderr --partial "api server init: unable to run plugin broker: while loading plugin: while getting process attributes: user: unknown user userdoesnotexist"
rune -0 wait-for \
--err "api server init: unable to run plugin broker: while loading plugin: while getting process attributes: user: unknown user userdoesnotexist" \
"${CROWDSEC}"
}
@test "misconfigured plugin, group does not exist" {
config_set '(.plugin_config.user=strenv(USER)) | (.plugin_config.group="groupdoesnotexist")'
config_set "${PROFILES_PATH}" '.notifications=["http_default"]'
rune -1 timeout 2s "${CROWDSEC}"
assert_stderr --partial "api server init: unable to run plugin broker: while loading plugin: while getting process attributes: group: unknown group groupdoesnotexist"
rune -0 wait-for \
--err "api server init: unable to run plugin broker: while loading plugin: while getting process attributes: group: unknown group groupdoesnotexist" \
"${CROWDSEC}"
}
@test "bad plugin name" {
config_set "${PROFILES_PATH}" '.notifications=["http_default"]'
cp "${PLUGIN_DIR}"/notification-http "${PLUGIN_DIR}"/badname
rune -1 timeout 2s "${CROWDSEC}"
assert_stderr --partial "api server init: unable to run plugin broker: while loading plugin: plugin name ${PLUGIN_DIR}/badname is invalid. Name should be like {type-name}"
rune -0 wait-for \
--err "api server init: unable to run plugin broker: while loading plugin: plugin name ${PLUGIN_DIR}/badname is invalid. Name should be like {type-name}" \
"${CROWDSEC}"
}
@test "duplicate notification config" {
@ -75,48 +80,55 @@ teardown() {
config_set "${PROFILES_PATH}" '.notifications=["slack_default"]'
# the slack plugin may fail or not, but we just need the logs
config_set '.common.log_media="stdout"'
rune timeout 2s "${CROWDSEC}"
assert_stderr --partial "notification 'email_default' is defined multiple times"
rune wait-for \
--err "notification 'email_default' is defined multiple times" \
"${CROWDSEC}"
}
@test "bad plugin permission (group writable)" {
config_set "${PROFILES_PATH}" '.notifications=["http_default"]'
chmod g+w "${PLUGIN_DIR}"/notification-http
rune -1 timeout 2s "${CROWDSEC}"
assert_stderr --partial "api server init: unable to run plugin broker: while loading plugin: plugin at ${PLUGIN_DIR}/notification-http is group writable, group writable plugins are invalid"
rune -0 wait-for \
--err "api server init: unable to run plugin broker: while loading plugin: plugin at ${PLUGIN_DIR}/notification-http is group writable, group writable plugins are invalid" \
"${CROWDSEC}"
}
@test "bad plugin permission (world writable)" {
config_set "${PROFILES_PATH}" '.notifications=["http_default"]'
chmod o+w "${PLUGIN_DIR}"/notification-http
rune -1 timeout 2s "${CROWDSEC}"
assert_stderr --partial "api server init: unable to run plugin broker: while loading plugin: plugin at ${PLUGIN_DIR}/notification-http is world writable, world writable plugins are invalid"
rune -0 wait-for \
--err "api server init: unable to run plugin broker: while loading plugin: plugin at ${PLUGIN_DIR}/notification-http is world writable, world writable plugins are invalid" \
"${CROWDSEC}"
}
@test "config.yaml: missing .plugin_config section" {
config_set 'del(.plugin_config)'
config_set "${PROFILES_PATH}" '.notifications=["http_default"]'
rune -1 timeout 2s "${CROWDSEC}"
assert_stderr --partial "api server init: plugins are enabled, but the plugin_config section is missing in the configuration"
rune -0 wait-for \
--err "api server init: plugins are enabled, but the plugin_config section is missing in the configuration" \
"${CROWDSEC}"
}
@test "config.yaml: missing config_paths.notification_dir" {
config_set 'del(.config_paths.notification_dir)'
config_set "${PROFILES_PATH}" '.notifications=["http_default"]'
rune -1 timeout 2s "${CROWDSEC}"
assert_stderr --partial "api server init: plugins are enabled, but config_paths.notification_dir is not defined"
rune -0 wait-for \
--err "api server init: plugins are enabled, but config_paths.notification_dir is not defined" \
"${CROWDSEC}"
}
@test "config.yaml: missing config_paths.plugin_dir" {
config_set 'del(.config_paths.plugin_dir)'
config_set "${PROFILES_PATH}" '.notifications=["http_default"]'
rune -1 timeout 2s "${CROWDSEC}"
assert_stderr --partial "api server init: plugins are enabled, but config_paths.plugin_dir is not defined"
rune -0 wait-for \
--err "api server init: plugins are enabled, but config_paths.plugin_dir is not defined" \
"${CROWDSEC}"
}
@test "unable to run plugin broker: while reading plugin config" {
config_set '.config_paths.notification_dir="/this/path/does/not/exist"'
config_set "${PROFILES_PATH}" '.notifications=["http_default"]'
rune -1 timeout 2s "${CROWDSEC}"
assert_stderr --partial "api server init: unable to run plugin broker: while loading plugin config: open /this/path/does/not/exist: no such file or directory"
rune -0 wait-for \
--err "api server init: unable to run plugin broker: while loading plugin config: open /this/path/does/not/exist: no such file or directory" \
"${CROWDSEC}"
}

116
test/bin/wait-for Executable file
View file

@ -0,0 +1,116 @@
#!/usr/bin/env python3
import asyncio
import argparse
import os
import re
import signal
import sys
DEFAULT_TIMEOUT = 30
# TODO: signal handler to terminate spawned process group when wait-for is killed
# TODO: better return codes esp. when matches are found
# TODO: multiple patterns (multiple out, err, both)
# TODO: print unmatched patterns
async def terminate(p):
# Terminate the process group (shell, crowdsec plugins)
try:
os.killpg(os.getpgid(p.pid), signal.SIGTERM)
except ProcessLookupError:
pass
async def monitor(cmd, args, want_out, want_err, timeout):
"""Monitor a process and terminate it if a pattern is matched in stdout or stderr.
Args:
cmd: The command to run.
args: A list of arguments to pass to the command.
stdout: A regular expression pattern to search for in stdout.
stderr: A regular expression pattern to search for in stderr.
timeout: The maximum number of seconds to wait for the process to terminate.
Returns:
The exit code of the process.
"""
status = None
async def read_stream(p, stream, outstream, pattern):
nonlocal status
if stream is None:
return
while True:
line = await stream.readline()
if line:
line = line.decode('utf-8')
outstream.write(line)
if pattern and pattern.search(line):
await terminate(process)
# this is nasty.
# if we timeout, we want to return a different exit code
# in case of a match, so that the caller can tell
# if the application was still running.
# XXX: still not good for match found, but return code != 0
if timeout != DEFAULT_TIMEOUT:
status = 128
else:
status = 0
break
else:
break
process = await asyncio.create_subprocess_exec(
cmd,
*args,
# capture stdout
stdout=asyncio.subprocess.PIPE,
# capture stderr
stderr=asyncio.subprocess.PIPE,
# disable buffering
bufsize=0,
# create a new process group
# (required to kill child processes when cmd is a shell)
preexec_fn=os.setsid)
out_regex = re.compile(want_out) if want_out else None
err_regex = re.compile(want_err) if want_err else None
# Apply a timeout
try:
await asyncio.wait_for(
asyncio.wait([
asyncio.create_task(process.wait()),
asyncio.create_task(read_stream(process, process.stdout, sys.stdout, out_regex)),
asyncio.create_task(read_stream(process, process.stderr, sys.stderr, err_regex))
]), timeout)
if status is None:
status = process.returncode
except asyncio.TimeoutError:
await terminate(process)
status = 241
# Return the same exit code, stdout and stderr as the spawned process
return status
async def main():
parser = argparse.ArgumentParser(
description='Monitor a process and terminate it if a pattern is matched in stdout or stderr.')
parser.add_argument('cmd', help='The command to run.')
parser.add_argument('args', nargs=argparse.REMAINDER, help='A list of arguments to pass to the command.')
parser.add_argument('--out', default='', help='A regular expression pattern to search for in stdout.')
parser.add_argument('--err', default='', help='A regular expression pattern to search for in stderr.')
parser.add_argument('--timeout', type=float, default=DEFAULT_TIMEOUT)
args = parser.parse_args()
exit_code = await monitor(args.cmd, args.args, args.out, args.err, args.timeout)
sys.exit(exit_code)
if __name__ == '__main__':
asyncio.run(main())

View file

@ -38,6 +38,8 @@ DATA_DIR="${LOCAL_DIR}/${REL_DATA_DIR}"
export DATA_DIR
CONFIG_DIR="${LOCAL_DIR}/${REL_CONFIG_DIR}"
export CONFIG_DIR
HUB_DIR="${CONFIG_DIR}/hub"
export HUB_DIR
if [[ $(uname) == "OpenBSD" ]]; then
TAR=gtar
@ -52,6 +54,51 @@ remove_init_data() {
# we need a separate function for initializing config when testing package
# because we want to test the configuration as well
preload_hub_items() {
# pre-download everything but don't install anything
# each test can install what it needs
echo "Purging existing hub..."
"$CSCLI" parsers delete --all --error --purge --force
"$CSCLI" scenarios delete --all --error --purge --force
"$CSCLI" postoverflows delete --all --error --purge --force
"$CSCLI" collections delete --all --error --purge --force
echo "Pre-downloading hub content..."
#shellcheck disable=SC2046
"$CSCLI" collections install \
$("$CSCLI" collections list -a -o json | jq -r '.collections[].name') \
--download-only \
--error
#shellcheck disable=SC2046
"$CSCLI" parsers install \
$("$CSCLI" parsers list -a -o json | jq -r '.parsers[].name') \
--download-only \
--error
#shellcheck disable=SC2046
"$CSCLI" scenarios install \
$("$CSCLI" scenarios list -a -o json | jq -r '.scenarios[].name') \
--download-only \
--error
#shellcheck disable=SC2046
"$CSCLI" postoverflows install \
$("$CSCLI" postoverflows list -a -o json | jq -r '.postoverflows[].name') \
--download-only \
--error
# XXX: download-only works only for collections, not for parsers, scenarios, postoverflows.
# so we have to delete the links manually, and leave the downloaded files in place
"$CSCLI" parsers delete --all --error
"$CSCLI" scenarios delete --all --error
"$CSCLI" postoverflows delete --all --error
}
make_init_data() {
./bin/assert-crowdsec-not-running || die "Cannot create fixture data."
@ -61,6 +108,8 @@ make_init_data() {
# when installed packages are always using sqlite, so no need to regenerate
# local credz for sqlite
preload_hub_items
[[ "${DB_BACKEND}" == "sqlite" ]] || ${CSCLI} machines add --auto
mkdir -p "$LOCAL_INIT_DIR"

View file

@ -105,31 +105,38 @@ preload_hub_items() {
# pre-download everything but don't install anything
# each test can install what it needs
echo "Downloading collections..."
echo "Purging existing hub..."
"$CSCLI" parsers delete --all --error --purge --force
"$CSCLI" scenarios delete --all --error --purge --force
"$CSCLI" postoverflows delete --all --error --purge --force
"$CSCLI" collections delete --all --error --purge --force
echo "Pre-downloading hub content..."
#shellcheck disable=SC2046
"$CSCLI" collections install \
$("$CSCLI" collections list -a -o json | jq -r '.collections[].name') \
--download-only \
--warning
--error
#shellcheck disable=SC2046
"$CSCLI" parsers install \
$("$CSCLI" parsers list -a -o json | jq -r '.parsers[].name') \
--download-only \
--warning
--error
#shellcheck disable=SC2046
"$CSCLI" scenarios install \
$("$CSCLI" scenarios list -a -o json | jq -r '.scenarios[].name') \
--download-only \
--warning
--error
#shellcheck disable=SC2046
"$CSCLI" postoverflows install \
$("$CSCLI" postoverflows list -a -o json | jq -r '.postoverflows[].name') \
--download-only \
--warning
--error
# XXX: download-only works only for collections, not for parsers, scenarios, postoverflows.
# so we have to delete the links manually, and leave the downloaded files in place

View file

@ -20,6 +20,7 @@ eval "$(debug)"
# Allow tests to use relative paths for helper scripts.
# shellcheck disable=SC2164
cd "${TEST_DIR}"
export PATH="${TEST_DIR}/bin:${PATH}"
# complain if there's a crowdsec running system-wide or leftover from a previous test
./bin/assert-crowdsec-not-running