From 07909b9d5e62821b43c7d277e6f6bd33c3a238e9 Mon Sep 17 00:00:00 2001 From: Dmitry Kalashnik Date: Mon, 3 Aug 2015 17:25:41 +0300 Subject: [PATCH] Add system tests for security scans Test types: * Fuel master Credentialed Patch Audit * Fuel master Advanced Web Services tests * Ubuntu controller Credentialed Patch Audit Implements: blueprint security-scanning-nessus Change-Id: I953b8bfb989becc264a9d6be3f66cf9d60f8842f --- doc/base_tests.rst | 9 + doc/helpers.rst | 5 + fuelweb_test/helpers/nessus.py | 175 ++++++++++++ fuelweb_test/run_tests.py | 1 + fuelweb_test/settings.py | 6 + fuelweb_test/tests/tests_security/__init__.py | 0 .../tests/tests_security/test_run_nessus.py | 263 ++++++++++++++++++ 7 files changed, 459 insertions(+) create mode 100644 fuelweb_test/helpers/nessus.py create mode 100644 fuelweb_test/tests/tests_security/__init__.py create mode 100644 fuelweb_test/tests/tests_security/test_run_nessus.py diff --git a/doc/base_tests.rst b/doc/base_tests.rst index dbd4934ee..c20c343c5 100644 --- a/doc/base_tests.rst +++ b/doc/base_tests.rst @@ -224,6 +224,15 @@ Patching tests :members: +Security tests +============== + +Nessus scan tests +----------------- +.. automodule:: fuelweb_test.tests.tests_security_test_run_nessus + :members: + + Strength tests ============== diff --git a/doc/helpers.rst b/doc/helpers.rst index ce208e323..7803761da 100644 --- a/doc/helpers.rst +++ b/doc/helpers.rst @@ -53,6 +53,11 @@ Multiple Networks Hacks .. automodule:: fuelweb_test.helpers.multiple_networks_hacks :members: +Nessus REST Client +------------------ +.. automodule:: fuelweb_test.helpers.nessus + :members: + Ntp --- .. automodule:: fuelweb_test.helpers.ntp diff --git a/fuelweb_test/helpers/nessus.py b/fuelweb_test/helpers/nessus.py new file mode 100644 index 000000000..5fd35a52c --- /dev/null +++ b/fuelweb_test/helpers/nessus.py @@ -0,0 +1,175 @@ +import json +import urlparse + +from devops.helpers.helpers import wait +from proboscis import asserts +import requests + +from fuelweb_test import logger + + +class NessusClient(object): + def __init__(self, hostname, port, username, password, ssl_verify=False): + self.nessus_auth_token = None + self.nessus_base_url = 'https://{0}:{1}'.format(hostname, port) + self.nessus_username = username + self.nessus_password = password + self.ssl_verify = ssl_verify + self.login() + + def log_request(self, url, method, request_headers, request_body, + status_code, response_headers, response_body): + log_fmt = ("Request {method} {url}\n" + "Request - Headers: {request_headers}\n" + " Body: {request_body}\n" + "Response status code: {status_code}\n" + "Response - Headers: {response_headers}\n" + " Body: {response_body}\n") + + logger.info(log_fmt.format(url=url, + method=method, + request_headers=request_headers, + request_body=request_body, + status_code=status_code, + response_headers=response_headers, + response_body=response_body)) + + def request(self, method, url, body=None, **kwargs): + headers = {'X-Cookie': 'token={0}'.format(self.nessus_auth_token), + 'Content-Type': 'application/json'} + url = urlparse.urljoin(self.nessus_base_url, url) + + response = requests.request( + method, url, data=body, headers=headers, + verify=self.ssl_verify, **kwargs) + + self.log_request(url, method, headers, body, + response.status_code, response.headers, + response.content[:1024]) + + asserts.assert_equal( + response.status_code, 200, + "Request failed: {0}\n{1}".format(response.status_code, + response.content)) + + return response + + def get(self, url, body=None): + return self.request("GET", url, json.dumps(body)).json() + + def get_raw(self, url, body=None): + return self.request("GET", url, json.dumps(body)).content + + def post(self, url, body=None): + return self.request("POST", url, json.dumps(body)).json() + + def login(self): + creds = {'username': self.nessus_username, + 'password': self.nessus_password} + + self.nessus_auth_token = self.post('/session', creds)['token'] + + def add_policy(self, policy_def): + return self.post('/policies', policy_def) + + def list_policy_templates(self): + return self.get('/editor/policy/templates')['templates'] + + def add_cpa_policy(self, name, description, pid): + policy_def = \ + { + "uuid": pid, + "settings": { + "name": name, + "description": description + }, + "credentials": { + "add": { + "Host": { + "SSH": [ + { + "auth_method": "password", + "username": "root", + "password": "r00tme", + "elevate_privileges_with": "Nothing" + } + ] + } + } + } + } + + return self.add_policy(policy_def)['policy_id'] + + def add_wat_policy(self, name, desc, pid): + policy_def = \ + { + "uuid": pid, + "settings": { + "name": name, + "description": desc, + "discovery_mode": "Port scan (all ports)", + "assessment_mode": "Scan for all web vulnerabilities " + "(complex)", + + } + } + + return self.add_policy(policy_def)['policy_id'] + + def create_scan(self, name, description, target_ip, + policy_id, policy_template_id): + scan_def = \ + { + "uuid": policy_template_id, + "settings": { + "name": name, + "description": description, + "scanner_id": "1", + "policy_id": policy_id, + "text_targets": target_ip, + "launch": "ONETIME", + "enabled": False, + "launch_now": False + } + } + + return self.post('/scans', scan_def)['scan']['id'] + + def launch_scan(self, scan_id): + return self.post('/scans/{0}/launch'.format(scan_id))['scan_uuid'] + + def get_scan_history(self, scan_id, history_id): + return self.get('/scans/{0}'.format(scan_id), + {'history_id': history_id})['info'] + + def get_scan_status(self, scan_id, history_id): + return self.get_scan_history(scan_id, history_id)['status'] + + def list_scan_history_ids(self, scan_id): + data = self.get('/scans/{0}'.format(scan_id)) + return dict((h['uuid'], h['history_id']) for h in data['history']) + + def check_scan_export_status(self, scan_id, file_id): + return self.get('/scans/{0}/export/{1}/status' + .format(scan_id, file_id))['status'] == 'ready' + + def export_scan(self, scan_id, history_id, save_format): + export_def = {'history_id': history_id, + 'format': save_format, + 'chapters': 'vuln_hosts_summary'} + file_id = self.post('/scans/{0}/export'.format(scan_id), + body=export_def)['file'] + wait(lambda: self.check_scan_export_status(scan_id, file_id), + interval=10, timeout=600) + return file_id + + def download_scan_result(self, scan_id, file_id, scan_type, save_format): + report = self.get_raw('/scans/{0}/export/{1}/download' + .format(scan_id, file_id)) + + filename = 'nessus_report_scan_{0}_{1}.{2}'\ + .format(scan_id, scan_type, save_format) + logger.info("Saving Nessus scan report: {0}".format(filename)) + with open(filename, 'w') as report_file: + report_file.write(report) diff --git a/fuelweb_test/run_tests.py b/fuelweb_test/run_tests.py index c3919287f..62a7e5a17 100644 --- a/fuelweb_test/run_tests.py +++ b/fuelweb_test/run_tests.py @@ -53,6 +53,7 @@ def import_tests(): from tests import test_ha_one_controller # noqa from tests import test_vcenter # noqa from tests import test_reduced_footprint # noqa + from tests.tests_security import test_run_nessus # noqa from tests.tests_separate_services import test_separate_db # noqa from tests.tests_separate_services import test_separate_horizon # noqa from tests.tests_separate_services import test_separate_keystone # noqa diff --git a/fuelweb_test/settings.py b/fuelweb_test/settings.py index b721a60f9..c7303c352 100644 --- a/fuelweb_test/settings.py +++ b/fuelweb_test/settings.py @@ -492,3 +492,9 @@ RALLY_TAGS = os.environ.get('RALLY_TAGS', 'nova').split(',') REGENERATE_ENV_IMAGE = get_var_as_bool('REGENERATE_ENV_IMAGE', False) LATE_ARTIFACTS_JOB_URL = os.environ.get("LATE_ARTIFACTS_JOB_URL", '') + +NESSUS_ADDRESS = os.environ.get("NESSUS_ADDRESS", None) +NESSUS_PORT = os.environ.get("NESSUS_PORT", 8834) +NESSUS_USERNAME = os.environ.get("NESSUS_USERNAME") +NESSUS_PASSWORD = os.environ.get("NESSUS_PASSWORD") +NESSUS_SSL_VERIFY = get_var_as_bool("NESSUS_SSL_VERIFY", False) diff --git a/fuelweb_test/tests/tests_security/__init__.py b/fuelweb_test/tests/tests_security/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/fuelweb_test/tests/tests_security/test_run_nessus.py b/fuelweb_test/tests/tests_security/test_run_nessus.py new file mode 100644 index 000000000..07b9aa234 --- /dev/null +++ b/fuelweb_test/tests/tests_security/test_run_nessus.py @@ -0,0 +1,263 @@ +# Copyright 2015 Mirantis, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +import time + +from devops.helpers.helpers import tcp_ping +from devops.helpers.helpers import wait +import netaddr +from proboscis import test + +from fuelweb_test.helpers import decorators +from fuelweb_test.helpers import nessus +from fuelweb_test import settings as CONF +from fuelweb_test.tests import base_test_case +from fuelweb_test.tests import test_neutron_tun + + +@test(groups=["nessus"]) +class TestNessus(test_neutron_tun.NeutronTunHaBase): + """Security tests by Nessus + + Environment variables: + - SECURITY_TEST - True if you have pre-built Nessus qcow image. + Default: False + - NESSUS_IMAGE_PATH - path to pre-built Nessus qcow image. + Default: /var/lib/libvirt/images/nessus.qcow2 + - NESSUS_ADDRESS - Nessus API IP address of pre-installed Nessus. + Note: Nessus should have access to all virtual networks, all nodes + and all ports. + Default: None, address will be detected automatically by scanning + admin network. + - NESSUS_PORT - Nessus API port. + Default: 8834 + - NESSUS_USERNAME - Username to login to Nessus. + - NESSUS_PASSWORD - Password to login to Nessus. + - NESSUS_SSL_VERIFY - True if you want verify Nessus SSL + Default: False + """ + + def enable_password_login_for_ssh_on_slaves(self, slave_names): + for node_name in slave_names: + with self.fuel_web.get_ssh_for_node(node_name) as remote: + remote.execute("sed -i 's/PasswordAuthentication no/" + "PasswordAuthentication yes/g' " + "/etc/ssh/sshd_config") + remote.execute("service ssh restart") + + def find_nessus_address(self, + nessus_net_name='admin', + nessus_port=8834): + admin_net_cidr = \ + self.env.d_env.get_network(name=nessus_net_name).ip_network + + for address in netaddr.IPNetwork(admin_net_cidr).iter_hosts(): + if tcp_ping(address.format(), nessus_port): + return address.format() + + @test(depends_on=[base_test_case.SetupEnvironment.prepare_slaves_5], + groups=["deploy_neutron_tun_ha_nessus"]) + @decorators.log_snapshot_after_test + def deploy_neutron_tun_ha_nessus(self): + """Deploy cluster in HA mode with Neutron VXLAN for Nessus + + Scenario: + 1. Create cluster + 2. Add 3 nodes with controller role + 3. Add 2 nodes with compute role + 4. Deploy the cluster + 5. Run network verification + 6. Run OSTF + + Duration 80m + Snapshot deploy_neutron_tun_ha_nessus + """ + super(self.__class__, self).deploy_neutron_tun_ha_base( + snapshot_name="deploy_neutron_tun_ha_nessus") + + @test(depends_on=[deploy_neutron_tun_ha_nessus], + groups=["nessus_cpa", "nessus_fuel_master_cpa"]) + def nessus_fuel_master_cpa(self): + """Fuel master Credentialed Patch Audit. + + Scenario: + 1. Configure Nessus to run Credentialed Patch Audit + against Fuel Master + 2. Start scan + 3. Download scan results + + Duration 40m + Snapshot nessus_fuel_master_cpa + + """ + self.env.revert_snapshot("deploy_neutron_tun_ha_nessus") + + if CONF.NESSUS_ADDRESS is None: + CONF.NESSUS_ADDRESS = \ + self.find_nessus_address(nessus_net_name='admin', + nessus_port=CONF.NESSUS_PORT) + + nessus_client = nessus.NessusClient(CONF.NESSUS_ADDRESS, + CONF.NESSUS_PORT, + CONF.NESSUS_USERNAME, + CONF.NESSUS_PASSWORD, + CONF.NESSUS_SSL_VERIFY) + + scan_start_date = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) + + scan_name = "Scan CPA {0}".format(scan_start_date) + + policies_list = nessus_client.list_policy_templates() + cpa_policy_template = filter( + lambda template: template['title'] == 'Credentialed Patch Audit', + policies_list)[0] + + policy_id = nessus_client.add_cpa_policy( + scan_name, CONF.ENV_NAME, cpa_policy_template['uuid']) + + scan_id = nessus_client.create_scan( + scan_name, CONF.ENV_NAME, self.fuel_web.admin_node_ip, + policy_id, cpa_policy_template['uuid']) + scan_uuid = nessus_client.launch_scan(scan_id) + history_id = nessus_client.list_scan_history_ids(scan_id)[scan_uuid] + + check_scan_complete = \ + lambda: (nessus_client.get_scan_status(scan_id, history_id) == + 'completed') + wait(check_scan_complete, interval=10, timeout=60 * 30) + + file_id = nessus_client.export_scan(scan_id, history_id, 'html') + nessus_client.download_scan_result(scan_id, file_id, + 'master_cpa', 'html') + + self.env.make_snapshot("nessus_fuel_master_cpa") + + @test(depends_on=[deploy_neutron_tun_ha_nessus], + groups=["nessus_wat", "nessus_fuel_master_wat"]) + def nessus_fuel_master_wat(self): + """Fuel master Advanced Web Services tests. + + Scenario: + 1. Configure Nessus to run Advanced Web Services tests + againstFuel Master + 2. Start scan + 3. Download scan results + + Duration 40 min + Snapshot nessus_fuel_master_wat + + """ + self.env.revert_snapshot("deploy_neutron_tun_ha_nessus") + + if CONF.NESSUS_ADDRESS is None: + CONF.NESSUS_ADDRESS = \ + self.find_nessus_address(nessus_net_name='admin', + nessus_port=CONF.NESSUS_PORT) + + nessus_client = nessus.NessusClient(CONF.NESSUS_ADDRESS, + CONF.NESSUS_PORT, + CONF.NESSUS_USERNAME, + CONF.NESSUS_PASSWORD, + CONF.NESSUS_SSL_VERIFY) + + scan_start_date = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) + + scan_name = "Scan WAT {0}".format(scan_start_date) + + policies_list = nessus_client.list_policy_templates() + wat_policy_template = filter( + lambda template: template['title'] == 'Web Application Tests', + policies_list)[0] + + policy_id = nessus_client.add_wat_policy( + scan_name, CONF.ENV_NAME, wat_policy_template['uuid']) + + scan_id = nessus_client.create_scan( + scan_name, CONF.ENV_NAME, self.fuel_web.admin_node_ip, + policy_id, wat_policy_template['uuid']) + + scan_uuid = nessus_client.launch_scan(scan_id) + history_id = nessus_client.list_scan_history_ids(scan_id)[scan_uuid] + + check_scan_complete = \ + lambda: (nessus_client.get_scan_status(scan_id, history_id) == + 'completed') + wait(check_scan_complete, interval=10, timeout=60 * 30) + + file_id = nessus_client.export_scan(scan_id, history_id, 'html') + nessus_client.download_scan_result(scan_id, file_id, + 'master_wat', 'html') + + self.env.make_snapshot("nessus_fuel_master_wat") + + @test(depends_on=[deploy_neutron_tun_ha_nessus], + groups=["nessus_cpa", "nessus_controller_ubuntu_cpa"]) + def nessus_controller_ubuntu_cpa(self): + """Ubuntu controller Credentialed Patch Audit. + + Scenario: + 1. Configure Nessus to run Credentialed Patch Audit + against MOS controller on Ubuntu + 2. Start scan + 3. Download scan results + + Duration 40 min + Snapshot nessus_controller_ubuntu_cpa + + """ + self.env.revert_snapshot("deploy_neutron_tun_ha_nessus") + + self.enable_password_login_for_ssh_on_slaves(['slave-01']) + + if CONF.NESSUS_ADDRESS is None: + CONF.NESSUS_ADDRESS = \ + self.find_nessus_address(nessus_net_name='admin', + nessus_port=CONF.NESSUS_PORT) + + nessus_client = nessus.NessusClient(CONF.NESSUS_ADDRESS, + CONF.NESSUS_PORT, + CONF.NESSUS_USERNAME, + CONF.NESSUS_PASSWORD, + CONF.NESSUS_SSL_VERIFY) + + scan_start_date = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) + + scan_name = "Scan CPA {0}".format(scan_start_date) + + policies_list = nessus_client.list_policy_templates() + cpa_policy_template = filter( + lambda template: template['title'] == 'Credentialed Patch Audit', + policies_list)[0] + + policy_id = nessus_client.add_cpa_policy( + scan_name, CONF.ENV_NAME, cpa_policy_template['uuid']) + + slave_address = \ + self.fuel_web.get_nailgun_node_by_name('slave-01')['ip'] + + scan_id = nessus_client.create_scan( + scan_name, CONF.ENV_NAME, slave_address, + policy_id, cpa_policy_template['uuid']) + scan_uuid = nessus_client.launch_scan(scan_id) + history_id = nessus_client.list_scan_history_ids(scan_id)[scan_uuid] + + check_scan_complete = \ + lambda: (nessus_client.get_scan_status(scan_id, history_id) == + 'completed') + wait(check_scan_complete, interval=10, timeout=60 * 30) + + file_id = nessus_client.export_scan(scan_id, history_id, 'html') + nessus_client.download_scan_result(scan_id, file_id, + 'controller_cpa', 'html') + + self.env.make_snapshot("nessus_controller_ubuntu_cpa")