From 3b3ae92c65d7212b9ec319643839da39b0cf9179 Mon Sep 17 00:00:00 2001 From: jhughesbiot Date: Tue, 6 Jun 2023 13:57:12 -0600 Subject: [PATCH 1/5] Initial add of connection test module with ping test --- test_orc/modules/conn/bin/start_test_module | 39 +++++++++++ test_orc/modules/conn/conf/module_config.json | 22 ++++++ test_orc/modules/conn/conn.Dockerfile | 11 +++ .../conn/python/src/connection_module.py | 49 +++++++++++++ test_orc/modules/conn/python/src/run.py | 68 +++++++++++++++++++ 5 files changed, 189 insertions(+) create mode 100644 test_orc/modules/conn/bin/start_test_module create mode 100644 test_orc/modules/conn/conf/module_config.json create mode 100644 test_orc/modules/conn/conn.Dockerfile create mode 100644 test_orc/modules/conn/python/src/connection_module.py create mode 100644 test_orc/modules/conn/python/src/run.py diff --git a/test_orc/modules/conn/bin/start_test_module b/test_orc/modules/conn/bin/start_test_module new file mode 100644 index 000000000..4550849ce --- /dev/null +++ b/test_orc/modules/conn/bin/start_test_module @@ -0,0 +1,39 @@ +#!/bin/bash + +# Setup and start the connection test module + +# Define where the python source files are located +PYTHON_SRC_DIR=/testrun/python/src + +# Fetch module name +MODULE_NAME=$1 + +# Default interface should be veth0 for all containers +DEFAULT_IFACE=veth0 + +# Allow a user to define an interface by passing it into this script +DEFINED_IFACE=$2 + +# Select which interace to use +if [[ -z $DEFINED_IFACE || "$DEFINED_IFACE" == "null" ]] +then + echo "No interface defined, defaulting to veth0" + INTF=$DEFAULT_IFACE +else + INTF=$DEFINED_IFACE +fi + +# Create and set permissions on the log files +LOG_FILE=/runtime/output/$MODULE_NAME.log +RESULT_FILE=/runtime/output/$MODULE_NAME-result.json +touch $LOG_FILE +touch $RESULT_FILE +chown $HOST_USER:$HOST_USER $LOG_FILE +chown $HOST_USER:$HOST_USER $RESULT_FILE + +# Run the python scrip that will execute the tests for this module +# -u flag allows python print statements +# to be logged by docker by running unbuffered +python3 -u $PYTHON_SRC_DIR/run.py "-m $MODULE_NAME" + +echo Module has finished \ No newline at end of file diff --git a/test_orc/modules/conn/conf/module_config.json b/test_orc/modules/conn/conf/module_config.json new file mode 100644 index 000000000..e73846340 --- /dev/null +++ b/test_orc/modules/conn/conf/module_config.json @@ -0,0 +1,22 @@ +{ + "config": { + "meta": { + "name": "connection", + "display_name": "Connection", + "description": "Connection tests" + }, + "network": true, + "docker": { + "depends_on": "base", + "enable_container": true, + "timeout": 30 + }, + "tests":[ + { + "name": "connection.target_ping", + "description": "The device under test responds to an ICMP echo (ping) request.", + "expected_behavior": "The device under test responds to an ICMP echo (ping) request." + } + ] + } +} \ No newline at end of file diff --git a/test_orc/modules/conn/conn.Dockerfile b/test_orc/modules/conn/conn.Dockerfile new file mode 100644 index 000000000..f6a2c86b4 --- /dev/null +++ b/test_orc/modules/conn/conn.Dockerfile @@ -0,0 +1,11 @@ +# Image name: test-run/conn-test +FROM test-run/base-test:latest + +# Copy over all configuration files +COPY modules/conn/conf /testrun/conf + +# Load device binary files +COPY modules/conn/bin /testrun/bin + +# Copy over all python files +COPY modules/conn/python /testrun/python \ No newline at end of file diff --git a/test_orc/modules/conn/python/src/connection_module.py b/test_orc/modules/conn/python/src/connection_module.py new file mode 100644 index 000000000..086f32a04 --- /dev/null +++ b/test_orc/modules/conn/python/src/connection_module.py @@ -0,0 +1,49 @@ +# Copyright 2023 Google LLC +# +# 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 +# +# https://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. + +"""Connection test module""" +import util +import sys +from test_module import TestModule + +LOG_NAME = "test_connection" +LOGGER = None + + +class ConnectionModule(TestModule): + """Connection Test module""" + + def __init__(self, module): + super().__init__(module_name=module, log_name=LOG_NAME) + global LOGGER + LOGGER = self._get_logger() + + def _connection_target_ping(self): + LOGGER.info("Running connection.target_ping") + + # If the ipv4 address wasn't resolved yet, try again + if self._device_ipv4_addr is None: + self._device_ipv4_addr = self._get_device_ipv4(self) + + if self._device_ipv4_addr is None: + LOGGER.error("No device IP could be resolved") + sys.exit(1) + else: + return self._ping(self._device_ipv4_addr) + + + def _ping(self, host): + cmd = 'ping -c 1 ' + str(host) + success = util.run_command(cmd, output=False) + return success \ No newline at end of file diff --git a/test_orc/modules/conn/python/src/run.py b/test_orc/modules/conn/python/src/run.py new file mode 100644 index 000000000..5165b58c6 --- /dev/null +++ b/test_orc/modules/conn/python/src/run.py @@ -0,0 +1,68 @@ +# Copyright 2023 Google LLC +# +# 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 +# +# https://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. + +"""Run NMAP test module""" +import argparse +import signal +import sys +import logger + +from connection_module import ConnectionModule + +LOGGER = logger.get_logger('connection_module') + + +class ConnectionModuleRunner: + """Run the Connection module tests.""" + + def __init__(self, module): + + signal.signal(signal.SIGINT, self._handler) + signal.signal(signal.SIGTERM, self._handler) + signal.signal(signal.SIGABRT, self._handler) + signal.signal(signal.SIGQUIT, self._handler) + + LOGGER.info('Starting connection module') + + self._test_module = ConnectionModule(module) + self._test_module.run_tests() + + def _handler(self, signum): + LOGGER.debug('SigtermEnum: ' + str(signal.SIGTERM)) + LOGGER.debug('Exit signal received: ' + str(signum)) + if signum in (2, signal.SIGTERM): + LOGGER.info('Exit signal received. Stopping connection test module...') + LOGGER.info('Test module stopped') + sys.exit(1) + + +def run(): + parser = argparse.ArgumentParser( + description='Connection Module Help', + formatter_class=argparse.ArgumentDefaultsHelpFormatter) + + parser.add_argument( + '-m', + '--module', + help='Define the module name to be used to create the log file') + + args = parser.parse_args() + + # For some reason passing in the args from bash adds an extra + # space before the argument so we'll just strip out extra space + ConnectionModuleRunner(args.module.strip()) + + +if __name__ == '__main__': + run() From ec8b415a3d8873519ef5dab49670e085b37fc107 Mon Sep 17 00:00:00 2001 From: jhughesbiot Date: Tue, 6 Jun 2023 14:52:26 -0600 Subject: [PATCH 2/5] Update host user resolving --- net_orc/python/src/network_orchestrator.py | 42 ++++++++++++++++++++- test_orc/python/src/test_orchestrator.py | 43 +++++++++++++++++++++- 2 files changed, 83 insertions(+), 2 deletions(-) diff --git a/net_orc/python/src/network_orchestrator.py b/net_orc/python/src/network_orchestrator.py index 726eef3b9..77af509f2 100644 --- a/net_orc/python/src/network_orchestrator.py +++ b/net_orc/python/src/network_orchestrator.py @@ -469,7 +469,7 @@ def _start_network_service(self, net_module): privileged=True, detach=True, mounts=net_module.mounts, - environment={'HOST_USER': getpass.getuser()}) + environment={'HOST_USER': self._get_host_user()}) except docker.errors.ContainerError as error: LOGGER.error('Container run error') LOGGER.error(error) @@ -477,6 +477,46 @@ def _start_network_service(self, net_module): if network != 'host': self._attach_service_to_network(net_module) + def _get_host_user(self): + user = self._get_os_user() + + # If primary method failed, try secondary + if user is None: + user = self._get_user() + + LOGGER.debug("Network orchestrator host user: " + user) + return user + + def _get_os_user(self): + user = None + try: + user = os.getlogin() + except OSError as e: + # Handle the OSError exception + LOGGER.error("An OS error occurred while retrieving the login name.") + except Exception as e: + # Catch any other unexpected exceptions + LOGGER.error("An exception occurred:", e) + return user + + def _get_user(self): + user = None + try: + user = getpass.getuser() + except (KeyError, ImportError, ModuleNotFoundError, OSError) as e: + # Handle specific exceptions individually + if isinstance(e, KeyError): + LOGGER.error("USER environment variable not set or unavailable.") + elif isinstance(e, ImportError): + LOGGER.error("Unable to import the getpass module.") + elif isinstance(e, ModuleNotFoundError): + LOGGER.error("The getpass module was not found.") + elif isinstance(e, OSError): + LOGGER.error("An OS error occurred while retrieving the username.") + else: + LOGGER.error("An exception occurred:", e) + return user + def _stop_service_module(self, net_module, kill=False): LOGGER.debug('Stopping Service container ' + net_module.container_name) try: diff --git a/test_orc/python/src/test_orchestrator.py b/test_orc/python/src/test_orchestrator.py index 08b720150..e122221f5 100644 --- a/test_orc/python/src/test_orchestrator.py +++ b/test_orc/python/src/test_orchestrator.py @@ -153,7 +153,7 @@ def _run_test_module(self, module, device): read_only=True), ], environment={ - "HOST_USER": getpass.getuser(), + "HOST_USER": self._get_host_user(), "DEVICE_MAC": device.mac_addr, "DEVICE_TEST_MODULES": device.test_modules, "IPV4_SUBNET": self._net_orc.network_config.ipv4_network, @@ -206,6 +206,47 @@ def _get_module_container(self, module): LOGGER.error(error) return container + def _get_host_user(self): + user = self._get_os_user() + + # If primary method failed, try secondary + if user is None: + user = self._get_user() + + LOGGER.debug("Test orchestrator host user: " + user) + return user + + def _get_os_user(self): + user = None + try: + user = os.getlogin() + except OSError as e: + # Handle the OSError exception + LOGGER.error("An OS error occurred while retrieving the login name.") + except Exception as e: + # Catch any other unexpected exceptions + LOGGER.error("An exception occurred:", e) + return user + + def _get_user(self): + user = None + try: + user = getpass.getuser() + except (KeyError, ImportError, ModuleNotFoundError, OSError) as e: + # Handle specific exceptions individually + if isinstance(e, KeyError): + LOGGER.error("USER environment variable not set or unavailable.") + elif isinstance(e, ImportError): + LOGGER.error("Unable to import the getpass module.") + elif isinstance(e, ModuleNotFoundError): + LOGGER.error("The getpass module was not found.") + elif isinstance(e, OSError): + LOGGER.error("An OS error occurred while retrieving the username.") + else: + LOGGER.error("An exception occurred:", e) + return user + + def _load_test_modules(self): """Load network modules from module_config.json.""" LOGGER.debug("Loading test modules from /" + TEST_MODULES_DIR) From 8fb64e2bb9553413b555fda0d93f3b12bbc28008 Mon Sep 17 00:00:00 2001 From: jhughesbiot Date: Tue, 6 Jun 2023 15:36:12 -0600 Subject: [PATCH 3/5] Update host user resolving for validator --- net_orc/python/src/network_validator.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/net_orc/python/src/network_validator.py b/net_orc/python/src/network_validator.py index e76e49a5c..4a81ea3c4 100644 --- a/net_orc/python/src/network_validator.py +++ b/net_orc/python/src/network_validator.py @@ -150,7 +150,7 @@ def _start_network_device(self, device): privileged=True, detach=True, mounts=device.mounts, - environment={'HOST_USER': getpass.getuser()}) + environment={'HOST_USER': self._get_host_user()}) except docker.errors.ContainerError as error: LOGGER.error('Container run error') LOGGER.error(error) @@ -167,6 +167,28 @@ def _start_network_device(self, device): LOGGER.info('Validation device ' + device.name + ' has finished') + def _get_host_user(self): + user = self._get_os_user() + + # If primary method failed, try secondary + if user is None: + user = self._get_user() + + LOGGER.debug("Network validator host user: " + user) + return user + + def _get_os_user(self): + user = None + try: + user = os.getlogin() + except OSError as e: + # Handle the OSError exception + LOGGER.error("An OS error occurred while retrieving the login name.") + except Exception as e: + # Catch any other unexpected exceptions + LOGGER.error("An exception occurred:", e) + return user + def _get_device_status(self, module): container = self._get_device_container(module) if container is not None: From 48831d7f5aa851f7c8001bb7e95139fc58602e2a Mon Sep 17 00:00:00 2001 From: jhughesbiot Date: Wed, 7 Jun 2023 07:42:46 -0600 Subject: [PATCH 4/5] add get user method to validator --- net_orc/python/src/network_validator.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/net_orc/python/src/network_validator.py b/net_orc/python/src/network_validator.py index 4a81ea3c4..4a3a2a080 100644 --- a/net_orc/python/src/network_validator.py +++ b/net_orc/python/src/network_validator.py @@ -189,6 +189,24 @@ def _get_os_user(self): LOGGER.error("An exception occurred:", e) return user + def _get_user(self): + user = None + try: + user = getpass.getuser() + except (KeyError, ImportError, ModuleNotFoundError, OSError) as e: + # Handle specific exceptions individually + if isinstance(e, KeyError): + LOGGER.error("USER environment variable not set or unavailable.") + elif isinstance(e, ImportError): + LOGGER.error("Unable to import the getpass module.") + elif isinstance(e, ModuleNotFoundError): + LOGGER.error("The getpass module was not found.") + elif isinstance(e, OSError): + LOGGER.error("An OS error occurred while retrieving the username.") + else: + LOGGER.error("An exception occurred:", e) + return user + def _get_device_status(self, module): container = self._get_device_container(module) if container is not None: From 4009f98fbe161365592b39bfc23b0135fca8c135 Mon Sep 17 00:00:00 2001 From: jhughesbiot Date: Wed, 7 Jun 2023 10:18:23 -0600 Subject: [PATCH 5/5] Add mac_oui test Add option to return test result and details of test for reporting --- .../modules/base/python/src/test_module.py | 7 +- test_orc/modules/conn/conf/module_config.json | 5 + test_orc/modules/conn/conn.Dockerfile | 9 +- .../conn/python/src/connection_module.py | 23 ++++ test_orc/python/src/runner.py | 110 +++++++++--------- 5 files changed, 97 insertions(+), 57 deletions(-) diff --git a/test_orc/modules/base/python/src/test_module.py b/test_orc/modules/base/python/src/test_module.py index 57795a182..f29668bb2 100644 --- a/test_orc/modules/base/python/src/test_module.py +++ b/test_orc/modules/base/python/src/test_module.py @@ -95,7 +95,12 @@ def run_tests(self): else: LOGGER.info('Test ' + test['name'] + ' disabled. Skipping') if result is not None: - test['result'] = 'compliant' if result else 'non-compliant' + success = None + if isinstance(result,bool): + test['result'] = 'compliant' if result else 'non-compliant' + else: + test['result'] = 'compliant' if result[0] else 'non-compliant' + test['result_details'] = result[1] else: test['result'] = 'skipped' test['end'] = datetime.now().isoformat() diff --git a/test_orc/modules/conn/conf/module_config.json b/test_orc/modules/conn/conf/module_config.json index e73846340..505cc9e78 100644 --- a/test_orc/modules/conn/conf/module_config.json +++ b/test_orc/modules/conn/conf/module_config.json @@ -16,6 +16,11 @@ "name": "connection.target_ping", "description": "The device under test responds to an ICMP echo (ping) request.", "expected_behavior": "The device under test responds to an ICMP echo (ping) request." + }, + { + "name": "connection.mac_oui", + "description": "The device under test hs a MAC address prefix that is registered against a known manufacturer.", + "expected_behavior": "The MAC address prefix is registered in the IEEE Organizationally Unique Identifier database." } ] } diff --git a/test_orc/modules/conn/conn.Dockerfile b/test_orc/modules/conn/conn.Dockerfile index f6a2c86b4..105583be1 100644 --- a/test_orc/modules/conn/conn.Dockerfile +++ b/test_orc/modules/conn/conn.Dockerfile @@ -1,6 +1,12 @@ # Image name: test-run/conn-test FROM test-run/base-test:latest +#install all necessary packages +RUN apt-get install -y wget + +#Update the oui.txt file from ieee +RUN wget http://standards-oui.ieee.org/oui.txt -P /usr/local/etc/ + # Copy over all configuration files COPY modules/conn/conf /testrun/conf @@ -8,4 +14,5 @@ COPY modules/conn/conf /testrun/conf COPY modules/conn/bin /testrun/bin # Copy over all python files -COPY modules/conn/python /testrun/python \ No newline at end of file +COPY modules/conn/python /testrun/python + diff --git a/test_orc/modules/conn/python/src/connection_module.py b/test_orc/modules/conn/python/src/connection_module.py index 086f32a04..1104ce2ce 100644 --- a/test_orc/modules/conn/python/src/connection_module.py +++ b/test_orc/modules/conn/python/src/connection_module.py @@ -19,6 +19,7 @@ LOG_NAME = "test_connection" LOGGER = None +OUI_FILE="/usr/local/etc/oui.txt" class ConnectionModule(TestModule): @@ -42,6 +43,28 @@ def _connection_target_ping(self): else: return self._ping(self._device_ipv4_addr) + def _connection_mac_oui(self): + LOGGER.info("Running connection.mac_oui") + manufacturer = self._get_oui_manufacturer(self._device_mac) + if manufacturer is not None: + LOGGER.info("OUI Manufacturer found: " + manufacturer) + return True, "OUI Manufacturer found: " + manufacturer + else: + LOGGER.info("No OUI Manufacturer found for: " + self._device_mac) + return False, "No OUI Manufacturer found for: " + self._device_mac + + + def _get_oui_manufacturer(self,mac_address): + # Do some quick fixes on the format of the mac_address + # to match the oui file pattern + mac_address = mac_address.replace(":","-").upper() + with open(OUI_FILE, "r") as file: + for line in file: + if mac_address.startswith(line[:8]): + start = line.index("(hex)") + len("(hex)") + return line[start:].strip() # Extract the company name + + return None def _ping(self, host): cmd = 'ping -c 1 ' + str(host) diff --git a/test_orc/python/src/runner.py b/test_orc/python/src/runner.py index 363f800af..ed3b9059a 100644 --- a/test_orc/python/src/runner.py +++ b/test_orc/python/src/runner.py @@ -1,55 +1,55 @@ -# Copyright 2023 Google LLC -# -# 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 -# -# https://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. - -"""Provides high level management of the test orchestrator.""" -import time -import logger - -LOGGER = logger.get_logger('runner') - - -class Runner: - """Holds the state of the testing for one device.""" - - def __init__(self, test_orc, device): - self._test_orc = test_orc - self._device = device - - def run(self): - self._run_test_modules() - - def _run_test_modules(self): - """Iterates through each test module and starts the container.""" - LOGGER.info('Running test modules...') - for module in self._test_modules: - self.run_test_module(module) - LOGGER.info('All tests complete') - - def run_test_module(self, module): - """Start the test container and extract the results.""" - - if module is None or not module.enable_container: - return - - self._test_orc.start_test_module(module) - - # Determine the module timeout time - test_module_timeout = time.time() + module.timeout - status = self._test_orc.get_module_status(module) - - while time.time() < test_module_timeout and status == 'running': - time.sleep(1) - status = self._test_orc.get_module_status(module) - - LOGGER.info(f'Test module {module.display_name} has finished') +# Copyright 2023 Google LLC +# +# 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 +# +# https://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. + +"""Provides high level management of the test orchestrator.""" +import time +import logger + +LOGGER = logger.get_logger('runner') + + +class Runner: + """Holds the state of the testing for one device.""" + + def __init__(self, test_orc, device): + self._test_orc = test_orc + self._device = device + + def run(self): + self._run_test_modules() + + def _run_test_modules(self): + """Iterates through each test module and starts the container.""" + LOGGER.info('Running test modules...') + for module in self._test_modules: + self.run_test_module(module) + LOGGER.info('All tests complete') + + def run_test_module(self, module): + """Start the test container and extract the results.""" + + if module is None or not module.enable_container: + return + + self._test_orc.start_test_module(module) + + # Determine the module timeout time + test_module_timeout = time.time() + module.timeout + status = self._test_orc.get_module_status(module) + + while time.time() < test_module_timeout and status == 'running': + time.sleep(1) + status = self._test_orc.get_module_status(module) + + LOGGER.info(f'Test module {module.display_name} has finished')