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
@@ -374,8 +374,8 @@ def generate_header(self, json_data):
tr_img_b64 = base64.b64encode(f.read()).decode('utf-8')
return f'''
'''
@@ -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