From be18fea136e590bb6c9bb08268ef19cbce5b2ca8 Mon Sep 17 00:00:00 2001 From: mmetc <92726601+mmetc@users.noreply.github.com> Date: Tue, 21 Feb 2023 22:12:08 +0100 Subject: [PATCH] Propagate taints to top collections (fix #2064) (#2066) --- docker/test/tests/test_hub_collections.py | 60 +++++++++++++ pkg/cwhub/loader.go | 3 + tests/bats/20_collections.bats | 105 ++++++++++++++-------- 3 files changed, 131 insertions(+), 37 deletions(-) diff --git a/docker/test/tests/test_hub_collections.py b/docker/test/tests/test_hub_collections.py index e7238006a..8b023e692 100644 --- a/docker/test/tests/test_hub_collections.py +++ b/docker/test/tests/test_hub_collections.py @@ -6,10 +6,14 @@ Test collection management from http import HTTPStatus import json +import os +import pwd +import time from pytest_cs import wait_for_log, wait_for_http import pytest +import yaml pytestmark = pytest.mark.docker @@ -75,3 +79,59 @@ def test_install_and_disable_collection(crowdsec, flavor): logs = cont.logs().decode().splitlines() # check that there was no attempt to install 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 +def test_taint_bubble_up(crowdsec, tmp_path_factory, flavor): + coll = 'crowdsecurity/nginx' + env = { + 'COLLECTIONS': f'{coll}' + } + + hub = tmp_path_factory.mktemp("hub") + volumes = { + hub: {'bind': '/etc/crowdsec/hub', 'mode': 'rw'} + } + + with crowdsec(flavor=flavor, environment=env, volumes=volumes) as cont: + wait_for_http(cont, 8080, '/health', want_status=HTTPStatus.OK) + res = cont.exec_run('cscli collections list -o json') + assert res.exit_code == 0 + j = json.loads(res.output) + items = {c['name']: c for c in j['collections']} + # implicit check for tainted=False + assert items[coll]['status'] == 'enabled' + wait_for_log(cont, [ + f'*Enabled collections : {coll}*', + ]) + + # change file permissions to allow edit + current_uid = pwd.getpwuid(os.getuid()).pw_uid + res = cont.exec_run(f'chown -R {current_uid} /etc/crowdsec/hub') + assert res.exit_code == 0 + + scenario = 'crowdsecurity/http-crawl-non_statics' + scenario_file = hub / f'scenarios/{scenario}.yaml' + + with open(scenario_file) as f: + yml = yaml.safe_load(f) + + yml['description'] += ' (tainted)' + # won't be able to read it back because description is taken from the index + + with open(scenario_file, 'w') as f: + yaml.dump(yml, f) + + with crowdsec(flavor=flavor, environment=env, volumes=volumes) as cont: + wait_for_http(cont, 8080, '/health', want_status=HTTPStatus.OK) + res = cont.exec_run(f'cscli scenarios inspect {scenario} -o json') + assert res.exit_code == 0 + j = json.loads(res.output) + assert j['tainted'] is True + + res = cont.exec_run('cscli collections list -o json') + assert res.exit_code == 0 + j = json.loads(res.output) + items = {c['name']: c for c in j['collections']} + assert items['crowdsecurity/nginx']['status'] == 'enabled,tainted' + assert items['crowdsecurity/base-http-scenarios']['status'] == 'enabled,tainted' diff --git a/pkg/cwhub/loader.go b/pkg/cwhub/loader.go index 496ef42a5..aa63510a8 100644 --- a/pkg/cwhub/loader.go +++ b/pkg/cwhub/loader.go @@ -267,6 +267,9 @@ func CollecDepsCheck(v *Item) error { if val.Type == COLLECTIONS { log.Tracef("collec, recurse.") if err := CollecDepsCheck(&val); err != nil { + if val.Tainted { + v.Tainted = true + } return fmt.Errorf("sub collection %s is broken : %s", val.Name, err) } hubIdx[ptrtype][p] = val diff --git a/tests/bats/20_collections.bats b/tests/bats/20_collections.bats index 842a637ea..7a8efa469 100644 --- a/tests/bats/20_collections.bats +++ b/tests/bats/20_collections.bats @@ -28,87 +28,118 @@ teardown() { } @test "there are 2 collections (linux and sshd)" { - run -0 --separate-stderr cscli collections list -o json - run -0 jq '.collections | length' <(output) + rune -0 cscli collections list -o json + rune -0 jq '.collections | length' <(output) assert_output 2 } @test "can install a collection (as a regular user) and remove it" { # collection is not installed - run -0 --separate-stderr cscli collections list -o json - run -0 jq -r '.collections[].name' <(output) + rune -0 cscli collections list -o json + rune -0 jq -r '.collections[].name' <(output) refute_line "crowdsecurity/mysql" # we install it - run -0 --separate-stderr cscli collections install crowdsecurity/mysql -o human + rune -0 cscli collections install crowdsecurity/mysql -o human assert_stderr --partial "Enabled crowdsecurity/mysql" # it has been installed - run -0 --separate-stderr cscli collections list -o json - run -0 jq -r '.collections[].name' <(output) + rune -0 cscli collections list -o json + rune -0 jq -r '.collections[].name' <(output) assert_line "crowdsecurity/mysql" # we install it - run -0 cscli collections remove crowdsecurity/mysql -o human - assert_output --partial "Removed symlink [crowdsecurity/mysql]" + rune -0 cscli collections remove crowdsecurity/mysql -o human + assert_stderr --partial "Removed symlink [crowdsecurity/mysql]" # it has been removed - run -0 --separate-stderr cscli collections list -o json - run -0 jq -r '.collections[].name' <(output) + rune -0 cscli collections list -o json + rune -0 jq -r '.collections[].name' <(output) refute_line "crowdsecurity/mysql" } @test "must use --force to remove a collection that belongs to another, which becomes tainted" { # we expect no error since we may have multiple collections, some removed and some not - run -0 --separate-stderr cscli collections remove crowdsecurity/sshd + rune -0 cscli collections remove crowdsecurity/sshd assert_stderr --partial "crowdsecurity/sshd belongs to other collections" assert_stderr --partial "[crowdsecurity/linux]" - run -0 --separate-stderr cscli collections remove crowdsecurity/sshd --force + rune -0 cscli collections remove crowdsecurity/sshd --force assert_stderr --partial "Removed symlink [crowdsecurity/sshd]" - run -0 --separate-stderr cscli collections inspect crowdsecurity/linux -o json - run -0 jq -r '.tainted' <(output) + rune -0 cscli collections inspect crowdsecurity/linux -o json + rune -0 jq -r '.tainted' <(output) assert_output "true" } @test "can remove a collection" { - run -0 cscli collections remove crowdsecurity/linux - assert_output --partial "Removed" - assert_output --regexp ".*for the new configuration to be effective." - run -0 cscli collections inspect crowdsecurity/linux -o human + rune -0 cscli collections remove crowdsecurity/linux + assert_stderr --partial "Removed" + assert_stderr --regexp ".*for the new configuration to be effective." + rune -0 cscli collections inspect crowdsecurity/linux -o human assert_line 'installed: false' } @test "collections delete is an alias for collections remove" { - run -0 cscli collections delete crowdsecurity/linux - assert_output --partial "Removed" - assert_output --regexp ".*for the new configuration to be effective." + rune -0 cscli collections delete crowdsecurity/linux + assert_stderr --partial "Removed" + assert_stderr --regexp ".*for the new configuration to be effective." } @test "removing a collection that does not exist is noop" { - run -0 cscli collections remove crowdsecurity/apache2 - refute_output --partial "Removed" - assert_output --regexp ".*for the new configuration to be effective." + rune -0 cscli collections remove crowdsecurity/apache2 + refute_stderr --partial "Removed" + assert_stderr --regexp ".*for the new configuration to be effective." } @test "can remove a removed collection" { - run -0 cscli collections install crowdsecurity/mysql - run -0 cscli collections remove crowdsecurity/mysql - assert_output --partial "Removed" - run -0 cscli collections remove crowdsecurity/mysql - refute_output --partial "Removed" + rune -0 cscli collections install crowdsecurity/mysql + rune -0 cscli collections remove crowdsecurity/mysql + assert_stderr --partial "Removed" + rune -0 cscli collections remove crowdsecurity/mysql + refute_stderr --partial "Removed" } @test "can remove all collections" { # we may have this too, from package installs - run cscli parsers delete crowdsecurity/whitelists - run -0 cscli collections remove --all - assert_output --partial "Removed symlink [crowdsecurity/sshd]" - assert_output --partial "Removed symlink [crowdsecurity/linux]" - run -0 --separate-stderr cscli hub list -o json + rune cscli parsers delete crowdsecurity/whitelists + rune -0 cscli collections remove --all + assert_stderr --partial "Removed symlink [crowdsecurity/sshd]" + assert_stderr --partial "Removed symlink [crowdsecurity/linux]" + rune -0 --separate-stderr cscli hub list -o json assert_json '{collections:[],parsers:[],postoverflows:[],scenarios:[]}' - run -0 cscli collections remove --all - assert_output --partial 'Disabled 0 items' + rune -0 cscli collections remove --all + assert_stderr --partial 'Disabled 0 items' +} + +@test "a taint bubbles up to the top collection" { + coll=crowdsecurity/nginx + subcoll=crowdsecurity/base-http-scenarios + scenario=crowdsecurity/http-crawl-non_statics + + # install a collection with dependencies + rune -0 cscli collections install "$coll" + + # the collection, subcollection and scenario are installed and not tainted + # we have to default to false because tainted is (as of 1.4.6) returned + # only when true + rune -0 cscli collections inspect "$coll" -o json + rune -0 jq -e '(.installed,.tainted|false)==(true,false)' <(output) + rune -0 cscli collections inspect "$subcoll" -o json + rune -0 jq -e '(.installed,.tainted|false)==(true,false)' <(output) + rune -0 cscli scenarios inspect "$scenario" -o json + rune -0 jq -e '(.installed,.tainted|false)==(true,false)' <(output) + + # we taint the scenario + HUB_DIR=$(config_get '.config_paths.hub_dir') + yq e '.description="I am tainted"' -i "$HUB_DIR/scenarios/$scenario.yaml" + + # the collection, subcollection and scenario are now tainted + rune -0 cscli scenarios inspect "$scenario" -o json + rune -0 jq -e '(.installed,.tainted)==(true,true)' <(output) + rune -0 cscli collections inspect "$subcoll" -o json + rune -0 jq -e '(.installed,.tainted)==(true,true)' <(output) + rune -0 cscli collections inspect "$coll" -o json + rune -0 jq -e '(.installed,.tainted)==(true,true)' <(output) } # TODO test download-only