diff --git a/roles/tester/defaults/main.yml b/roles/tester/defaults/main.yml index b15ceca..1849f28 100644 --- a/roles/tester/defaults/main.yml +++ b/roles/tester/defaults/main.yml @@ -4,11 +4,13 @@ tester_required_vars: - tester_cve_repo tester_packages: + - pytest - python-module-pytest - python-module-pytest-bdd - python-module-paramiko - git - curl + - gcc tester_username: abuser -tester_username: abuser_sudo +tester_username_sudo: abuser_sudo diff --git a/roles/tester/tasks/main.yml b/roles/tester/tasks/main.yml index 541e929..51ec784 100644 --- a/roles/tester/tasks/main.yml +++ b/roles/tester/tasks/main.yml @@ -16,13 +16,13 @@ - name: "ensure that the {{ tester_username_sudo }} exists" user: - name: "{{ tester_username }}" + name: "{{ tester_username_sudo }}" groups: wheel append: true - name: fetch CVE repository git: repo: "{{ tester_cve_repo }}" - dist: "/{{ tester_username }}/cve" + dest: "/home/{{ tester_username_sudo }}/cve" become: yes - become_user: "{{ tester_username }}" + become_user: "{{ tester_username_sudo }}" diff --git a/roles/tester/tests/inventory b/roles/tester/tests.ansible/inventory similarity index 100% rename from roles/tester/tests/inventory rename to roles/tester/tests.ansible/inventory diff --git a/roles/tester/tests/test.yml b/roles/tester/tests.ansible/test.yml similarity index 100% rename from roles/tester/tests/test.yml rename to roles/tester/tests.ansible/test.yml diff --git a/roles/tester/tests/__init__.py b/roles/tester/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/roles/tester/tests/conftest.py b/roles/tester/tests/conftest.py new file mode 100644 index 0000000..ed2cf8e --- /dev/null +++ b/roles/tester/tests/conftest.py @@ -0,0 +1,108 @@ +"""Configuration for pytest runner.""" + +from pytest_bdd import given, when +import pytest +import paramiko +from paramiko.config import SSHConfig +from os.path import expanduser +import sys +import os +import inspect + +pytest_plugins = "pytester" +cvet = {} + +SSH_USERNAME = os.getenv("SSH_USERNAME", "root") + +class Target: + def __init__(self, host): + self.host = host + config_file = open(expanduser('.tmp/ssh_config')) + config = SSHConfig() + config.parse(config_file) + ip = config.lookup(host).get('hostname', None) + port = config.lookup(host).get('port', 22) + pk = config.lookup(host).get('identityfile', None) + + self.ssh = paramiko.SSHClient() + self.ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + self.ssh.connect(hostname=ip, port=int(port), username=SSH_USERNAME, key_filename=pk) + s = self.ssh.get_transport().open_session() + paramiko.agent.AgentRequestHandler(s) + def __exit__(self): + self.ssh.close() + + def exec_command(self, cmd): + ssh_stdin, ssh_stdout, ssh_stderr = self.ssh.exec_command(cmd) + self.res = {'rc': ssh_stdout.channel.recv_exit_status(), + 'cmd': cmd, + 'host': self.host, + 'stdout': ssh_stdout.read(), + 'stderr': ssh_stderr.read()} + return self.res + + +@given("I have a root fixture") +def root(): + return "root" + + +@when("I use a when step from the parent conftest") +def global_when(): + pass + + +def assert_cmd(expect, res): + assert expect(res), "execution of '{}' failed on '{}' with '{}'; lambda is: {}".format(res['cmd'], res['host'], res['stdout'] + res['stderr'], inspect.getsource(expect)) + + +def read_env_vars(): + req_vars = ['CVET_ABUSER_NODE', + 'CVET_ABUSER_USER', + 'CVET_ABUSER_USER_WITH_SUDO', + 'CVET_VICTIM_NODE', + 'CVET_VICTIM_NODE_SHORT', + 'CVET_VICTIM_ADDRESS', + 'CVET_ABUSERS', + 'CVET_VICTIMS', + 'CVET_SRC_NIC', + 'CVET_CVE', + 'CVET_TTL', + 'CVET_SRC_START_ADDRESS_RANGE', + 'CVET_SRC_END_ADDRESS_RANGE' + ] + list_vars = ['CVET_ABUSERS' + 'CVET_VICTIMS' + ] + + for v in req_vars: + if v not in os.environ: + print('{} required but is not set'.format(v)) + sys.exit(1) + cvet[v.split('_',1)[1].lower()] = os.environ[v] if v not in list_vars else filter(None, os.environ[v].split(' ')) + cvet['all_hosts'] = cvet['abusers'] + cvet['victims'] +read_env_vars() + +@pytest.fixture(scope='session', params=cvet['all_hosts']) +def ssh_all(request): + return Target(request.param) + +@pytest.fixture(scope='session', params=[cvet['abuser_node']]) +def ssh_abuser(request): + return Target(request.param) + +@pytest.fixture(scope='session', params=[cvet['victim_node']]) +def ssh_victim(request): + return Target(request.param) + +@pytest.fixture(scope='session', params=cvet['abusers']) +def ssh_abusers(request): + return Target(request.param) + +@pytest.fixture(scope='session', params=cvet['victims']) +def ssh_victims(request): + return Target(request.param) + +@pytest.fixture(scope='session') +def cvet_params(): + return cvet diff --git a/roles/tester/tests/cve/test_cve_exploit.py b/roles/tester/tests/cve/test_cve_exploit.py new file mode 100644 index 0000000..6c2de44 --- /dev/null +++ b/roles/tester/tests/cve/test_cve_exploit.py @@ -0,0 +1,48 @@ +# coding=utf-8 +"""Perform exploit from <cve> feature tests.""" + +from pytest_bdd import ( + given, + scenario, + then, + when, +) + + +@scenario('../features/cve.feature', 'Victim node should pass all checks after performing exploit') +def test_victim_node_should_pass_all_checks_after_performing_exploit(ssh_abuser, ssh_victim): + """Victim node should pass all checks after performing exploit.""" + + +@given('prepared <abuser> node') +def prepared_abuser_node(ssh_abuser, cvet_params): + """prepared <abuser> node.""" + res = ssh_abuser.exec_command('for s in ~abuser/cve/{cve}/abuser-side-*.sh; do $s; done'.format(cve=cvet_params['cve'])) + assert res['rc'] == 0, "STDERR: {}\nSTDOUT: {}".format(res['stderr'], res['stdout']) + + +@given('prepared <victim> node') +def prepared_victim_node(ssh_abuser, ssh_victim, cvet_params): + """prepared <victim> node.""" + res = ssh_abuser.exec_command('for s in ~abuser/cve/{cve}/victim-side-*.sh; do scp $s {victim}:/tmp/$(basename $s); done'.format(cve=cvet_params['cve'], victim=cvet_params['victim_node_short'])) + assert res['rc'] == 0, "STDERR: {}\nSTDOUT: {}".format(res['stderr'], res['stdout']) + res = ssh_victim.exec_command('for s in /tmp/victim-side-*.sh; do $s; done') + assert res['rc'] == 0, "STDERR: {}\nSTDOUT: {}".format(res['stderr'], res['stdout']) + + +@when('exploit is finished') +def exploit_is_finished(ssh_abuser, cvet_params): + """exploit is finished.""" + env=' '.join(['{}="{}"'.format(k,v) for k,v in cvet_params.items()]) + res = ssh_abuser.exec_command('pushd ~abuser/cve/{cve}/; {env_vars} timeout --kill-after=10 --signal=9 -v {ttl} ./perform-sploit.sh'.format(cve=cvet_params['cve'], ttl=cvet_params['ttl'], env_vars=env)) + assert res['rc'] == (128 + 9), "STDERR: {}\nSTDOUT: {}".format(res['stderr'], res['stdout']) + + +@then('all checks against <victim> are passed') +def all_checks_against_victim_are_passed(ssh_abuser, ssh_victim, cvet_params): + """all checks against <victim> are passed.""" + env=' '.join(['{}="{}"'.format(k,v) for k,v in cvet_params.items()]) + res = ssh_abuser.exec_command('for s in ~abuser/cve/{cve}/check-victim-*.sh; do scp $s {victim}:/tmp/$(basename $s); done'.format(cve=cvet_params['cve'], victim=cvet_params['victim_node_short'])) + res = ssh_victim.exec_command('{env_vars} /tmp/check-victim-*.sh'.format(env_vars=env)) + assert res['rc'] == 0, "STDERR: {}\nSTDOUT: {}".format(res['stderr'], res['stdout']) + diff --git a/roles/tester/tests/features/cve.feature b/roles/tester/tests/features/cve.feature new file mode 100644 index 0000000..19125d7 --- /dev/null +++ b/roles/tester/tests/features/cve.feature @@ -0,0 +1,8 @@ +Feature: Perform exploit from <cve> + Environment should not be affectable by <cve> exploit + + Scenario Outline: Victim node should pass all checks after performing exploit + Given prepared <abuser> node + And prepared <victim> node + When exploit is finished + Then all checks against <victim> are passed