diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 0cc53ff34..a89448fa2 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -13,7 +13,7 @@ jobs: timeout-minutes: 20 steps: - name: Checkout source - uses: actions/checkout@v4.1.1 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Install dependencies shell: bash {0} run: cmd/prepare @@ -36,7 +36,7 @@ jobs: timeout-minutes: 45 steps: - name: Checkout source - uses: actions/checkout@v4.1.1 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Install dependencies shell: bash {0} run: cmd/prepare @@ -53,7 +53,7 @@ jobs: if: ${{ always() }} run: sudo tar --exclude-vcs -czf runtime.tgz /usr/local/testrun/runtime/ - name: Upload runtime results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@694cdabd8bdb0f10b2cea11669e1bf5453eed0a6 # v4.2.0 if: ${{ always() }} with: if-no-files-found: error @@ -67,7 +67,7 @@ jobs: timeout-minutes: 40 steps: - name: Checkout source - uses: actions/checkout@v4.1.1 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Install dependencies shell: bash {0} run: cmd/prepare @@ -85,7 +85,7 @@ jobs: if: ${{ always() }} run: sudo tar --exclude-vcs -czf runtime.tgz /usr/local/testrun/runtime/ /usr/local/testrun/local/ - name: Upload runtime results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@694cdabd8bdb0f10b2cea11669e1bf5453eed0a6 # v4.2.0 if: ${{ always() }} with: if-no-files-found: error @@ -99,7 +99,7 @@ jobs: timeout-minutes: 5 steps: - name: Checkout source - uses: actions/checkout@v4.1.1 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Run pylint shell: bash {0} run: testing/pylint/test_pylint @@ -111,12 +111,12 @@ jobs: timeout-minutes: 5 steps: - name: Checkout source - uses: actions/checkout@v4.1.1 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Package Testrun shell: bash {0} run: cmd/package - name: Archive package - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@694cdabd8bdb0f10b2cea11669e1bf5453eed0a6 # v4.2.0 with: name: testrun_installer path: testrun*.deb @@ -126,9 +126,9 @@ jobs: name: UI runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4.1.1 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Install Node - uses: actions/setup-node@v4 + uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 # v4.0.1 with: node-version: 18.13.0 - name: Install Chromium Browser @@ -149,9 +149,9 @@ jobs: name: ESLint runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4.1.1 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Install Node - uses: actions/setup-node@v4 + uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 # v4.0.1 with: node-version: 18.13.0 - name: Install dependencies @@ -162,4 +162,4 @@ jobs: working-directory: ./modules/ui - name: Run format check run: npm run format - working-directory: ./modules/ui \ No newline at end of file + working-directory: ./modules/ui diff --git a/.gitignore b/.gitignore index 013a3fc0a..bc014793a 100644 --- a/.gitignore +++ b/.gitignore @@ -5,7 +5,9 @@ error pylint.out __pycache__/ build/ + # Ignore generated files from unit tests +testing/unit_test/temp/ testing/unit/dns/output/ testing/unit/nmap/output/ testing/unit/ntp/output/ diff --git a/framework/python/src/api/api.py b/framework/python/src/api/api.py index a40e6ba46..43efeb593 100644 --- a/framework/python/src/api/api.py +++ b/framework/python/src/api/api.py @@ -12,7 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. """Provides Testrun data via REST API.""" - from fastapi import FastAPI, APIRouter, Response, Request, status from fastapi.responses import FileResponse from fastapi.middleware.cors import CORSMiddleware @@ -187,7 +186,8 @@ async def start_test_run(self, request: Request, response: Response): ]: LOGGER.debug("Testrun is already running. Cannot start another instance") response.status_code = status.HTTP_409_CONFLICT - return self._generate_msg(False, "Testrun is already running") + return self._generate_msg(False, "Testrun cannot be started " + + "whilst a test is running on another device") # Check if requested device is known in the device repository if device is None: @@ -386,7 +386,6 @@ async def delete_device(self, request: Request, response: Response): "the device") async def save_device(self, request: Request, response: Response): - LOGGER.debug("Received device post request") try: diff --git a/framework/python/src/common/session.py b/framework/python/src/common/session.py index e42222ce5..ba65b4b63 100644 --- a/framework/python/src/common/session.py +++ b/framework/python/src/common/session.py @@ -25,6 +25,7 @@ MONITOR_PERIOD_KEY = 'monitor_period' STARTUP_TIMEOUT_KEY = 'startup_timeout' LOG_LEVEL_KEY = 'log_level' +API_URL_KEY = 'api_url' API_PORT_KEY = 'api_port' MAX_DEVICE_REPORTS_KEY = 'max_device_reports' @@ -81,6 +82,7 @@ def _get_default_config(self): 'startup_timeout': 60, 'monitor_period': 30, 'max_device_reports': 5, + 'api_url': 'http://localhost', 'api_port': 8000 } @@ -118,6 +120,9 @@ def _load_config(self): if LOG_LEVEL_KEY in config_file_json: self._config[LOG_LEVEL_KEY] = config_file_json.get(LOG_LEVEL_KEY) + if API_URL_KEY in config_file_json: + self._config[API_URL_KEY] = config_file_json.get(API_URL_KEY) + if API_PORT_KEY in config_file_json: self._config[API_PORT_KEY] = config_file_json.get(API_PORT_KEY) @@ -165,6 +170,9 @@ def get_monitor_period(self): def get_startup_timeout(self): return self._config.get(STARTUP_TIMEOUT_KEY) + def get_api_url(self): + return self._config.get(API_URL_KEY) + def get_api_port(self): return self._config.get(API_PORT_KEY) diff --git a/framework/python/src/common/testreport.py b/framework/python/src/common/testreport.py index 9300db8c1..cccfaf25a 100644 --- a/framework/python/src/common/testreport.py +++ b/framework/python/src/common/testreport.py @@ -331,7 +331,7 @@ def generate_results(self, json_data, page_num): result_list = '''
- Results List +

Results List

Name
Description
@@ -374,8 +374,8 @@ def generate_header(self, json_data): tr_img_b64 = base64.b64encode(f.read()).decode('utf-8') return f'''
-

Testrun report

-

{json_data["device"]["manufacturer"]} {json_data["device"]["model"]}

+

Testrun report

+

{json_data["device"]["manufacturer"]} {json_data["device"]["model"]}

Test Run
''' @@ -431,6 +431,36 @@ def generate_summary(self, json_data): summary += '
' + # Add device configuration + summary += ''' +
+
+

Device Configuration

+
+ ''' + + if 'test_modules' in json_data['device']: + + sorted_modules = {} + + for test_module in json_data['device']['test_modules']: + if 'enabled' in json_data['device']['test_modules'][test_module]: + sorted_modules[test_module] = json_data['device']['test_modules'][ + test_module]['enabled'] + + # Sort the modules by enabled first + sorted_modules = sorted(sorted_modules.items(), + key=lambda x:x[1], + reverse=True) + + for module in sorted_modules: + summary += self.generate_device_module_label( + module[0], + module[1] + ) + + summary += '
' + # Add the result summary summary += self.generate_result_summary(json_data) @@ -485,7 +515,7 @@ def generate_result_summary_item(self, key, value, style=None): def generate_device_summary_label(self, key, value, trailing_space=True): label = f''' -
{key}
+

{key}

{value}
''' if trailing_space: @@ -563,18 +593,30 @@ def generate_css(self): position: relative; } - .header-text { + h1 { margin: 0 0 8px 0; font-size: 20px; font-weight: 400; } - .header-title { + h2 { margin: 0px; font-size: 48px; font-weight: 700; } + h3 { + font-size: 24px; + } + + h4 { + font-size: 12px; + font-weight: 500; + color: #5F6368; + margin-bottom: 0; + margin-top: 0; + } + /* Define the summary related css elements*/ .summary-content { position: relative; @@ -586,9 +628,6 @@ def generate_css(self): .summary-item-label { position: relative; - font-size: 12px; - font-weight: 500; - color: #5F6368; } .summary-item-value { diff --git a/framework/python/src/test_orc/test_orchestrator.py b/framework/python/src/test_orc/test_orchestrator.py index 73a7596f6..062a3ca9c 100644 --- a/framework/python/src/test_orc/test_orchestrator.py +++ b/framework/python/src/test_orc/test_orchestrator.py @@ -24,6 +24,7 @@ from common.testreport import TestReport from test_orc.module import TestModule from test_orc.test_case import TestCase +import threading LOG_NAME = "test_orc" LOGGER = logger.get_logger("test_orc") @@ -35,7 +36,6 @@ LOCAL_DEVICE_REPORTS = "local/devices/{device_folder}/reports" DEVICE_ROOT_CERTS = "local/root_certs" TESTRUN_DIR = "/usr/local/testrun" -API_URL = "http://localhost:8000" class TestOrchestrator: @@ -43,7 +43,10 @@ class TestOrchestrator: def __init__(self, session, net_orc): self._test_modules = [] + self._container_logs = [] self._session = session + self._api_url = (self._session.get_api_url() + ":" + + str(self._session.get_api_port())) self._net_orc = net_orc self._test_in_progress = False self._path = os.path.dirname( @@ -88,7 +91,7 @@ def run_test_modules(self): test_modules = [] for module in self._test_modules: - if module is None or not module.enable_container or not module.enabled: + if module is None or not module.enable_container: continue if not self._is_module_enabled(module, device): @@ -139,15 +142,15 @@ def _write_reports(self, test_report): LOGGER.debug(f"Writing reports to {out_dir}") # Write the json report - with open(os.path.join(out_dir,"report.json"),"w", encoding="utf-8") as f: + with open(os.path.join(out_dir, "report.json"), "w", encoding="utf-8") as f: json.dump(test_report.to_json(), f, indent=2) # Write the html report - with open(os.path.join(out_dir,"report.html"),"w", encoding="utf-8") as f: + with open(os.path.join(out_dir, "report.html"), "w", encoding="utf-8") as f: f.write(test_report.to_html()) # Write the pdf report - with open(os.path.join(out_dir,"report.pdf"),"wb") as f: + with open(os.path.join(out_dir, "report.pdf"), "wb") as f: f.write(test_report.to_pdf().getvalue()) util.run_command(f"chown -R {self._host_user} {out_dir}") @@ -163,11 +166,10 @@ def _generate_report(self): report["status"] = self._calculate_result() report["tests"] = self.get_session().get_report_tests() report["report"] = ( - API_URL + "/" + - SAVED_DEVICE_REPORTS.replace("{device_folder}", - self.get_session().get_target_device().device_folder) + - self.get_session().get_started().strftime("%Y-%m-%dT%H:%M:%S") - ) + self._api_url + "/" + SAVED_DEVICE_REPORTS.replace( + "{device_folder}", + self.get_session().get_target_device().device_folder) + + self.get_session().get_started().strftime("%Y-%m-%dT%H:%M:%S")) return report @@ -230,18 +232,14 @@ def _find_oldest_test(self, completed_tests_dir): def _timestamp_results(self, device): # Define the current device results directory - cur_results_dir = os.path.join( - self._root_path, - RUNTIME_DIR, - device.mac_addr.replace(":", "") - ) + cur_results_dir = os.path.join(self._root_path, RUNTIME_DIR, + device.mac_addr.replace(":", "")) # Define the directory completed_results_dir = os.path.join( - self._root_path, - LOCAL_DEVICE_REPORTS.replace("{device_folder}", device.device_folder), - self.get_session().get_started().strftime("%Y-%m-%dT%H:%M:%S") - ) + self._root_path, + LOCAL_DEVICE_REPORTS.replace("{device_folder}", device.device_folder), + self.get_session().get_started().strftime("%Y-%m-%dT%H:%M:%S")) # Copy the results to the timestamp directory # leave current copy in place for quick reference to @@ -281,12 +279,18 @@ def test_in_progress(self): return self._test_in_progress def _is_module_enabled(self, module, device): + + # Enable module as fallback enabled = True if device.test_modules is not None: test_modules = device.test_modules if module.name in test_modules: if "enabled" in test_modules[module.name]: enabled = test_modules[module.name]["enabled"] + else: + # Module has not been specified in the device config + enabled = module.enabled + return enabled def _run_test_module(self, module): @@ -298,7 +302,7 @@ def _run_test_module(self, module): device = self._session.get_target_device() - LOGGER.info("Running test module " + module.name) + LOGGER.info(f"Running test module {module.name}") # Get all tests to be executed and set to in progress for test in module.tests: @@ -310,11 +314,13 @@ def _run_test_module(self, module): device_test_dir = os.path.join(self._root_path, RUNTIME_DIR, device.mac_addr.replace(":", "")) - root_certs_dir = os.path.join(self._root_path,DEVICE_ROOT_CERTS) + root_certs_dir = os.path.join(self._root_path, DEVICE_ROOT_CERTS) container_runtime_dir = os.path.join(device_test_dir, module.name) os.makedirs(container_runtime_dir, exist_ok=True) + container_log_file = os.path.join(container_runtime_dir, "module.log") + network_runtime_dir = os.path.join(self._root_path, "runtime/network") device_startup_capture = os.path.join(device_test_dir, "startup.pcap") @@ -355,13 +361,13 @@ def _run_test_module(self, module): read_only=True) ], environment={ - "TZ": self.get_session().get_timezone(), - "HOST_USER": self._host_user, - "DEVICE_MAC": device.mac_addr, - "IPV4_ADDR": device.ip_addr, - "DEVICE_TEST_MODULES": json.dumps(device.test_modules), - "IPV4_SUBNET": self._net_orc.network_config.ipv4_network, - "IPV6_SUBNET": self._net_orc.network_config.ipv6_network + "TZ": self.get_session().get_timezone(), + "HOST_USER": self._host_user, + "DEVICE_MAC": device.mac_addr, + "IPV4_ADDR": device.ip_addr, + "DEVICE_TEST_MODULES": json.dumps(device.test_modules), + "IPV4_SUBNET": self._net_orc.network_config.ipv4_network, + "IPV6_SUBNET": self._net_orc.network_config.ipv6_network }) except (docker.errors.APIError, docker.errors.ContainerError) as container_error: @@ -378,20 +384,25 @@ def _run_test_module(self, module): test_module_timeout = time.time() + module.timeout status = self._get_module_status(module) + # Resolving container logs is blocking so we need to spawn a new thread log_stream = module.container.logs(stream=True, stdout=True, stderr=True) + log_thread = threading.Thread(target=self._get_container_logs, + args=(log_stream, )) + log_thread.daemon = True + log_thread.start() + while (status == "running" and self._session.get_status() == "In Progress"): if time.time() > test_module_timeout: LOGGER.error("Module timeout exceeded, killing module: " + module.name) - self._stop_module(module=module,kill=True) + self._stop_module(module=module, kill=True) break - try: - line = next(log_stream).decode("utf-8").strip() - if re.search(LOG_REGEX, line): - print(line) - except Exception: # pylint: disable=W0718 - time.sleep(1) status = self._get_module_status(module) + # Save all container logs to file + with open(container_log_file, "w", encoding="utf-8") as f: + for line in self._container_logs: + f.write(line + "\n") + # Check that Testrun has not been stopped whilst this module was running if self.get_session().get_status() == "Stopping": # Discard results for this module @@ -440,6 +451,20 @@ def _run_test_module(self, module): LOGGER.info(f"Test module {module.name} has finished") + # Resolve all current log data in the containers log_stream + # this method is blocking so should be called in + # a thread or within a proper blocking context + def _get_container_logs(self, log_stream): + self._container_logs = [] + for log_chunk in log_stream: + lines = log_chunk.decode("utf-8").splitlines() + # Process each line and strip blank space + processed_lines = [line.strip() for line in lines if line.strip()] + self._container_logs.extend(processed_lines) + for line in lines: + if re.search(LOG_REGEX, line): + print(line) + def _get_module_status(self, module): container = self._get_module_container(module) if container is not None: @@ -521,9 +546,8 @@ def _load_test_module(self, module_dir): if "recommendations" in test_case_json: test_case.recommendations = test_case_json["recommendations"] - module.tests.append(test_case) - except Exception as error: + except Exception as error: # pylint: disable=W0718 LOGGER.error("Failed to load test case. See error for details") LOGGER.error(error) diff --git a/framework/requirements.txt b/framework/requirements.txt index a74098884..9f9e4ea91 100644 --- a/framework/requirements.txt +++ b/framework/requirements.txt @@ -11,7 +11,7 @@ scapy==2.5.0 weasyprint==60.2 # Requirements for the API -fastapi==0.99.1 +fastapi==0.109.1 psutil==5.9.8 uvicorn==0.27.0 pydantic==1.10.11 diff --git a/modules/network/base/base.Dockerfile b/modules/network/base/base.Dockerfile index a9317e85c..b30f6a7d9 100644 --- a/modules/network/base/base.Dockerfile +++ b/modules/network/base/base.Dockerfile @@ -13,7 +13,7 @@ # limitations under the License. # Image name: test-run/base -FROM ubuntu:jammy +FROM ubuntu@sha256:e6173d4dc55e76b87c4af8db8821b1feae4146dd47341e4d431118c7dd060a74 RUN apt-get update diff --git a/modules/test/base/base.Dockerfile b/modules/test/base/base.Dockerfile index 878273055..9c9f095cf 100644 --- a/modules/test/base/base.Dockerfile +++ b/modules/test/base/base.Dockerfile @@ -13,7 +13,7 @@ # limitations under the License. # Image name: test-run/base-test -FROM ubuntu:jammy +FROM ubuntu@sha256:e6173d4dc55e76b87c4af8db8821b1feae4146dd47341e4d431118c7dd060a74 ARG MODULE_NAME=base ARG MODULE_DIR=modules/test/$MODULE_NAME diff --git a/modules/test/base/python/src/test_module.py b/modules/test/base/python/src/test_module.py index 60196fa12..54e3c3008 100644 --- a/modules/test/base/python/src/test_module.py +++ b/modules/test/base/python/src/test_module.py @@ -95,10 +95,15 @@ def run_tests(self): LOGGER.debug('Attempting to run test: ' + test['name']) # Resolve the correct python method by test name and run test if hasattr(self, test_method_name): - if 'config' in test: - result = getattr(self, test_method_name)(config=test['config']) - else: - result = getattr(self, test_method_name)() + try: + if 'config' in test: + result = getattr(self, test_method_name)(config=test['config']) + else: + result = getattr(self, test_method_name)() + except Exception as e: + LOGGER.info(f'An error occurred whilst running {test["name"]}') + LOGGER.error(e) + return None else: LOGGER.info(f'Test {test["name"]} not implemented. Skipping') result = None diff --git a/modules/test/nmap/python/src/nmap_module.py b/modules/test/nmap/python/src/nmap_module.py index 22e62bdd9..93477b7ee 100644 --- a/modules/test/nmap/python/src/nmap_module.py +++ b/modules/test/nmap/python/src/nmap_module.py @@ -401,4 +401,4 @@ def _security_ssh_version(self, config): return True, f"SSH server found running {open_port_info['version']}" else: return (False, - f'''SSH server found running {open_port_info['version']}''') + f"SSH server found running {open_port_info['version']}") diff --git a/modules/test/nmap/python/src/run.py b/modules/test/nmap/python/src/run.py index 49c002b39..2a85bb074 100644 --- a/modules/test/nmap/python/src/run.py +++ b/modules/test/nmap/python/src/run.py @@ -17,7 +17,6 @@ import signal import sys import logger - from nmap_module import NmapModule LOG_NAME = 'nmap_runner' diff --git a/modules/test/tls/bin/check_cert_signature.sh b/modules/test/tls/bin/check_cert_signature.sh index ebd4a7549..37ea0f187 100644 --- a/modules/test/tls/bin/check_cert_signature.sh +++ b/modules/test/tls/bin/check_cert_signature.sh @@ -1,5 +1,19 @@ #!/bin/bash +# 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. + ROOT_CERT=$1 DEVICE_CERT=$2 diff --git a/modules/test/tls/bin/get_ciphers.sh b/modules/test/tls/bin/get_ciphers.sh index e82bbc180..3896af388 100644 --- a/modules/test/tls/bin/get_ciphers.sh +++ b/modules/test/tls/bin/get_ciphers.sh @@ -1,5 +1,19 @@ #!/bin/bash +# 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. + CAPTURE_FILE=$1 DST_IP=$2 DST_PORT=$3 diff --git a/modules/test/tls/bin/get_client_hello_packets.sh b/modules/test/tls/bin/get_client_hello_packets.sh index 13e42f791..03cfe903c 100644 --- a/modules/test/tls/bin/get_client_hello_packets.sh +++ b/modules/test/tls/bin/get_client_hello_packets.sh @@ -1,5 +1,19 @@ #!/bin/bash +# 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. + CAPTURE_FILE=$1 SRC_IP=$2 TLS_VERSION=$3 @@ -8,9 +22,9 @@ TSHARK_OUTPUT="-T json -e ip.src -e tcp.dstport -e ip.dst" TSHARK_FILTER="ssl.handshake.type==1 and ip.src==$SRC_IP" if [[ $TLS_VERSION == '1.2' || -z $TLS_VERSION ]];then - TSHARK_FILTER=$TSHARK_FILTER "and ssl.handshake.version==0x0303" -elif [ $TLS_VERSION == '1.2' ];then - TSHARK_FILTER=$TSHARK_FILTER "and ssl.handshake.version==0x0304" + TSHARK_FILTER="$TSHARK_FILTER and ssl.handshake.version==0x0303" +elif [ $TLS_VERSION == '1.3' ];then + TSHARK_FILTER="$TSHARK_FILTER and (ssl.handshake.version==0x0304 or tls.handshake.extensions.supported_version==0x0304)" fi response=$(tshark -r $CAPTURE_FILE $TSHARK_OUTPUT $TSHARK_FILTER) diff --git a/modules/test/tls/bin/get_handshake_complete.sh b/modules/test/tls/bin/get_handshake_complete.sh index de1eb887d..a2a6dc222 100644 --- a/modules/test/tls/bin/get_handshake_complete.sh +++ b/modules/test/tls/bin/get_handshake_complete.sh @@ -1,5 +1,19 @@ #!/bin/bash +# 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. + CAPTURE_FILE=$1 SRC_IP=$2 DST_IP=$3 diff --git a/modules/test/tls/bin/get_non_tls_client_connections.sh b/modules/test/tls/bin/get_non_tls_client_connections.sh new file mode 100644 index 000000000..08ed19090 --- /dev/null +++ b/modules/test/tls/bin/get_non_tls_client_connections.sh @@ -0,0 +1,32 @@ +#!/bin/bash + +# 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. + +CAPTURE_FILE=$1 +SRC_IP=$2 + +TSHARK_OUTPUT="-T json -e ip.src -e tcp.dstport -e ip.dst" +# Filter out TLS, DNS and NTP, ICMP (ping), braodcast and multicast packets +# - NTP and DNS traffic is not encrypted and if invalid NTP and/or DNS traffic has been detected +# this will be handled by their respective test modules. +# - Multicast and braodcast protocols are not typically encrypted so we aren't expecting them to +# be over TLS connections +# - ICMP (ping) requests are not encrypted so we also need to ignore these +TSHARK_FILTER="ip.src == $SRC_IP and not tls and not dns and not ntp and not icmp and not(ip.dst == 224.0.0.0/4 or ip.dst == 255.255.255.255)" + +response=$(tshark -r $CAPTURE_FILE $TSHARK_OUTPUT $TSHARK_FILTER) + +echo "$response" + \ No newline at end of file diff --git a/modules/test/tls/bin/get_tls_client_connections.sh b/modules/test/tls/bin/get_tls_client_connections.sh new file mode 100644 index 000000000..2486a5f9a --- /dev/null +++ b/modules/test/tls/bin/get_tls_client_connections.sh @@ -0,0 +1,26 @@ +#!/bin/bash + +# 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. + +CAPTURE_FILE=$1 +SRC_IP=$2 + +TSHARK_OUTPUT="-T json -e ip.src -e tcp.dstport -e ip.dst" +TSHARK_FILTER="ip.src == $SRC_IP and tls" + +response=$(tshark -r $CAPTURE_FILE $TSHARK_OUTPUT $TSHARK_FILTER) + +echo "$response" + \ No newline at end of file diff --git a/modules/test/tls/bin/get_tls_packets.sh b/modules/test/tls/bin/get_tls_packets.sh new file mode 100644 index 000000000..e06a51571 --- /dev/null +++ b/modules/test/tls/bin/get_tls_packets.sh @@ -0,0 +1,40 @@ +#!/bin/bash + +# 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. + + +CAPTURE_FILE=$1 +SRC_IP=$2 +TLS_VERSION=$3 + +TSHARK_OUTPUT="-T json -e ip.src -e tcp.dstport -e ip.dst" +# Handshakes will still report TLS version 1 even for TLS 1.2 connections +# so we need to filter thes out +TSHARK_FILTER="ip.src==$SRC_IP and ssl.handshake.type!=1" + +if [ $TLS_VERSION == '1.0' ];then + TSHARK_FILTER="$TSHARK_FILTER and ssl.record.version==0x0301" +elif [ $TLS_VERSION == '1.1' ];then + TSHARK_FILTER="$TSHARK_FILTER and ssl.record.version==0x0302" +elif [ $TLS_VERSION == '1.2' ];then + TSHARK_FILTER="$TSHARK_FILTER and ssl.record.version==0x0303" +elif [ $TLS_VERSION == '1.3' ];then + TSHARK_FILTER="$TSHARK_FILTER and ssl.record.version==0x0304" +fi + +response=$(tshark -r $CAPTURE_FILE $TSHARK_OUTPUT $TSHARK_FILTER) + +echo "$response" + \ No newline at end of file diff --git a/modules/test/tls/python/src/tls_module.py b/modules/test/tls/python/src/tls_module.py index 97182d392..b00cccc8d 100644 --- a/modules/test/tls/python/src/tls_module.py +++ b/modules/test/tls/python/src/tls_module.py @@ -78,54 +78,24 @@ def _security_tls_v1_3_client(self): return None, 'Could not resolve device IP address' def _validate_tls_client(self, client_ip, tls_version): - monitor_result = self._tls_util.validate_tls_client( + client_results = self._tls_util.validate_tls_client( client_ip=client_ip, tls_version=tls_version, - capture_file=MONITOR_CAPTURE_FILE) - startup_result = self._tls_util.validate_tls_client( - client_ip=client_ip, - tls_version=tls_version, - capture_file=STARTUP_CAPTURE_FILE) - gateway_result = self._tls_util.validate_tls_client( - client_ip=client_ip, - tls_version=tls_version, - capture_file=GATEWAY_CAPTURE_FILE) - - LOGGER.info('Montor: ' + str(monitor_result)) - LOGGER.info('Startup: ' + str(startup_result)) - LOGGER.info('Gateway: ' + str(gateway_result)) - + capture_files=[MONITOR_CAPTURE_FILE,STARTUP_CAPTURE_FILE, + GATEWAY_CAPTURE_FILE]) # Generate results based on the state - result_message = '' + result_message = 'No outbound connections were found.' result_state = None - #If any of the packetes detect failed client comms, fail the test - if (not monitor_result[0] and monitor_result[0] is not None) or ( - not startup_result[0] and startup_result[0] is not None) or ( - not gateway_result[0] and gateway_result[0] is not None): + + # If any of the packetes detect failed client comms, fail the test + if not client_results[0] and client_results[0] is not None: result_state = False - if monitor_result[0] is not None: - result_message += monitor_result[1] - if startup_result[0] is not None: - result_message += monitor_result[1] - if monitor_result[0] is not None: - gateway_result += monitor_result[1] + result_message = client_results[1] else: - # Append monitor results - if monitor_result[0]: - result_state = True - result_message += monitor_result[1] - - # Append startup results - if startup_result[0]: + if client_results[0]: result_state = True - result_message += startup_result[1] - - # Append gateway results - if gateway_result[0]: - result_state = True - result_message += gateway_result[1] - + result_message = client_results[1] return result_state, result_message def _resolve_device_ip(self): diff --git a/modules/test/tls/python/src/tls_util.py b/modules/test/tls/python/src/tls_util.py index ff7a79c44..85ddcc012 100644 --- a/modules/test/tls/python/src/tls_util.py +++ b/modules/test/tls/python/src/tls_util.py @@ -19,6 +19,7 @@ import json import os from common import util +import ipaddress LOG_NAME = 'tls_util' LOGGER = None @@ -177,39 +178,35 @@ def validate_signature(self, host): return True, 'Device signed by cert:' + root_cert else: LOGGER.info('Device not signed by cert: ' + root_cert) - except Exception as e: # pylint: disable=W0718 + except Exception as e: # pylint: disable=W0718 LOGGER.error('Failed to check cert:' + root_cert) LOGGER.error(str(e)) return False, 'Device certificate has not been signed' def process_tls_server_results(self, tls_1_2_results, tls_1_3_results): results = '' - if tls_1_2_results[0] is None and tls_1_3_results[0]: - results = True, 'TLS 1.3 validated: ' + tls_1_3_results[1] - elif tls_1_3_results[0] is None and tls_1_2_results[0]: - results = True, 'TLS 1.2 validated: ' + tls_1_2_results[1] - elif tls_1_2_results[0] and tls_1_3_results[0]: - description = 'TLS 1.2 validated: ' + tls_1_2_results[1] + '. ' - description += '\nTLS 1.3 validated: ' + tls_1_3_results[1] + '. ' - results = True, description - elif tls_1_2_results[0] and not tls_1_3_results[0]: - description = 'TLS 1.2 validated: ' + tls_1_2_results[1] + '. ' - description += '\nTLS 1.3 not validated: ' + tls_1_3_results[1] + '. ' - results = True, description - elif tls_1_3_results[0] and not tls_1_2_results[0]: - description = 'TLS 1.2 not validated: ' + tls_1_2_results[1] + '. ' - description += 'TLS 1.3 validated: ' + tls_1_3_results[1] + '. ' - results = True, description - elif not tls_1_3_results[0] and not tls_1_2_results[0] and tls_1_2_results[ - 0] is not None and tls_1_3_results is not None: - description = 'TLS 1.2 not validated:' + tls_1_2_results[1] + '. ' - description += 'TLS 1.3 not validated: ' + tls_1_3_results[1] + '. ' - results = False, description + if tls_1_2_results[0] is None and tls_1_3_results[0] is not None: + # Validate only TLS 1.3 results + description = 'TLS 1.3' + (' not' if not tls_1_3_results[ + 0] else '') + ' validated: ' + tls_1_3_results[1] + results = tls_1_3_results[0], description + elif tls_1_3_results[0] is None and tls_1_2_results[0] is not None: + # Vaidate only TLS 1.2 results + description = 'TLS 1.2' + (' not' if not tls_1_2_results[ + 0] else '') + ' validated: ' + tls_1_2_results[1] + results = tls_1_2_results[0], description + elif tls_1_3_results[0] is not None and tls_1_2_results[0] is not None: + # Validate both results + description = 'TLS 1.2' + (' not' if not tls_1_2_results[ + 0] else '') + ' validated: ' + tls_1_2_results[1] + description += '\nTLS 1.3' + (' not' if not tls_1_3_results[ + 0] else '') + ' validated: ' + tls_1_3_results[1] + results = tls_1_2_results[0] or tls_1_3_results[0], description else: - description = 'TLS 1.2 not validated: ' + tls_1_2_results[1] + '. ' - description += 'TLS 1.3 not validated: ' + tls_1_3_results[1] + '. ' + description = f'TLS 1.2 not validated: {tls_1_2_results[1]}' + description += f'\nTLS 1.3 not validated: {tls_1_3_results[1]}' results = None, description - LOGGER.info('TLS 1.2 server test results: ' + str(results)) + LOGGER.info('TLS server test results: ' + str(results)) return results def validate_tls_server(self, host, tls_version): @@ -261,22 +258,74 @@ def get_ciphers(self, capture_file, dst_ip, dst_port): ciphers = response[0].split('\n') return ciphers - def get_hello_packets(self, capture_file, src_ip, tls_version): - bin_file = self._bin_dir + '/get_client_hello_packets.sh' - args = f'{capture_file} {src_ip} {tls_version}' - command = f'{bin_file} {args}' - response = util.run_command(command) - packets = response[0].strip() - return self.parse_hello_packets(json.loads(packets), capture_file) - - def get_handshake_complete(self, capture_file, src_ip, dst_ip, tls_version): - bin_file = self._bin_dir + '/get_handshake_complete.sh' - args = f'{capture_file} {src_ip} {dst_ip} {tls_version}' - command = f'{bin_file} {args}' - response = util.run_command(command) - return response - - def parse_hello_packets(self, packets, capture_file): + def get_hello_packets(self, capture_files, src_ip, tls_version): + combined_results = [] + for capture_file in capture_files: + bin_file = self._bin_dir + '/get_client_hello_packets.sh' + args = f'{capture_file} {src_ip} {tls_version}' + command = f'{bin_file} {args}' + response = util.run_command(command) + packets = response[0].strip() + if len(packets) > 0: + # Parse each packet and append key-value pairs to combined_results + result = self.parse_packets(json.loads(packets), capture_file) + combined_results.extend(result) + return combined_results + + def get_handshake_complete(self, capture_files, src_ip, dst_ip, tls_version): + combined_results = '' + for capture_file in capture_files: + bin_file = self._bin_dir + '/get_handshake_complete.sh' + args = f'{capture_file} {src_ip} {dst_ip} {tls_version}' + command = f'{bin_file} {args}' + response = util.run_command(command) + if len(response) > 0: + combined_results += response[0] + return combined_results + + # Resolve all connections from the device that don't use TLS + def get_non_tls_packetes(self, client_ip, capture_files): + combined_packets = [] + for capture_file in capture_files: + bin_file = self._bin_dir + '/get_non_tls_client_connections.sh' + args = f'{capture_file} {client_ip}' + command = f'{bin_file} {args}' + response = util.run_command(command) + if len(response) > 0: + packets = json.loads(response[0].strip()) + combined_packets.extend(packets) + return combined_packets + + # Resolve all connections from the device that use TLS + def get_tls_client_connection_packetes(self, client_ip, capture_files): + combined_packets = [] + for capture_file in capture_files: + bin_file = self._bin_dir + '/get_tls_client_connections.sh' + args = f'{capture_file} {client_ip}' + command = f'{bin_file} {args}' + response = util.run_command(command) + packets = json.loads(response[0].strip()) + combined_packets.extend(packets) + return combined_packets + + # Resolve any TLS packets for the specified version. Does not care if the + # connections are established or any other validation only + # that there is some level of connection attempt from the device + # using the TLS version specified. + def get_tls_packets(self, capture_files, src_ip, tls_version): + combined_results = [] + for capture_file in capture_files: + bin_file = self._bin_dir + '/get_tls_packets.sh' + args = f'{capture_file} {src_ip} {tls_version}' + command = f'{bin_file} {args}' + response = util.run_command(command) + packets = response[0].strip() + # Parse each packet and append key-value pairs to combined_results + result = self.parse_packets(json.loads(packets), capture_file) + combined_results.extend(result) + return combined_results + + def parse_packets(self, packets, capture_file): hello_packets = [] for packet in packets: # Extract all the basic IP information about the packet @@ -300,7 +349,7 @@ def parse_hello_packets(self, packets, capture_file): hello_packets.append(hello_packet) return hello_packets - def process_hello_packets(self,hello_packets, tls_version = '1.2'): + def process_hello_packets(self, hello_packets, tls_version='1.2'): # Validate the ciphers only for tls 1.2 client_hello_results = {'valid': [], 'invalid': []} if tls_version == '1.2': @@ -327,10 +376,87 @@ def process_hello_packets(self,hello_packets, tls_version = '1.2'): client_hello_results['valid'] = hello_packets return client_hello_results - def validate_tls_client(self, client_ip, tls_version, capture_file): + # Check if the device has made any outbound connections that don't + # use TLS. Since some protocols do use non-encrypted methods (NTP, DHCP, etc.) + # we will assume any local connections using the same IP subnet as our + # local network are approved and only connections to IP addresses outside + # our network will be flagged. + def get_non_tls_client_connection_ips(self, client_ip, capture_files): + LOGGER.info('Checking client for non-TLS client connections') + packets = self.get_non_tls_packetes(client_ip=client_ip, + capture_files=capture_files) + + # Extract the subnet from the client IP address + src_ip = ipaddress.ip_address(client_ip) + src_subnet = ipaddress.ip_network(src_ip, strict=False) + subnet_with_mask = ipaddress.ip_network( + src_subnet, strict=False).supernet(new_prefix=24) + + non_tls_dst_ips = set() # Store unique destination IPs + for packet in packets: + # Check if an IP address is within the specified subnet. + dst_ip = ipaddress.ip_address(packet['_source']['layers']['ip.dst'][0]) + if not dst_ip in subnet_with_mask: + non_tls_dst_ips.add(str(dst_ip)) + + return non_tls_dst_ips + + # Check if the device has made any outbound connections that don't + # use TLS. Since some protocols do use non-encrypted methods (NTP, DHCP, etc.) + # we will assume any local connections using the same IP subnet as our + # local network are approved and only connections to IP addresses outside + # our network will be flagged. + def get_unsupported_tls_ips(self, client_ip, capture_files): + LOGGER.info('Checking client for unsupported TLS client connections') + tls_1_0_packets = self.get_tls_packets(capture_files, client_ip, '1.0') + tls_1_1_packets = self.get_tls_packets(capture_files, client_ip, '1.1') + + unsupported_tls_dst_ips = {} + if len(tls_1_0_packets) > 0: + for packet in tls_1_0_packets: + dst_ip = packet['dst_ip'] + tls_version = '1.0' + if dst_ip not in unsupported_tls_dst_ips: + LOGGER.info(f'''Unsupported TLS {tls_version} + connections detected to {dst_ip}''') + unsupported_tls_dst_ips[dst_ip] = [tls_version] + + if len(tls_1_1_packets) > 0: + for packet in tls_1_1_packets: + dst_ip = packet['dst_ip'] + tls_version = '1.1' + # Check if the IP is already in the dictionary + if dst_ip in unsupported_tls_dst_ips: + # If the IP is already present, append the new TLS version to the + # list + unsupported_tls_dst_ips[dst_ip].append(tls_version) + else: + # If the IP is not present, create a new list with the current + # TLS version + LOGGER.info(f'''Unsupported TLS {tls_version} connections detected + to {dst_ip}''') + unsupported_tls_dst_ips[dst_ip] = [tls_version] + return unsupported_tls_dst_ips + + # Check if the device has made any outbound connections that use any + # version of TLS. + def get_tls_client_connection_ips(self, client_ip, capture_files): + LOGGER.info('Checking client for TLS client connections') + packets = self.get_tls_client_connection_packetes( + client_ip=client_ip, capture_files=capture_files) + + tls_dst_ips = set() # Store unique destination IPs + for packet in packets: + dst_ip = ipaddress.ip_address(packet['_source']['layers']['ip.dst'][0]) + tls_dst_ips.add(str(dst_ip)) + return tls_dst_ips + + def validate_tls_client(self, client_ip, tls_version, capture_files): LOGGER.info('Validating client for TLS: ' + tls_version) - hello_packets = self.get_hello_packets(capture_file, client_ip, tls_version) - client_hello_results = self.process_hello_packets(hello_packets,tls_version) + hello_packets = self.get_hello_packets(capture_files, client_ip, + tls_version) + client_hello_results = self.process_hello_packets(hello_packets, + tls_version) handshakes = {'complete': [], 'incomplete': []} for packet in client_hello_results['valid']: @@ -338,7 +464,7 @@ def validate_tls_client(self, client_ip, tls_version, capture_file): if not packet['dst_ip'] in handshakes['complete'] and not packet[ 'dst_ip'] in handshakes['incomplete']: handshake_complete = self.get_handshake_complete( - capture_file, packet['src_ip'], packet['dst_ip'], tls_version) + capture_files, packet['src_ip'], packet['dst_ip'], tls_version) # One of the responses will be a complaint about running as root so # we have to have at least 2 entries to consider a completed handshake @@ -382,6 +508,35 @@ def validate_tls_client(self, client_ip, tls_version, capture_file): else: LOGGER.info('No client hello packets detected') tls_client_details = 'No client hello packets detected' + + # Resolve all non-TLS related client connections + non_tls_client_ips = self.get_non_tls_client_connection_ips( + client_ip, capture_files) + + # Resolve all TLS related client connections + tls_client_ips = self.get_tls_client_connection_ips(client_ip, + capture_files) + + # Filter out all outbound TLS connections regardless on whether + # or not they were validated. If they were not validated, + # they will already be failed by those tests and we only + # need to report true unencrypted oubound connections + if len(non_tls_client_ips) > 0: + for ip in non_tls_client_ips: + if ip not in tls_client_ips: + tls_client_valid = False + tls_client_details += f'''\nNon-TLS connection detected to {ip}''' + else: + LOGGER.info(f'''TLS connection detected to {ip}. + Ignoring non-TLS traffic detected to this IP''') + + unsupported_tls_ips = self.get_unsupported_tls_ips(client_ip, capture_files) + if len(unsupported_tls_ips) > 0: + tls_client_valid = False + for ip, tls_versions in unsupported_tls_ips.items(): + for tls_version in tls_versions: + tls_client_details += f'''\nUnsupported TLS {tls_version} + connection detected to {ip}''' return tls_client_valid, tls_client_details def is_ecdh_and_ecdsa(self, ciphers): diff --git a/modules/ui/package-lock.json b/modules/ui/package-lock.json index 543076999..0d0668990 100644 --- a/modules/ui/package-lock.json +++ b/modules/ui/package-lock.json @@ -2679,262 +2679,6 @@ "node": ">=10.0.0" } }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.11.tgz", - "integrity": "sha512-FnzU0LyE3ySQk7UntJO4+qIiQgI7KoODnZg5xzXIrFJlKd2P2gwHsHY4927xj9y5PJmJSzULiUCWmv7iWnNa7g==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.11.tgz", - "integrity": "sha512-5OVapq0ClabvKvQ58Bws8+wkLCV+Rxg7tUVbo9xu034Nm536QTII4YzhaFriQ7rMrorfnFKUsArD2lqKbFY4vw==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.11.tgz", - "integrity": "sha512-aiu7K/5JnLj//KOnOfEZ0D90obUkRzDMyqd/wNAUQ34m4YUPVhRZpnqKV9uqDGxT7cToSDnIHsGooyIczu9T+Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.11.tgz", - "integrity": "sha512-eccxjlfGw43WYoY9QgB82SgGgDbibcqyDTlk3l3C0jOVHKxrjdc9CTwDUQd0vkvYg5um0OH+GpxYvp39r+IPOg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.11.tgz", - "integrity": "sha512-ETp87DRWuSt9KdDVkqSoKoLFHYTrkyz2+65fj9nfXsaV3bMhTCjtQfw3y+um88vGRKRiF7erPrh/ZuIdLUIVxQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.11.tgz", - "integrity": "sha512-fkFUiS6IUK9WYUO/+22omwetaSNl5/A8giXvQlcinLIjVkxwTLSktbF5f/kJMftM2MJp9+fXqZ5ezS7+SALp4g==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.11.tgz", - "integrity": "sha512-lhoSp5K6bxKRNdXUtHoNc5HhbXVCS8V0iZmDvyWvYq9S5WSfTIHU2UGjcGt7UeS6iEYp9eeymIl5mJBn0yiuxA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.11.tgz", - "integrity": "sha512-JkUqn44AffGXitVI6/AbQdoYAq0TEullFdqcMY/PCUZ36xJ9ZJRtQabzMA+Vi7r78+25ZIBosLTOKnUXBSi1Kw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.11.tgz", - "integrity": "sha512-3CRkr9+vCV2XJbjwgzjPtO8T0SZUmRZla+UL1jw+XqHZPkPgZiyWvbDvl9rqAN8Zl7qJF0O/9ycMtjU67HN9/Q==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.11.tgz", - "integrity": "sha512-LneLg3ypEeveBSMuoa0kwMpCGmpu8XQUh+mL8XXwoYZ6Be2qBnVtcDI5azSvh7vioMDhoJFZzp9GWp9IWpYoUg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.11.tgz", - "integrity": "sha512-caHy++CsD8Bgq2V5CodbJjFPEiDPq8JJmBdeyZ8GWVQMjRD0sU548nNdwPNvKjVpamYYVL40AORekgfIubwHoA==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.11.tgz", - "integrity": "sha512-ppZSSLVpPrwHccvC6nQVZaSHlFsvCQyjnvirnVjbKSHuE5N24Yl8F3UwYUUR1UEPaFObGD2tSvVKbvR+uT1Nrg==", - "cpu": [ - "loong64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.11.tgz", - "integrity": "sha512-B5x9j0OgjG+v1dF2DkH34lr+7Gmv0kzX6/V0afF41FkPMMqaQ77pH7CrhWeR22aEeHKaeZVtZ6yFwlxOKPVFyg==", - "cpu": [ - "mips64el" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.11.tgz", - "integrity": "sha512-MHrZYLeCG8vXblMetWyttkdVRjQlQUb/oMgBNurVEnhj4YWOr4G5lmBfZjHYQHHN0g6yDmCAQRR8MUHldvvRDA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.11.tgz", - "integrity": "sha512-f3DY++t94uVg141dozDu4CCUkYW+09rWtaWfnb3bqe4w5NqmZd6nPVBm+qbz7WaHZCoqXqHz5p6CM6qv3qnSSQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.11.tgz", - "integrity": "sha512-A5xdUoyWJHMMlcSMcPGVLzYzpcY8QP1RtYzX5/bS4dvjBGVxdhuiYyFwp7z74ocV7WDc0n1harxmpq2ePOjI0Q==", - "cpu": [ - "s390x" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, "node_modules/@esbuild/linux-x64": { "version": "0.19.11", "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.11.tgz", @@ -2951,102 +2695,6 @@ "node": ">=12" } }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.11.tgz", - "integrity": "sha512-13jvrQZJc3P230OhU8xgwUnDeuC/9egsjTkXN49b3GcS5BKvJqZn86aGM8W9pd14Kd+u7HuFBMVtrNGhh6fHEQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.11.tgz", - "integrity": "sha512-ysyOGZuTp6SNKPE11INDUeFVVQFrhcNDVUgSQVDzqsqX38DjhPEPATpid04LCoUr2WXhQTEZ8ct/EgJCUDpyNw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.11.tgz", - "integrity": "sha512-Hf+Sad9nVwvtxy4DXCZQqLpgmRTQqyFyhT3bZ4F2XlJCjxGmRFF0Shwn9rzhOYRB61w9VMXUkxlBy56dk9JJiQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.11.tgz", - "integrity": "sha512-0P58Sbi0LctOMOQbpEOvOL44Ne0sqbS0XWHMvvrg6NE5jQ1xguCSSw9jQeUk2lfrXYsKDdOe6K+oZiwKPilYPQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.11.tgz", - "integrity": "sha512-6YOrWS+sDJDmshdBIQU+Uoyh7pQKrdykdefC1avn76ss5c+RN6gut3LZA4E2cH5xUEp5/cA0+YxRaVtRAb0xBg==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.11.tgz", - "integrity": "sha512-vfkhltrjCAb603XaFhqhAF4LGDi2M4OrCRrFusyQ+iTLQ/o60QQXxc9cZC/FFpihBI9N1Grn6SMKVJ4KP7Fuiw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -4610,102 +4258,6 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true }, - "node_modules/@nx/nx-darwin-arm64": { - "version": "17.2.8", - "resolved": "https://registry.npmjs.org/@nx/nx-darwin-arm64/-/nx-darwin-arm64-17.2.8.tgz", - "integrity": "sha512-dMb0uxug4hM7tusISAU1TfkDK3ixYmzc1zhHSZwpR7yKJIyKLtUpBTbryt8nyso37AS1yH+dmfh2Fj2WxfBHTg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@nx/nx-darwin-x64": { - "version": "17.2.8", - "resolved": "https://registry.npmjs.org/@nx/nx-darwin-x64/-/nx-darwin-x64-17.2.8.tgz", - "integrity": "sha512-0cXzp1tGr7/6lJel102QiLA4NkaLCkQJj6VzwbwuvmuCDxPbpmbz7HC1tUteijKBtOcdXit1/MEoEU007To8Bw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@nx/nx-freebsd-x64": { - "version": "17.2.8", - "resolved": "https://registry.npmjs.org/@nx/nx-freebsd-x64/-/nx-freebsd-x64-17.2.8.tgz", - "integrity": "sha512-YFMgx5Qpp2btCgvaniDGdu7Ctj56bfFvbbaHQWmOeBPK1krNDp2mqp8HK6ZKOfEuDJGOYAp7HDtCLvdZKvJxzA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@nx/nx-linux-arm-gnueabihf": { - "version": "17.2.8", - "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm-gnueabihf/-/nx-linux-arm-gnueabihf-17.2.8.tgz", - "integrity": "sha512-iN2my6MrhLRkVDtdivQHugK8YmR7URo1wU9UDuHQ55z3tEcny7LV3W9NSsY9UYPK/FrxdDfevj0r2hgSSdhnzA==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@nx/nx-linux-arm64-gnu": { - "version": "17.2.8", - "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm64-gnu/-/nx-linux-arm64-gnu-17.2.8.tgz", - "integrity": "sha512-Iy8BjoW6mOKrSMiTGujUcNdv+xSM1DALTH6y3iLvNDkGbjGK1Re6QNnJAzqcXyDpv32Q4Fc57PmuexyysZxIGg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@nx/nx-linux-arm64-musl": { - "version": "17.2.8", - "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm64-musl/-/nx-linux-arm64-musl-17.2.8.tgz", - "integrity": "sha512-9wkAxWzknjpzdofL1xjtU6qPFF1PHlvKCZI3hgEYJDo4mQiatGI+7Ttko+lx/ZMP6v4+Umjtgq7+qWrApeKamQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, "node_modules/@nx/nx-linux-x64-gnu": { "version": "17.2.8", "resolved": "https://registry.npmjs.org/@nx/nx-linux-x64-gnu/-/nx-linux-x64-gnu-17.2.8.tgz", @@ -4738,38 +4290,6 @@ "node": ">= 10" } }, - "node_modules/@nx/nx-win32-arm64-msvc": { - "version": "17.2.8", - "resolved": "https://registry.npmjs.org/@nx/nx-win32-arm64-msvc/-/nx-win32-arm64-msvc-17.2.8.tgz", - "integrity": "sha512-XBWUY/F/GU3vKN9CAxeI15gM4kr3GOBqnzFZzoZC4qJt2hKSSUEWsMgeZtsMgeqEClbi4ZyCCkY7YJgU32WUGA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@nx/nx-win32-x64-msvc": { - "version": "17.2.8", - "resolved": "https://registry.npmjs.org/@nx/nx-win32-x64-msvc/-/nx-win32-x64-msvc-17.2.8.tgz", - "integrity": "sha512-HTqDv+JThlLzbcEm/3f+LbS5/wYQWzb5YDXbP1wi7nlCTihNZOLNqGOkEmwlrR5tAdNHPRpHSmkYg4305W0CtA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -4830,110 +4350,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.9.6.tgz", - "integrity": "sha512-MVNXSSYN6QXOulbHpLMKYi60ppyO13W9my1qogeiAqtjb2yR4LSmfU2+POvDkLzhjYLXz9Rf9+9a3zFHW1Lecg==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.9.6.tgz", - "integrity": "sha512-T14aNLpqJ5wzKNf5jEDpv5zgyIqcpn1MlwCrUXLrwoADr2RkWA0vOWP4XxbO9aiO3dvMCQICZdKeDrFl7UMClw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.9.6.tgz", - "integrity": "sha512-CqNNAyhRkTbo8VVZ5R85X73H3R5NX9ONnKbXuHisGWC0qRbTTxnF1U4V9NafzJbgGM0sHZpdO83pLPzq8uOZFw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.9.6.tgz", - "integrity": "sha512-zRDtdJuRvA1dc9Mp6BWYqAsU5oeLixdfUvkTHuiYOHwqYuQ4YgSmi6+/lPvSsqc/I0Omw3DdICx4Tfacdzmhog==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.9.6.tgz", - "integrity": "sha512-oNk8YXDDnNyG4qlNb6is1ojTOGL/tRhbbKeE/YuccItzerEZT68Z9gHrY3ROh7axDc974+zYAPxK5SH0j/G+QQ==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.9.6.tgz", - "integrity": "sha512-Z3O60yxPtuCYobrtzjo0wlmvDdx2qZfeAWTyfOjEDqd08kthDKexLpV97KfAeUXPosENKd8uyJMRDfFMxcYkDQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.9.6.tgz", - "integrity": "sha512-gpiG0qQJNdYEVad+1iAsGAbgAnZ8j07FapmnIAQgODKcOTjLEWM9sRb+MbQyVsYCnA0Im6M6QIq6ax7liws6eQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.9.6.tgz", - "integrity": "sha512-+uCOcvVmFUYvVDr27aiyun9WgZk0tXe7ThuzoUTAukZJOwS5MrGbmSlNOhx1j80GdpqbOty05XqSl5w4dQvcOA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, "node_modules/@rollup/rollup-linux-x64-gnu": { "version": "4.9.6", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.9.6.tgz", @@ -4960,45 +4376,6 @@ "linux" ] }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.9.6.tgz", - "integrity": "sha512-VD6qnR99dhmTQ1mJhIzXsRcTBvTjbfbGGwKAHcu+52cVl15AC/kplkhxzW/uT0Xl62Y/meBKDZvoJSJN+vTeGA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.9.6.tgz", - "integrity": "sha512-J9AFDq/xiRI58eR2NIDfyVmTYGyIZmRcvcAoJ48oDld/NTR8wyiPUu2X/v1navJ+N/FGg68LEbX3Ejd6l8B7MQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.9.6.tgz", - "integrity": "sha512-jqzNLhNDvIZOrt69Ce4UjGRpXJBzhUBzawMwnaDAwyHriki3XollsewxWzOzz+4yOFDkuJHtTsZFwMxhYJWmLQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, "node_modules/@schematics/angular": { "version": "17.0.10", "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-17.0.10.tgz", @@ -9404,20 +8781,6 @@ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "dev": true }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", diff --git a/modules/ui/src/app/components/general-settings/general-settings.component.html b/modules/ui/src/app/components/general-settings/general-settings.component.html deleted file mode 100644 index e69de29bb..000000000 diff --git a/modules/ui/src/app/interceptors/error.interceptor.ts b/modules/ui/src/app/interceptors/error.interceptor.ts index df5ad3fee..8fe5d0c23 100644 --- a/modules/ui/src/app/interceptors/error.interceptor.ts +++ b/modules/ui/src/app/interceptors/error.interceptor.ts @@ -29,6 +29,7 @@ import { TimeoutError, } from 'rxjs'; import { NotificationService } from '../services/notification.service'; + import { SYSTEM_STOP } from '../services/test-run.service'; const DEFAULT_TIMEOUT_MS = 5000; diff --git a/modules/ui/src/app/pages/devices/components/device-form/device-form.component.ts b/modules/ui/src/app/pages/devices/components/device-form/device-form.component.ts index 497a9067b..b05d2cb90 100644 --- a/modules/ui/src/app/pages/devices/components/device-form/device-form.component.ts +++ b/modules/ui/src/app/pages/devices/components/device-form/device-form.component.ts @@ -22,6 +22,7 @@ import { Validators, } from '@angular/forms'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; + import { Device, TestModule } from '../../../../model/device'; import { TestRunService } from '../../../../services/test-run.service'; import { DeviceValidators } from './device.validators'; diff --git a/modules/ui/src/app/pages/devices/components/device-form/device.validators.ts b/modules/ui/src/app/pages/devices/components/device-form/device.validators.ts index 288c05daa..2b049d7a0 100644 --- a/modules/ui/src/app/pages/devices/components/device-form/device.validators.ts +++ b/modules/ui/src/app/pages/devices/components/device-form/device.validators.ts @@ -17,7 +17,6 @@ import { Injectable } from '@angular/core'; import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms'; import { TestRunService } from '../../../../services/test-run.service'; import { Device } from '../../../../model/device'; - @Injectable({ providedIn: 'root' }) /** diff --git a/modules/ui/src/app/pages/testrun/components/progress-table/progress-table.component.spec.ts b/modules/ui/src/app/pages/testrun/components/progress-table/progress-table.component.spec.ts index 4281eb7e2..b88325367 100644 --- a/modules/ui/src/app/pages/testrun/components/progress-table/progress-table.component.spec.ts +++ b/modules/ui/src/app/pages/testrun/components/progress-table/progress-table.component.spec.ts @@ -16,6 +16,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ProgressTableComponent } from './progress-table.component'; + import { IResult, StatusOfTestResult } from '../../../../model/testrun-status'; import { of } from 'rxjs'; import { diff --git a/modules/ui/src/app/services/notification.service.ts b/modules/ui/src/app/services/notification.service.ts index 3c48c8d5d..3a068b4e7 100644 --- a/modules/ui/src/app/services/notification.service.ts +++ b/modules/ui/src/app/services/notification.service.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { Injectable } from '@angular/core'; import { MatSnackBar, diff --git a/modules/ui/src/app/services/test-run.service.spec.ts b/modules/ui/src/app/services/test-run.service.spec.ts index 840d9f3ba..43403f921 100644 --- a/modules/ui/src/app/services/test-run.service.spec.ts +++ b/modules/ui/src/app/services/test-run.service.spec.ts @@ -255,22 +255,6 @@ describe('TestRunService', () => { req.flush(reports); }); - - /* it('should return [] when error happens', () => { - let result: TestrunStatus[] | null = null; - - service.getHistory().subscribe(res => { - expect(res).toEqual(result); - }); - - result = []; - service.fetchHistory(); - const req = httpTestingController.expectOne({ - url: 'http://localhost:8000/reports', - }); - - req.flush([], { status: 500, statusText: 'error' }); - });*/ }); describe('#getResultClass', () => { @@ -414,39 +398,6 @@ describe('TestRunService', () => { req.flush({}); }); - /* it('removeReport should remove device from history list', fakeAsync(() => { - const reports = [ - { - status: 'Completed', - device: device, - report: 'https://api.testrun.io/report.pdf', - started: '2023-06-22T10:11:00.123Z', - finished: '2023-06-22T10:17:00.123Z', - }, - { - status: 'Completed', - device: device, - report: 'https://api.testrun.io/report.pdf', - started: '2023-07-22T10:11:00.123Z', - finished: '2023-07-22T10:17:00.123Z', - }, - ] as TestrunStatus[]; - - service.getHistory().next(reports); - tick(); - service.removeReport('00:1e:42:35:73:c4', '2023-06-22T10:11:00.123Z'); - - expect( - service - .getHistory() - .value?.some( - report => - report.device.mac_addr === '00:1e:42:35:73:c4' && - report.started === '2023-06-22T10:11:00.123Z' - ) - ).toEqual(false); - }));*/ - it('#saveDevice should have necessary request data', () => { const apiUrl = 'http://localhost:8000/device'; diff --git a/modules/ui/src/app/services/test-run.service.ts b/modules/ui/src/app/services/test-run.service.ts index 0a54f04f4..b8de6b0b8 100644 --- a/modules/ui/src/app/services/test-run.service.ts +++ b/modules/ui/src/app/services/test-run.service.ts @@ -28,7 +28,7 @@ import { } from '../model/testrun-status'; import { Version } from '../model/version'; -const API_URL = 'http://localhost:8000'; +const API_URL = `http://${window.location.hostname}:8000`; export const SYSTEM_STOP = '/system/stop'; @Injectable({ diff --git a/modules/ui/ui.Dockerfile b/modules/ui/ui.Dockerfile index 2b4081470..da56be93e 100644 --- a/modules/ui/ui.Dockerfile +++ b/modules/ui/ui.Dockerfile @@ -13,14 +13,14 @@ # limitations under the License. # Image name: test-run/ui -FROM node:20 as build +FROM node@sha256:ffebb4405810c92d267a764b21975fb2d96772e41877248a37bf3abaa0d3b590 as build WORKDIR /modules/ui COPY modules/ui/ /modules/ui RUN npm install RUN npm run build -FROM nginx:1.25.1 +FROM nginx@sha256:4c0fdaa8b6341bfdeca5f18f7837462c80cff90527ee35ef185571e1c327beac COPY --from=build /modules/ui/dist/ /usr/share/nginx/html diff --git a/testing/api/test_api.py b/testing/api/test_api.py index c6368148c..44daef335 100644 --- a/testing/api/test_api.py +++ b/testing/api/test_api.py @@ -231,7 +231,7 @@ def test_get_system_interfaces(testrun): r = requests.get(f"{API}/system/interfaces") response = json.loads(r.text) local_interfaces = get_network_interfaces() - assert set(response) == set(local_interfaces) + assert set(response.keys()) == set(local_interfaces) # schema expects a flat list assert all([isinstance(x, str) for x in response]) @@ -262,13 +262,18 @@ def test_modify_device(testing_devices, testrun): } updated_device["test_modules"] = new_test_modules + updated_device_payload = {} + updated_device_payload["device"] = updated_device + updated_device_payload["mac_addr"] = mac_addr + print("updated_device") pretty_print(updated_device) print("api_device") pretty_print(api_device) # update device - r = requests.post(f"{API}/device", data=json.dumps(updated_device)) + r = requests.post(f"{API}/device/edit", + data=json.dumps(updated_device_payload)) assert r.status_code == 200 diff --git a/testing/baseline/test_baseline b/testing/baseline/test_baseline index 7ecc6ba19..c52c08aef 100755 --- a/testing/baseline/test_baseline +++ b/testing/baseline/test_baseline @@ -23,7 +23,7 @@ ifconfig sudo apt-get update sudo apt-get install openvswitch-common openvswitch-switch tcpdump jq moreutils coreutils isc-dhcp-client -pip3 install pytest +pip3 install pytest==7.4.4 # Setup device network sudo ip link add dev endev0a type veth peer name endev0b diff --git a/testing/docker/ci_baseline/Dockerfile b/testing/docker/ci_baseline/Dockerfile index 468c6f7a0..93ad905f9 100644 --- a/testing/docker/ci_baseline/Dockerfile +++ b/testing/docker/ci_baseline/Dockerfile @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -FROM ubuntu:jammy +FROM ubuntu@sha256:e6173d4dc55e76b87c4af8db8821b1feae4146dd47341e4d431118c7dd060a74 # Update and get all additional requirements not contained in the base image RUN apt-get update && apt-get -y upgrade diff --git a/testing/docker/ci_test_device1/Dockerfile b/testing/docker/ci_test_device1/Dockerfile index 1c62d231d..0279df5ef 100644 --- a/testing/docker/ci_test_device1/Dockerfile +++ b/testing/docker/ci_test_device1/Dockerfile @@ -1,5 +1,5 @@ -FROM ubuntu:jammy +FROM ubuntu@sha256:e6173d4dc55e76b87c4af8db8821b1feae4146dd47341e4d431118c7dd060a74 ENV DEBIAN_FRONTEND=noninteractive diff --git a/testing/pylint/test_pylint b/testing/pylint/test_pylint index 6d28226cc..82949d909 100755 --- a/testing/pylint/test_pylint +++ b/testing/pylint/test_pylint @@ -19,7 +19,7 @@ ERROR_LIMIT=175 sudo cmd/install source venv/bin/activate -sudo pip3 install pylint +sudo pip3 install pylint==3.0.3 files=$(find ./framework -path ./venv -prune -o -name '*.py' -print) diff --git a/testing/tests/test_tests b/testing/tests/test_tests index 9fad12d80..3b509f702 100755 --- a/testing/tests/test_tests +++ b/testing/tests/test_tests @@ -27,7 +27,7 @@ mkdir -p $TEST_DIR sudo apt-get update sudo apt-get install -y openvswitch-common openvswitch-switch tcpdump jq moreutils coreutils isc-dhcp-client -pip3 install pytest +pip3 install pytest==7.4.4 # Start OVS # Setup device network diff --git a/testing/unit/run_tests.sh b/testing/unit/run_tests.sh index 18ef79409..c42923a2d 100644 --- a/testing/unit/run_tests.sh +++ b/testing/unit/run_tests.sh @@ -1,5 +1,19 @@ #!/bin/bash -e +# 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. + # This script should be run from within the unit_test directory. If # it is run outside this directory, paths will not be resolved correctly. @@ -10,6 +24,7 @@ echo "Root Dir: $PWD" # Add the framework sources PYTHONPATH="$PWD/framework/python/src:$PWD/framework/python/src/common" + # Add the test module sources PYTHONPATH="$PYTHONPATH:$PWD/modules/test/base/python/src" PYTHONPATH="$PYTHONPATH:$PWD/modules/test/tls/python/src" diff --git a/testing/unit/tls/monitor.pcap b/testing/unit/tls/monitor.pcap new file mode 100644 index 000000000..52f5034de Binary files /dev/null and b/testing/unit/tls/monitor.pcap differ diff --git a/testing/unit/tls/no_tls.pcap b/testing/unit/tls/no_tls.pcap new file mode 100644 index 000000000..557e66b1a Binary files /dev/null and b/testing/unit/tls/no_tls.pcap differ diff --git a/testing/unit/tls/tls_module_test.py b/testing/unit/tls/tls_module_test.py index 661ff817c..baed9c395 100644 --- a/testing/unit/tls/tls_module_test.py +++ b/testing/unit/tls/tls_module_test.py @@ -90,6 +90,87 @@ def security_tls_v1_2_for_1_3_and_1_2_fail_server_test(self): tls_1_3_results) self.assertTrue(test_results[0]) + def security_tls_server_results_test(self, ): + # Generic messages to test they are passing through + # to the results as expected + fail_message = 'Certificate not validated' + success_message = 'Certificate validated' + none_message = 'Failed to resolve public certificate' + + # Both None + tls_1_2_results = None, none_message + tls_1_3_results = None, none_message + expected = None, (f'TLS 1.2 not validated: {none_message}\n' + f'TLS 1.3 not validated: {none_message}') + result = TLS_UTIL.process_tls_server_results(tls_1_2_results, + tls_1_3_results) + self.assertEqual(result, expected) + + # TLS 1.2 Pass and TLS 1.3 None + tls_1_2_results = True, success_message + expected = True, f'TLS 1.2 validated: {success_message}' + result = TLS_UTIL.process_tls_server_results(tls_1_2_results, + tls_1_3_results) + self.assertEqual(result, expected) + + # TLS 1.2 Fail and TLS 1.3 None + tls_1_2_results = False, fail_message + expected = False, f'TLS 1.2 not validated: {fail_message}' + result = TLS_UTIL.process_tls_server_results(tls_1_2_results, + tls_1_3_results) + self.assertEqual(result, expected) + + # TLS 1.3 Pass and TLS 1.2 None + tls_1_2_results = None, fail_message + tls_1_3_results = True, success_message + expected = True, f'TLS 1.3 validated: {success_message}' + result = TLS_UTIL.process_tls_server_results(tls_1_2_results, + tls_1_3_results) + self.assertEqual(result, expected) + + # TLS 1.3 Fail and TLS 1.2 None + tls_1_3_results = False, fail_message + expected = False, f'TLS 1.3 not validated: {fail_message}' + result = TLS_UTIL.process_tls_server_results(tls_1_2_results, + tls_1_3_results) + self.assertEqual(result, expected) + + # TLS 1.2 Pass and TLS 1.3 Pass + tls_1_2_results = True, success_message + tls_1_3_results = True, success_message + expected = True, (f'TLS 1.2 validated: {success_message}\n' + f'TLS 1.3 validated: {success_message}') + result = TLS_UTIL.process_tls_server_results(tls_1_2_results, + tls_1_3_results) + self.assertEqual(result, expected) + + # TLS 1.2 Pass and TLS 1.3 Fail + tls_1_2_results = True, success_message + tls_1_3_results = False, fail_message + expected = True, (f'TLS 1.2 validated: {success_message}\n' + f'TLS 1.3 not validated: {fail_message}') + result = TLS_UTIL.process_tls_server_results(tls_1_2_results, + tls_1_3_results) + self.assertEqual(result, expected) + + # TLS 1.2 Fail and TLS 1.2 Pass + tls_1_2_results = False, fail_message + tls_1_3_results = True, success_message + expected = True, (f'TLS 1.2 not validated: {fail_message}\n' + f'TLS 1.3 validated: {success_message}') + result = TLS_UTIL.process_tls_server_results(tls_1_2_results, + tls_1_3_results) + self.assertEqual(result, expected) + + + # TLS 1.2 Fail and TLS 1.2 Fail + tls_1_3_results = False, fail_message + expected = False, (f'TLS 1.2 not validated: {fail_message}\n' + f'TLS 1.3 not validated: {fail_message}') + result = TLS_UTIL.process_tls_server_results(tls_1_2_results, + tls_1_3_results) + self.assertEqual(result, expected) + # Test 1.2 server when 1.3 and 1.2 failed connection is established def security_tls_v1_2_fail_server_test(self): tls_1_2_results = False, 'Signature faild' @@ -120,10 +201,16 @@ def security_tls_v1_2_client_cipher_fail_test(self): print(str(test_results)) self.assertFalse(test_results[0]) + # Scan a known capture without any TLS traffic to + # generate a skip result def security_tls_client_skip_test(self): - # 1.1 will fail to connect and so no hello client will exist - # which should result in a skip result - test_results = self.test_client_tls('1.2', tls_generate='1.1') + print('security_tls_client_skip_test') + capture_file = os.path.join(TEST_FILES_DIR, 'no_tls.pcap') + + # Run the client test + test_results = TLS_UTIL.validate_tls_client(client_ip='172.27.253.167', + tls_version='1.2', + capture_files=[capture_file]) print(str(test_results)) self.assertIsNone(test_results[0]) @@ -176,7 +263,31 @@ def test_client_tls(self, # Run the client test return TLS_UTIL.validate_tls_client(client_ip=client_ip, tls_version=tls_version, - capture_file=capture_file) + capture_files=[capture_file]) + + def test_client_tls_with_non_tls_client(self): + print('\ntest_client_tls_with_non_tls_client') + capture_file = os.path.join(TEST_FILES_DIR, 'monitor.pcap') + + # Run the client test + test_results = TLS_UTIL.validate_tls_client(client_ip='10.10.10.14', + tls_version='1.2', + capture_files=[capture_file]) + print(str(test_results)) + self.assertFalse(test_results[0]) + + # Scan a known capture without u unsupported TLS traffic to + # generate a fail result + def security_tls_client_unsupported_tls_client(self): + print('\nsecurity_tls_client_unsupported_tls_client') + capture_file = os.path.join(TEST_FILES_DIR, 'unsupported_tls.pcap') + + # Run the client test + test_results = TLS_UTIL.validate_tls_client(client_ip='172.27.253.167', + tls_version='1.2', + capture_files=[capture_file]) + print(str(test_results)) + self.assertFalse(test_results[0]) def generate_tls_traffic(self, capture_file, @@ -263,7 +374,6 @@ def get_interface_ip(self, interface_name): print(f'Error: {e}') return None - if __name__ == '__main__': suite = unittest.TestSuite() suite.addTest(TLSModuleTest('client_hello_packets_test')) @@ -277,12 +387,18 @@ def get_interface_ip(self, interface_name): TLSModuleTest('security_tls_v1_2_for_1_3_and_1_2_fail_server_test')) suite.addTest(TLSModuleTest('security_tls_v1_2_fail_server_test')) suite.addTest(TLSModuleTest('security_tls_v1_2_none_server_test')) - # # TLS 1.3 server tests + + # TLS 1.3 server tests suite.addTest(TLSModuleTest('security_tls_v1_3_server_test')) # TLS client tests suite.addTest(TLSModuleTest('security_tls_v1_2_client_test')) suite.addTest(TLSModuleTest('security_tls_v1_3_client_test')) suite.addTest(TLSModuleTest('security_tls_client_skip_test')) suite.addTest(TLSModuleTest('security_tls_v1_2_client_cipher_fail_test')) + suite.addTest(TLSModuleTest('test_client_tls_with_non_tls_client')) + suite.addTest(TLSModuleTest('security_tls_client_unsupported_tls_client')) + + suite.addTest(TLSModuleTest('security_tls_server_results_test')) + runner = unittest.TextTestRunner() runner.run(suite) diff --git a/testing/unit/tls/unsupported_tls.pcap b/testing/unit/tls/unsupported_tls.pcap new file mode 100644 index 000000000..08e6c3c04 Binary files /dev/null and b/testing/unit/tls/unsupported_tls.pcap differ