diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml
index 99bfa4e96..eae056eca 100644
--- a/.github/workflows/package.yml
+++ b/.github/workflows/package.yml
@@ -49,6 +49,20 @@ jobs:
- name: Install package
shell: bash {0}
run: sudo apt install ./testrun*.deb
+ - name: Start testrun
+ shell: bash {0}
+ run: sudo testrun > >(tee testrun_output.log) 2>&1 &
+ - name: Verify testrun started
+ shell: bash {0}
+ run: |
+ sleep 5
+ if grep -q "API waiting for requests" testrun_output.log; then
+ echo "Testrun started successfully."
+ else
+ echo "Testrun did not start correctly."
+ cat testrun_output.log
+ exit 1
+ fi
install_package_22:
permissions: {}
@@ -69,6 +83,20 @@ jobs:
- name: Install package
shell: bash {0}
run: sudo apt install ./testrun*.deb
+ - name: Start testrun
+ shell: bash {0}
+ run: sudo testrun > >(tee testrun_output.log) 2>&1 &
+ - name: Verify testrun started
+ shell: bash {0}
+ run: |
+ sleep 5
+ if grep -q "API waiting for requests" testrun_output.log; then
+ echo "Testrun started successfully."
+ else
+ echo "Testrun did not start correctly."
+ cat testrun_output.log
+ exit 1
+ fi
install_package_24:
permissions: {}
@@ -89,3 +117,17 @@ jobs:
- name: Install package
shell: bash {0}
run: sudo apt install ./testrun*.deb
+ - name: Start testrun
+ shell: bash {0}
+ run: sudo testrun > >(tee testrun_output.log) 2>&1 &
+ - name: Verify testrun started
+ shell: bash {0}
+ run: |
+ sleep 5
+ if grep -q "API waiting for requests" testrun_output.log; then
+ echo "Testrun started successfully."
+ else
+ echo "Testrun did not start correctly."
+ cat testrun_output.log
+ exit 1
+ fi
diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml
index 2aaa55b25..9ba417f9f 100644
--- a/.github/workflows/testing.yml
+++ b/.github/workflows/testing.yml
@@ -97,16 +97,13 @@ jobs:
- name: Run tests for reports
shell: bash {0}
run: bash testing/unit/run_report_test.sh testing/unit/report/report_test.py
- - name: Archive HTML reports for modules
- if: ${{ always() }}
- run: sudo tar --exclude-vcs -czf html_reports.tgz testing/unit/report/output/
- - name: Upload HTML reports
+ - name: Upload reports
uses: actions/upload-artifact@694cdabd8bdb0f10b2cea11669e1bf5453eed0a6 # v4.2.0
if: ${{ always() }}
with:
if-no-files-found: error
- name: html-reports_${{ github.run_id }}
- path: html_reports.tgz
+ name: reports_${{ github.run_id }}
+ path: testing/unit/report/output
pylint:
permissions: {}
@@ -138,7 +135,7 @@ jobs:
- name: Run tests
run: |
export CHROME_BIN=/usr/bin/chromium-browser
- CI=true npm run test-headless
+ CI=true npm run test-ci
env:
CI: true
working-directory: ./modules/ui
diff --git a/.gitignore b/.gitignore
index 92779dc04..f1c4ea203 100644
--- a/.gitignore
+++ b/.gitignore
@@ -18,6 +18,11 @@ testing/unit/report/output/
testing/unit/risk_profile/output/
testing/unit/services/output/
+# Ignore generated files from requirements generation
+*requirements_freeze.txt
+*unique_freeze.txt
+*requirements_gen.txt
+
*.deb
make/DEBIAN/postinst
diff --git a/README.md b/README.md
index ce9602637..8f1739031 100644
--- a/README.md
+++ b/README.md
@@ -35,7 +35,7 @@ Testrun provides the network and assistive tools for engineers when manual testi
## Hardware
- PC running Ubuntu LTS 20.04, 22.04, or 24.04 (laptop or desktop)
-- 2x USB Ethernet adapter (one may be built-in Ethernet)
+- 2x ethernet ports (USB ethernet adapters work too)
- Internet connection
## Software
@@ -72,7 +72,7 @@ We strongly encourage contributions from the community. Review the requirements
You can resolve most issues by reinstalling Testrun using these commands:
- `sudo docker system prune -a`
-- `sudo apt install ./testrun-*.deb`
+- `sudo apt install ./testrun*.deb`
If this doesn't resolve the problem, [raise an issue](https://github.com/google/testrun/issues).
diff --git a/cmd/build_ui b/cmd/build_ui
index afb0d8827..17ccf0c3c 100755
--- a/cmd/build_ui
+++ b/cmd/build_ui
@@ -30,8 +30,8 @@ docker kill tr-ui-build 2> /dev/null || true
echo "Building the user interface"
-# Start build container and build the ui dist
-docker run --rm -v $PWD/modules/ui:/modules/ui testrun/build-ui /bin/sh -c "npm install && npm run build"
+# Start build container and build the ui dist
+docker run --rm -v "$(pwd)"/modules/ui:/modules/ui testrun/build-ui /bin/sh -c "npm install && npm run build"
# Kill the container (Should not be running anymore)
docker kill tr-ui-build 2> /dev/null || true
diff --git a/cmd/install b/cmd/install
index 906550abf..baf0f5469 100755
--- a/cmd/install
+++ b/cmd/install
@@ -74,7 +74,7 @@ mkdir -p local/{devices,root_certs,risk_profiles}
# This does not work on GitHub actions
if logname ; then
USER_NAME=$(logname)
- sudo chown -R "$USER_NAME" local || true
+ sudo chown -R "$USER_NAME" local resources || true
fi
echo Finished installing Testrun
diff --git a/cmd/package b/cmd/package
index 719258a83..8897e947e 100755
--- a/cmd/package
+++ b/cmd/package
@@ -22,6 +22,12 @@ if [[ "$EUID" == 0 ]]; then
exit 1
fi
+# Check that user is in docker group
+if ! (id -nGz "$USER" | grep -qzxF "docker"); then
+ echo User is not in docker group. Follow https://docs.docker.com/engine/install/linux-postinstall/ to finish setting up docker.
+ exit 1
+fi
+
MAKE_SRC_DIR=make
MAKE_CONTROL_DIR=make/DEBIAN/control
diff --git a/cmd/update_requirements b/cmd/update_requirements
new file mode 100755
index 000000000..f185f87d9
--- /dev/null
+++ b/cmd/update_requirements
@@ -0,0 +1,115 @@
+# #!/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.
+
+# update_requirements(){
+# modules_dir="$1"
+# for dir in $modules_dir/* ; do
+# echo "dir: $dir"
+# module=$(basename "$dir")
+# MODULE_DIR="$PWD/$modules_dir/$module/python"
+# IMAGE=testrun/$module
+# # Check if updating the test modules
+# if [[ $modules_dir == *test* ]]; then
+# # Append '-test' to the variable
+# IMAGE="${IMAGE}-test"
+# fi
+# echo "Module dir: $MODULE_DIR"
+# echo "Image: $IMAGE"
+# echo Updating requirements for module $modules_dir/$module...
+
+# if [ -e "$MODULE_DIR/requirements.txt" ]; then
+# if docker run --rm -v "$PWD/$modules_dir/$module/python/:/testrun/python/" --entrypoint /bin/bash $IMAGE -c "pip3 freeze > /testrun/python/requirements_freeze.txt" ; then
+# echo Successfully built requirements file for module $modules_dir/$module
+
+# # Normalize line endings and remove extra spaces
+# dos2unix "$MODULE_DIR/requirements.txt" "$MODULE_DIR/requirements_freeze.txt"
+# sed -i 's/^[ \t]*//;s/[ \t]*$//' "$MODULE_DIR/requirements.txt" "$MODULE_DIR/requirements_freeze.txt"
+
+# # Temporary file to store unique packages
+# > "$MODULE_DIR/unique_freeze.txt"
+
+# # Find unique packages in requirements_freeze.txt that are not in requirements.txt
+# while IFS= read -r freeze_line; do
+# # Extract the package name from freeze_line
+# freeze_package=$(echo "$freeze_line" | cut -d'=' -f1 | xargs)
+
+# echo "Frozen package: $freeze_package"
+# # Search for the package name in requirements.txt, ignoring case and whitespace
+# if ! grep -iq "^${freeze_package}$" "$MODULE_DIR/requirements.txt"; then
+# echo "$freeze_line" >> "$MODULE_DIR/unique_freeze.txt"
+# fi
+# done < "$MODULE_DIR/requirements_freeze.txt"
+
+# # Temporary file to store generated requirements
+# > "$MODULE_DIR/requirements_gen.txt"
+
+# # Add the downstream packages at the top of requirements_gen.txt
+# # so we pull in package dependencies before the defined dependency
+# # to prevent auto-upgrades in the package pipeline
+# echo "# Dependencies to user defined packages" > "$MODULE_DIR/requirements_gen.txt"
+# echo -e "# Package dependencies should always be defined before the user defined" >> "$MODULE_DIR/requirements_gen.txt"
+# echo -e "# packages to prevent auto-upgrades of stable dependencies" >> "$MODULE_DIR/requirements_gen.txt"
+# cat "$MODULE_DIR/unique_freeze.txt" >> "$MODULE_DIR/requirements_gen.txt"
+
+# # Create the requirements_gen.txt file
+# echo -e "\n# User defined packages" >> "$MODULE_DIR/requirements_gen.txt"
+
+# # Loop through each package in requirements.txt
+# while IFS= read -r package; do
+# # Trim leading and trailing whitespace from package
+# package=$(echo "$package" | xargs)
+# echo "Package: $package"
+
+# # Extract the base package name (without version or any comparison operator)
+# base_package=$(echo "$package" | sed -E 's/[<>=!].*//')
+# echo "Base package: $base_package"
+
+# # Check if the base package is a comment and non-empty
+# if [[ -n "$base_package" && "$base_package" != \#* ]]; then
+# # Perform the grep only if it is not a comment
+# versioned_package=$(grep -i "^${base_package}==" "$MODULE_DIR/requirements_freeze.txt")
+# else
+# # Set versioned_package to empty if it's a comment
+# versioned_package=""
+# fi
+
+# # Debug output: Print the result of matching
+# echo "Versioned Package: '$versioned_package'"
+
+# if [ -n "$versioned_package" ]; then
+# # If the package with the version is found, add it to requirements_gen.txt
+# echo "$versioned_package" >> "$MODULE_DIR/requirements_gen.txt"
+# else
+# # If not found, just add the package as is from requirements.txt
+# echo "$package" >> "$MODULE_DIR/requirements_gen.txt"
+# fi
+# done < "$MODULE_DIR/requirements.txt"
+# echo "Module done"
+
+# else
+# echo An error occurred while building requirements file for network module $module
+# exit 1
+# fi
+# else
+# echo No requirements.txt file defined for this module
+# fi
+# done
+# }
+# echo Updating python requirements in network modules
+# update_requirements modules/network
+
+# echo Updating python requirements in test modules
+# update_requirements modules/test
diff --git a/docs/dev/postman.json b/docs/dev/postman.json
index 39e4529b3..642090dd4 100644
--- a/docs/dev/postman.json
+++ b/docs/dev/postman.json
@@ -1,9 +1,10 @@
{
"info": {
- "_postman_id": "3d270980-478e-4243-9130-ee5cab278ed5",
+ "_postman_id": "f42dd4c6-e1a3-4ae3-a991-24cceb4d2627",
"name": "Testrun",
+ "description": "API endpoints for the Testrun API",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
- "_exporter_id": "28312403"
+ "_exporter_id": "37950585"
},
"item": [
{
@@ -21,11 +22,12 @@
"system",
"interfaces"
]
- }
+ },
+ "description": "Obtain a list of applicable and available network adapters for use in testing"
},
"response": [
{
- "name": "Interfaces",
+ "name": "Interfaces Available (200)",
"originalRequest": {
"method": "GET",
"header": [],
@@ -43,10 +45,100 @@
},
"status": "OK",
"code": 200,
- "_postman_previewlanguage": null,
- "header": null,
+ "_postman_previewlanguage": "json",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json",
+ "description": "",
+ "type": "text"
+ }
+ ],
"cookie": [],
"body": "{\n \"enx00e04c020fa8\": \"00:e0:4c:02:0f:a8\",\n \"enx207bd26205e9\": \"20:7b:d2:62:05:e9\"\n}"
+ },
+ {
+ "name": "No interfaces (200)",
+ "originalRequest": {
+ "method": "GET",
+ "header": [],
+ "url": {
+ "raw": "localhost:8000/system/interfaces",
+ "host": [
+ "localhost"
+ ],
+ "port": "8000",
+ "path": [
+ "system",
+ "interfaces"
+ ]
+ }
+ },
+ "status": "OK",
+ "code": 200,
+ "_postman_previewlanguage": "json",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json",
+ "description": "",
+ "type": "text"
+ }
+ ],
+ "cookie": [],
+ "body": "{}"
+ }
+ ]
+ },
+ {
+ "name": "Get Configuration",
+ "request": {
+ "method": "GET",
+ "header": [],
+ "url": {
+ "raw": "localhost:8000/system/config",
+ "host": [
+ "localhost"
+ ],
+ "port": "8000",
+ "path": [
+ "system",
+ "config"
+ ]
+ },
+ "description": "Get the current system configuration"
+ },
+ "response": [
+ {
+ "name": "Get Configuration (200)",
+ "originalRequest": {
+ "method": "GET",
+ "header": [],
+ "url": {
+ "raw": "localhost:8000/system/config",
+ "host": [
+ "localhost"
+ ],
+ "port": "8000",
+ "path": [
+ "system",
+ "config"
+ ]
+ }
+ },
+ "status": "OK",
+ "code": 200,
+ "_postman_previewlanguage": "json",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json",
+ "description": "",
+ "type": "text"
+ }
+ ],
+ "cookie": [],
+ "body": "{\n \"network\": {\n \"device_intf\": \"enx207bd2620617\",\n \"internet_intf\": \"enx207bd26205e9\"\n },\n \"log_level\": \"INFO\",\n \"startup_timeout\": 60,\n \"monitor_period\": 60,\n \"runtime\": 120\n}"
}
]
},
@@ -57,7 +149,7 @@
"header": [],
"body": {
"mode": "raw",
- "raw": "{\n \"network\": {\n \"device_intf\": \"enx207bd2620617\",\n \"internet_intf\": \"enx207bd26205e9\"\n },\n \"log_level\": \"DEBUG\",\n \"startup_timeout\": 60,\n \"monitor_period\": 20,\n \"runtime\": 120\n}",
+ "raw": "{\n \"network\": {\n \"device_intf\": \"enx207bd2620617\",\n \"internet_intf\": \"enx207bd26205e9\"\n },\n \"log_level\": \"DEBUG\",\n \"startup_timeout\": 60,\n \"monitor_period\": 20,\n \"runtime\": 120,\n \"org_name\": \"Google\"\n}",
"options": {
"raw": {
"language": "json"
@@ -74,17 +166,18 @@
"system",
"config"
]
- }
+ },
+ "description": "Update the current system configuration"
},
"response": [
{
- "name": "Configuration Set",
+ "name": "Configuration Set (200)",
"originalRequest": {
"method": "POST",
"header": [],
"body": {
"mode": "raw",
- "raw": "{\n \"network\": {\n \"device_intf\": \"enx00e04c020fa8\",\n \"internet_intf\": \"enx207bd26205e9\"\n },\n \"log_level\": \"DEBUG\",\n \"runtime\": 120,\n \"startup_timeout\": 60,\n \"monitor_period\": 60 \n}",
+ "raw": "{\n \"network\": {\n \"device_intf\": \"enx00e04c020fa8\",\n \"internet_intf\": \"enx207bd26205e9\"\n },\n \"log_level\": \"DEBUG\",\n \"runtime\": 120, // Optional\n \"startup_timeout\": 60, // Optional\n \"monitor_period\": 60 // Optional\n}",
"options": {
"raw": {
"language": "json"
@@ -105,13 +198,20 @@
},
"status": "OK",
"code": 200,
- "_postman_previewlanguage": null,
- "header": null,
+ "_postman_previewlanguage": "json",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json",
+ "description": "",
+ "type": "text"
+ }
+ ],
"cookie": [],
- "body": "{\n \"network\": {\n \"device_intf\": \"enx00e04c020fa8\",\n \"internet_intf\": \"enx207bd26205e9\"\n },\n \"log_level\": \"DEBUG\",\n \"runtime\": 120,\n \"startup_timeout\": 60,\n \"monitor_period\": 60 \n}"
+ "body": "{\n \"network\": {\n \"device_intf\": \"enx00e04c020fa8\",\n \"internet_intf\": \"enx207bd26205e9\"\n },\n \"log_level\": \"DEBUG\",\n \"runtime\": 120,\n \"startup_timeout\": 60,\n \"monitor_period\": 60\n}"
},
{
- "name": "Invalid JSON",
+ "name": "Invalid JSON (400)",
"originalRequest": {
"method": "POST",
"header": [],
@@ -138,8 +238,15 @@
},
"status": "Bad Request",
"code": 400,
- "_postman_previewlanguage": null,
- "header": null,
+ "_postman_previewlanguage": "json",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json",
+ "description": "",
+ "type": "text"
+ }
+ ],
"cookie": [],
"body": "{\n \"error\": \"Invalid JSON received\"\n}"
}
@@ -152,7 +259,7 @@
"header": [],
"body": {
"mode": "raw",
- "raw": "{\n \"device\": {\n \"mac_addr\": \"00:1e:42:35:73:c4\",\n \"firmware\": \"1.2.2\"\n }\n}",
+ "raw": "{\n \"device\": {\n \"mac_addr\": \"aa:bb:cc:dd:ee:ff\",\n \"firmware\": \"1.2.2\",\n \"test_modules\": {\n \"protocol\": {\n \"enabled\": true\n },\n \"services\": {\n \"enabled\": false\n },\n \"ntp\": {\n \"enabled\": true\n },\n \"tls\": {\n \"enabled\": false\n },\n \"connection\": {\n \"enabled\": true\n },\n \"dns\": {\n \"enabled\": true\n }\n }\n }\n}",
"options": {
"raw": {
"language": "json"
@@ -169,11 +276,52 @@
"system",
"start"
]
- }
+ },
+ "description": "Start Testrun against a target device"
},
"response": [
{
- "name": "Invalid request",
+ "name": "Starting (200)",
+ "originalRequest": {
+ "method": "POST",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"device\": {\n \"mac_addr\": \"aa:bb:cc:dd:ee:ff\",\n \"firmware\": \"1.2.2\",\n \"test_modules\": {\n \"protocol\": {\n \"enabled\": true\n },\n \"services\": {\n \"enabled\": false\n },\n \"ntp\": {\n \"enabled\": true\n },\n \"tls\": {\n \"enabled\": false\n },\n \"connection\": {\n \"enabled\": true\n },\n \"dns\": {\n \"enabled\": true\n }\n }\n }\n}",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "localhost:8000/system/start",
+ "host": [
+ "localhost"
+ ],
+ "port": "8000",
+ "path": [
+ "system",
+ "start"
+ ]
+ }
+ },
+ "status": "OK",
+ "code": 200,
+ "_postman_previewlanguage": "json",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json",
+ "description": "",
+ "type": "text"
+ }
+ ],
+ "cookie": [],
+ "body": "{\n \"status\": \"Waiting for Device\",\n \"device\": {\n \"mac_addr\": \"aa:bb:cc:dd:ee:ff\",\n \"manufacturer\": \"Teltonika\",\n \"model\": \"TRB140\",\n \"firmware\": \"1.2.2\"\n },\n \"started\": \"2023-07-18T11:14:42.917670\",\n \"finished\": null, \n \"report\": null,\n \"tests\": {\n \"total\": 0,\n \"results\": []\n }\n}"
+ },
+ {
+ "name": "Invalid Request (400)",
"originalRequest": {
"method": "POST",
"header": [],
@@ -200,19 +348,26 @@
},
"status": "Bad Request",
"code": 400,
- "_postman_previewlanguage": null,
- "header": null,
+ "_postman_previewlanguage": "json",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json",
+ "description": "",
+ "type": "text"
+ }
+ ],
"cookie": [],
"body": "{\n \"error\": \"Invalid request received\"\n}"
},
{
- "name": "Starting",
+ "name": "Invalid JSON (400)",
"originalRequest": {
"method": "POST",
"header": [],
"body": {
"mode": "raw",
- "raw": "{\n \"device\": {\n \"mac_addr\": \"aa:bb:cc:dd:ee:ff\",\n \"firmware\": \"1.2.2\"\n }\n}",
+ "raw": "{\n \"device\": {\n \"mac_addr\": \"aa:bb:cc:dd:ee:ff,\n \"firmware\": \"1.2.2\"\n }\n}",
"options": {
"raw": {
"language": "json"
@@ -231,15 +386,22 @@
]
}
},
- "status": "OK",
- "code": 200,
- "_postman_previewlanguage": null,
- "header": null,
+ "status": "Bad Request",
+ "code": 400,
+ "_postman_previewlanguage": "json",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json",
+ "description": "",
+ "type": "text"
+ }
+ ],
"cookie": [],
- "body": "{\n \"status\": \"Starting\",\n \"device\": {\n \"mac_addr\": \"00:1e:42:35:73:c4\",\n \"manufacturer\": \"Teltonika\",\n \"model\": \"TRB 140\",\n \"test_modules\": {\n \"dns\": {\n \"enabled\": false\n },\n \"connection\": {\n \"enabled\": true\n },\n \"ntp\": {\n \"enabled\": false\n },\n \"baseline\": {\n \"enabled\": false\n },\n \"nmap\": {\n \"enabled\": false\n }\n }\n }\n \"started\": \"2023-07-18T11:14:42.917670\",\n \"finished\": null,\n \"tests\": {\n \"total\": 0,\n \"results\": []\n }\n}"
+ "body": "{\n \"error\": \"Invalid JSON received\"\n}"
},
{
- "name": "Device Not Found",
+ "name": "Device Not Found (404)",
"originalRequest": {
"method": "POST",
"header": [],
@@ -266,19 +428,26 @@
},
"status": "Not Found",
"code": 404,
- "_postman_previewlanguage": null,
- "header": null,
+ "_postman_previewlanguage": "json",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json",
+ "description": "",
+ "type": "text"
+ }
+ ],
"cookie": [],
- "body": "{\n \"error\": \"Device not found\"\n}"
+ "body": "{\n \"error\": \"A device with that MAC address could not be found\"\n}"
},
{
- "name": "Invalid JSON",
+ "name": "Testrun Already in Progress (409)",
"originalRequest": {
"method": "POST",
"header": [],
"body": {
"mode": "raw",
- "raw": "{\n \"device\": {\n \"mac_addr\": \"aa:bb:cc:dd:ee:ff,\n \"firmware\": \"1.2.2\"\n }\n}",
+ "raw": "{\n \"device\": {\n \"mac_addr\": \"00:1e:42:35:73:c4\",\n \"firmware\": \"1.2.2\",\n \"test_modules\": {\n \"dns\": {\n \"enabled\": true\n },\n \"ntp\": {\n \"enabled\": false\n }\n }\n }\n}",
"options": {
"raw": {
"language": "json"
@@ -297,12 +466,19 @@
]
}
},
- "status": "Bad Request",
- "code": 400,
- "_postman_previewlanguage": null,
- "header": null,
+ "status": "Conflict",
+ "code": 409,
+ "_postman_previewlanguage": "json",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json",
+ "description": "",
+ "type": "text"
+ }
+ ],
"cookie": [],
- "body": "{\n \"error\": \"Invalid JSON received\"\n}"
+ "body": "{\n \"error\": \"Testrun cannot be started whilst a test is running on another device\"\n}"
}
]
},
@@ -330,11 +506,12 @@
"system",
"stop"
]
- }
+ },
+ "description": "Stop Testrun from running against a device"
},
"response": [
{
- "name": "Not Running",
+ "name": "Stopped (200)",
"originalRequest": {
"method": "POST",
"header": [],
@@ -359,15 +536,22 @@
]
}
},
- "status": "Bad Request",
- "code": 400,
- "_postman_previewlanguage": null,
- "header": null,
+ "status": "OK",
+ "code": 200,
+ "_postman_previewlanguage": "json",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json",
+ "description": "",
+ "type": "text"
+ }
+ ],
"cookie": [],
- "body": "{\n \"error\": \"Test Run is not running\"\n}"
+ "body": "{\n \"success\": \"Testrun stopped\"\n}"
},
{
- "name": "Stopped",
+ "name": "Not Running (404)",
"originalRequest": {
"method": "POST",
"header": [],
@@ -392,168 +576,151 @@
]
}
},
- "status": "OK",
- "code": 200,
- "_postman_previewlanguage": null,
- "header": null,
+ "status": "Not Found",
+ "code": 404,
+ "_postman_previewlanguage": "json",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json",
+ "description": "",
+ "type": "text"
+ }
+ ],
"cookie": [],
- "body": "{\n \"success\": \"Test Run has stopped\"\n}"
+ "body": "{\n \"error\": \"Testrun is not currently running\"\n}"
}
]
},
{
- "name": "Get Devices",
+ "name": "Get System Status",
+ "protocolProfileBehavior": {
+ "disableBodyPruning": true
+ },
"request": {
"method": "GET",
"header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
"url": {
- "raw": "localhost:8000/devices",
+ "raw": "localhost:8000/system/status",
"host": [
"localhost"
],
"port": "8000",
"path": [
- "devices"
+ "system",
+ "status"
]
- }
+ },
+ "description": "Get the current status of Testrun. Suggested that this is called every 5 seconds to capture most events"
},
"response": [
{
- "name": "Devices",
+ "name": "Monitoring (200)",
"originalRequest": {
"method": "GET",
"header": [],
"url": {
- "raw": "localhost:8000/devices",
+ "raw": "localhost:8000/system/status",
"host": [
"localhost"
],
"port": "8000",
"path": [
- "devices"
+ "system",
+ "status"
]
}
},
"status": "OK",
"code": 200,
- "_postman_previewlanguage": null,
- "header": null,
+ "_postman_previewlanguage": "json",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json",
+ "description": "",
+ "type": "text"
+ }
+ ],
"cookie": [],
- "body": "[\n {\n \"mac_addr\": \"00:1e:42:35:73:c4\",\n \"manufacturer\": \"Teltonika\",\n \"model\": \"TRB 140\",\n \"test_modules\": {\n \"dns\": {\n \"enabled\": false\n },\n \"connection\": {\n \"enabled\": true\n },\n \"ntp\": {\n \"enabled\": false\n },\n \"baseline\": {\n \"enabled\": false\n },\n \"nmap\": {\n \"enabled\": false\n }\n }\n },\n {\n \"mac_addr\": \"aa:bb:cc:dd:ee:ff\",\n \"manufacturer\": \"Manufacturer X\",\n \"model\": \"Device X\",\n \"test_modules\": {\n \"dns\": {\n \"enabled\": true\n },\n \"connection\": {\n \"enabled\": true\n },\n \"ntp\": {\n \"enabled\": true\n },\n \"baseline\": {\n \"enabled\": false\n },\n \"nmap\": {\n \"enabled\": true\n }\n }\n }\n]"
+ "body": "{\n \"status\": \"Monitoring\",\n \"device\": {\n \"manufacturer\": \"Delta\",\n \"model\": \"03-DIN-CPU\",\n \"mac_addr\": \"01:02:03:04:05:06\",\n \"firmware\": \"1.2.2\"\n },\n \"started\": \"2023-06-22T09:20:00.123Z\",\n \"finished\": null,\n \"report\": null,\n \"tests\": {\n \"total\": 0,\n \"results\": []\n }\n}"
},
{
- "name": "No Devices",
+ "name": "Test in Progress (200)",
"originalRequest": {
"method": "GET",
"header": [],
"url": {
- "raw": "localhost:8000/devices",
+ "raw": "localhost:8000/system/status",
"host": [
"localhost"
],
"port": "8000",
"path": [
- "devices"
+ "system",
+ "status"
]
}
},
"status": "OK",
"code": 200,
- "_postman_previewlanguage": null,
- "header": null,
- "cookie": [],
- "body": "[\n\n]"
- }
- ]
- },
- {
- "name": "Get Configuration",
- "request": {
- "method": "GET",
- "header": [],
- "url": {
- "raw": "localhost:8000/system/config",
- "host": [
- "localhost"
+ "_postman_previewlanguage": "json",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json",
+ "description": "",
+ "type": "text"
+ }
],
- "port": "8000",
- "path": [
- "system",
- "config"
- ]
- }
- },
- "response": [
+ "cookie": [],
+ "body": "{\n \"status\": \"In Progress\",\n \"device\": {\n \"manufacturer\": \"Delta\",\n \"model\": \"03-DIN-CPU\",\n \"mac_addr\": \"01:02:03:04:05:06\",\n \"firmware\": \"1.2.2\"\n },\n \"started\": \"2023-06-22T09:20:00.123Z\",\n \"finished\": null,\n \"tests\": {\n \"total\": 26,\n \"results\": [\n {\n \"name\": \"dns.network.hostname_resolution\",\n \"description\": \"The device should resolve hostnames\",\n \"result\": \"Compliant\"\n },\n {\n \"name\": \"dns.network.from_dhcp\",\n \"description\": \"The device should use the DNS server provided by the DHCP server\",\n \"result\": \"Non-Compliant\"\n }\n ]\n }\n}"
+ },
{
- "name": "Configuration",
+ "name": "Cancelled (200)",
"originalRequest": {
"method": "GET",
"header": [],
"url": {
- "raw": "localhost:8000/system/config",
+ "raw": "localhost:8000/system/status",
"host": [
"localhost"
],
"port": "8000",
"path": [
"system",
- "config"
+ "status"
]
}
},
"status": "OK",
"code": 200,
- "_postman_previewlanguage": null,
- "header": null,
- "cookie": [],
- "body": "{\n \"network\": {\n \"device_intf\": \"enx207bd2620617\",\n \"internet_intf\": \"enx207bd26205e9\"\n },\n \"log_level\": \"INFO\",\n \"startup_timeout\": 60,\n \"monitor_period\": 60,\n \"runtime\": 120\n}"
- }
- ]
- },
- {
- "name": "Get System Status",
- "protocolProfileBehavior": {
- "disableBodyPruning": true
- },
- "request": {
- "method": "GET",
- "header": [],
- "body": {
- "mode": "raw",
- "raw": "",
- "options": {
- "raw": {
- "language": "json"
+ "_postman_previewlanguage": "json",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json",
+ "description": "",
+ "type": "text"
}
- }
- },
- "url": {
- "raw": "localhost:8000/system/status",
- "host": [
- "localhost"
],
- "port": "8000",
- "path": [
- "system",
- "status"
- ]
- }
- },
- "response": [
+ "cookie": [],
+ "body": "{\n \"status\": \"Cancelled\",\n \"device\": {\n \"manufacturer\": \"Delta\",\n \"model\": \"03-DIN-CPU\",\n \"mac_addr\": \"01:02:03:04:05:06\",\n \"firmware\": \"1.2.2\"\n },\n \"started\": \"2023-06-22T09:20:00.123Z\",\n \"finished\": \"2023-06-22T09:24:00.123Z\",\n \"report\": null,\n \"tests\": {\n \"total\": 22,\n \"results\": [\n {\n \"name\": \"dns.network.hostname_resolution\",\n \"description\": \"The device should resolve hostnames\",\n \"result\": \"Compliant\",\n \"required_result\": \"Required\"\n },\n {\n \"name\": \"dns.network.from_dhcp\",\n \"description\": \"The device should use the DNS server provided by the DHCP server\",\n \"result\": \"Feature Not Present\",\n \"required_result\": \"Roadmap\"\n }\n ]\n }\n}"
+ },
{
- "name": "Test In Progress",
+ "name": "Waiting for Device (200)",
"originalRequest": {
"method": "GET",
"header": [],
- "body": {
- "mode": "raw",
- "raw": "",
- "options": {
- "raw": {
- "language": "json"
- }
- }
- },
"url": {
"raw": "localhost:8000/system/status",
"host": [
@@ -568,25 +735,23 @@
},
"status": "OK",
"code": 200,
- "_postman_previewlanguage": null,
- "header": null,
+ "_postman_previewlanguage": "json",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json",
+ "description": "",
+ "type": "text"
+ }
+ ],
"cookie": [],
- "body": "{\n \"status\": \"In Progress\",\n \"device\": {\n \"manufacturer\": \"Delta\",\n \"model\": \"03-DIN-CPU\",\n \"mac_addr\": \"01:02:03:04:05:06\",\n \"firmware\": \"1.2.2\"\n },\n \"started\": \"2023-06-22T09:20:00.123Z\",\n \"finished\": null,\n \"tests\": {\n \"total\": 26,\n \"results\": [\n {\n \"name\": \"dns.network.hostname_resolution\",\n \"description\": \"The device should resolve hostnames\",\n \"result\": \"Compliant\"\n },\n {\n \"name\": \"dns.network.from_dhcp\",\n \"description\": \"The device should use the DNS server provided by the DHCP server\",\n \"result\": \"Non-Compliant\"\n } \n ]\n }\n}"
+ "body": "{\n \"status\": \"Waiting for Device\",\n \"device\": {\n \"manufacturer\": \"Delta\",\n \"model\": \"03-DIN-CPU\",\n \"mac_addr\": \"01:02:03:04:05:06\",\n \"firmware\": \"1.2.2\"\n },\n \"started\": \"2023-06-22T09:20:00.123Z\",\n \"finished\": null,\n \"report\": null,\n \"tests\": {\n \"total\": 0,\n \"results\": []\n }\n}"
},
{
- "name": "Not Running",
+ "name": "Non-Compliant (200)",
"originalRequest": {
"method": "GET",
"header": [],
- "body": {
- "mode": "raw",
- "raw": "",
- "options": {
- "raw": {
- "language": "json"
- }
- }
- },
"url": {
"raw": "localhost:8000/system/status",
"host": [
@@ -601,83 +766,148 @@
},
"status": "OK",
"code": 200,
- "_postman_previewlanguage": null,
- "header": null,
+ "_postman_previewlanguage": "json",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json",
+ "description": "",
+ "type": "text"
+ }
+ ],
+ "cookie": [],
+ "body": "{\n \"status\": \"Non-Compliant\",\n \"device\": {\n \"manufacturer\": \"Delta\",\n \"model\": \"03-DIN-CPU\",\n \"mac_addr\": \"01:02:03:04:05:06\",\n \"firmware\": \"1.2.2\"\n },\n \"started\": \"2023-06-22T09:20:00.123Z\",\n \"finished\": \"2023-06-22T09:26:00.123Z\",\n \"report\": \"https://api.testrun.io/report.pdf\",\n \"tests\": {\n \"total\": 26,\n \"results\": [\n {\n \"name\": \"dns.network.hostname_resolution\",\n \"description\": \"The device should resolve hostnames\",\n \"result\": \"Non-Compliant\",\n \"required_result\": \"Compliant\"\n \"recommendations\": [\n \"An example of a step to resolve\",\n \"Disable any running NTP server\"\n ]\n },\n {\n \"name\": \"dns.network.from_dhcp\",\n \"description\": \"The device should use the DNS server provided by the DHCP server\",\n \"result\": \"Compliant\",\n \"required_result\": \"Roadmap\"\n },\n {\n \"name\": \"security.services.ftp\",\n \"description\": \"FTP server should not be available\",\n \"result\": \"Compliant\",\n \"required_result\": \"Required\"\n }\n ]\n }\n}"
+ },
+ {
+ "name": "Compliant (200)",
+ "originalRequest": {
+ "method": "GET",
+ "header": [],
+ "url": {
+ "raw": "localhost:8000/system/status",
+ "host": [
+ "localhost"
+ ],
+ "port": "8000",
+ "path": [
+ "system",
+ "status"
+ ]
+ }
+ },
+ "status": "OK",
+ "code": 200,
+ "_postman_previewlanguage": "json",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json",
+ "description": "",
+ "type": "text"
+ }
+ ],
+ "cookie": [],
+ "body": "{\n \"status\": \"Compliant\",\n \"device\": {\n \"manufacturer\": \"Delta\",\n \"model\": \"03-DIN-CPU\",\n \"mac_addr\": \"01:02:03:04:05:06\",\n \"firmware\": \"1.2.2\"\n },\n \"started\": \"2023-06-22T09:20:00.123Z\",\n \"finished\": \"2023-06-22T09:26:00.123Z\",\n \"report\": \"https://api.testrun.io/report.pdf\",\n \"tests\": {\n \"total\": 3,\n \"results\": [\n {\n \"name\": \"dns.network.hostname_resolution\",\n \"description\": \"The device should resolve hostnames\",\n \"result\": \"Compliant\",\n \"required_result\": \"Required\"\n },\n {\n \"name\": \"dns.network.from_dhcp\",\n \"description\": \"The device should use the DNS server provided by the DHCP server\",\n \"result\": \"Feature Not Present\",\n \"required_result\": \"Roadmap\"\n },\n {\n \"name\": \"security.services.ftp\",\n \"description\": \"FTP server should not be available\",\n \"result\": \"Compliant\",\n \"required_result\": \"Required\"\n }\n ]\n }\n}"
+ },
+ {
+ "name": "Not Running - Idle (200)",
+ "originalRequest": {
+ "method": "GET",
+ "header": [],
+ "url": {
+ "raw": "localhost:8000/system/status",
+ "host": [
+ "localhost"
+ ],
+ "port": "8000",
+ "path": [
+ "system",
+ "status"
+ ]
+ }
+ },
+ "status": "OK",
+ "code": 200,
+ "_postman_previewlanguage": "json",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json",
+ "description": "",
+ "type": "text"
+ }
+ ],
"cookie": [],
- "body": "{\n \"status\": \"Idle\",\n \"device\": null,\n \"started\": null,\n \"finished\": null,\n \"tests\": {\n \"total\": 0,\n \"results\": [\n ]\n }\n}"
+ "body": "{\n \"status\": \"Idle\",\n \"device\": null,\n \"started\": null,\n \"finished\": null,\n \"tests\": {\n \"total\": 0,\n \"results\": []\n }\n}"
}
]
},
{
- "name": "Get Test Reports",
- "protocolProfileBehavior": {
- "disableBodyPruning": true
- },
+ "name": "Shutdown Testrun",
"request": {
- "method": "GET",
+ "method": "POST",
"header": [],
- "body": {
- "mode": "raw",
- "raw": "",
- "options": {
- "raw": {
- "language": "json"
- }
- }
- },
"url": {
- "raw": "localhost:8000/reports",
+ "raw": "http://localhost:8000/system/shutdown",
+ "protocol": "http",
"host": [
"localhost"
],
"port": "8000",
"path": [
- "reports"
+ "system",
+ "shutdown"
]
- }
+ },
+ "description": "Stop the Testrun framework"
},
"response": [
{
- "name": "Test In Progress",
+ "name": "Shutdown Testrun (200)",
"originalRequest": {
- "method": "GET",
+ "method": "POST",
"header": [],
- "body": {
- "mode": "raw",
- "raw": "",
- "options": {
- "raw": {
- "language": "json"
- }
- }
- },
"url": {
- "raw": "localhost:8000/system/status",
+ "raw": "http://localhost:8000/system/shutdown",
+ "protocol": "http",
"host": [
"localhost"
],
"port": "8000",
"path": [
"system",
- "status"
+ "shutdown"
]
}
},
"status": "OK",
"code": 200,
- "_postman_previewlanguage": null,
- "header": null,
+ "_postman_previewlanguage": "json",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json",
+ "description": "",
+ "type": "text"
+ }
+ ],
"cookie": [],
- "body": "{\n \"status\": \"In Progress\",\n \"device\": {\n \"manufacturer\": \"Delta\",\n \"model\": \"03-DIN-CPU\",\n \"mac_addr\": \"01:02:03:04:05:06\",\n \"firmware\": \"1.2.2\"\n },\n \"started\": \"2023-06-22T09:20:00.123Z\",\n \"finished\": null,\n \"tests\": {\n \"total\": 26,\n \"results\": [\n {\n \"name\": \"dns.network.hostname_resolution\",\n \"description\": \"The device should resolve hostnames\",\n \"result\": \"Compliant\"\n },\n {\n \"name\": \"dns.network.from_dhcp\",\n \"description\": \"The device should use the DNS server provided by the DHCP server\",\n \"result\": \"Non-Compliant\"\n } \n ]\n }\n}"
+ "body": "null"
},
{
- "name": "Not Running",
+ "name": "Test in Progress (400)",
"originalRequest": {
- "method": "GET",
- "header": [],
+ "method": "POST",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json",
+ "type": "text"
+ }
+ ],
"body": {
"mode": "raw",
- "raw": "",
+ "raw": "{\n \"device\": {\n \"mac_addr\": \"00:1e:42:35:73:c4\",\n \"firmware\": \"1.2.2\",\n \"test_modules\": {\n \"dns\": {\n \"enabled\": true\n },\n \"ntp\": {\n \"enabled\": false\n }\n }\n }\n}",
"options": {
"raw": {
"language": "json"
@@ -685,34 +915,99 @@
}
},
"url": {
- "raw": "localhost:8000/system/status",
+ "raw": "http://localhost:8000/system/shutdown",
+ "protocol": "http",
"host": [
"localhost"
],
"port": "8000",
"path": [
"system",
- "status"
+ "shutdown"
+ ]
+ }
+ },
+ "status": "Bad Request",
+ "code": 400,
+ "_postman_previewlanguage": "json",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json",
+ "description": "",
+ "type": "text"
+ }
+ ],
+ "cookie": [],
+ "body": "{\n \"error\": \"Unable to shutdown. A test is currently in progress.\"\n}"
+ }
+ ]
+ },
+ {
+ "name": "Get Version",
+ "request": {
+ "method": "GET",
+ "header": [],
+ "url": {
+ "raw": "http://localhost:8000/system/version",
+ "protocol": "http",
+ "host": [
+ "localhost"
+ ],
+ "port": "8000",
+ "path": [
+ "system",
+ "version"
+ ]
+ },
+ "description": "Get the current Testrun version and check if a software update is available"
+ },
+ "response": [
+ {
+ "name": "Get Version (200)",
+ "originalRequest": {
+ "method": "GET",
+ "header": [],
+ "url": {
+ "raw": "http://localhost:8000/system/version",
+ "protocol": "http",
+ "host": [
+ "localhost"
+ ],
+ "port": "8000",
+ "path": [
+ "system",
+ "version"
]
}
},
"status": "OK",
"code": 200,
- "_postman_previewlanguage": null,
- "header": null,
+ "_postman_previewlanguage": "json",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json",
+ "description": "",
+ "type": "text"
+ }
+ ],
"cookie": [],
- "body": "{\n \"status\": \"Idle\",\n \"device\": null,\n \"started\": null,\n \"finished\": null,\n \"tests\": {\n \"total\": 0,\n \"results\": [\n ]\n }\n}"
+ "body": "{\n \"installed_version\": \"v2.0\",\n \"update_available\": false,\n \"latest_version\": \"v2.0\",\n \"latest_version_url\": \"https://github.com/google/testrun/releases/tag/v2.0\"\n}"
}
]
},
{
- "name": "Create Device",
+ "name": "Get Test Reports",
+ "protocolProfileBehavior": {
+ "disableBodyPruning": true
+ },
"request": {
- "method": "POST",
+ "method": "GET",
"header": [],
"body": {
"mode": "raw",
- "raw": "{\n \"manufacturer\": \"Delta\",\n \"mac_addr\": \"00:1e:42:35:73:c1\",\n \"model\": \"O3-DIN-CPU\",\n \"test_modules\": {\n \"connection\": {\n \"enabled\": true\n },\n \"nmap\": {\n \"enabled\": false\n },\n \"dns\": {\n \"enabled\": false\n },\n \"ntp\": {\n \"enabled\": false\n },\n \"baseline\": {\n \"enabled\": false\n },\n \"tls\": {\n \"enabled\": false\n }\n }\n}",
+ "raw": "",
"options": {
"raw": {
"language": "json"
@@ -720,91 +1015,107 @@
}
},
"url": {
- "raw": "localhost:8000/device",
+ "raw": "localhost:8000/reports",
"host": [
"localhost"
],
"port": "8000",
"path": [
- "device"
+ "reports"
]
- }
+ },
+ "description": "Get all previous Testrun reports"
},
"response": [
{
- "name": "Create Device",
+ "name": "Get Test Reports (200)",
"originalRequest": {
- "method": "POST",
- "header": [],
- "body": {
- "mode": "raw",
- "raw": "{\n \"manufacturer\": \"Delta\",\n \"model\": \"O3-DIN-CPU\",\n \"mac_addr\": \"00:1e:42:35:73:c4\",\n \"test_modules\": {\n \"dns\": {\n \"enabled\": false\n },\n \"nmap\": {\n \"enabled\": true\n },\n \"dhcp\": {\n \"enabled\": false\n }\n }\n}",
- "options": {
- "raw": {
- "language": "json"
- }
+ "method": "GET",
+ "header": [
+ {
+ "key": "Origin",
+ "value": "http://localhost:8000",
+ "type": "text"
}
- },
+ ],
"url": {
- "raw": "localhost:8000/device",
+ "raw": "localhost:8000/reports",
"host": [
"localhost"
],
"port": "8000",
"path": [
- "device"
+ "reports"
+ ],
+ "query": [
+ {
+ "key": "",
+ "value": null,
+ "disabled": true
+ }
]
}
},
"status": "OK",
"code": 200,
- "_postman_previewlanguage": null,
- "header": null,
+ "_postman_previewlanguage": "json",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json",
+ "description": "",
+ "type": "text"
+ }
+ ],
"cookie": [],
- "body": "{\n \"manufacturer\": \"Delta\",\n \"model\": \"O3-DIN-CPU\",\n \"mac_addr\": \"00:1e:42:35:73:c4\",\n \"test_modules\": [\n \"dns\": {\n \"enabled\": false\n },\n \"nmap\": {\n \"enabled\": true\n },\n \"dhcp\": {\n \"enabled\": false\n }\n ]\n}"
+ "body": "[\n {\n \"testrun\": {\n \"version\": \"2.0\"\n },\n \"mac_addr\": \"00:1e:42:28:9e:4a\",\n \"device\": {\n \"mac_addr\": \"00:1e:42:28:9e:4a\",\n \"manufacturer\": \"Teltonika\",\n \"model\": \"TRB140\",\n \"firmware\": \"test\",\n \"test_modules\": {\n \"protocol\": {\n \"enabled\": true\n },\n \"services\": {\n \"enabled\": true\n },\n \"ntp\": {\n \"enabled\": true\n },\n \"tls\": {\n \"enabled\": true\n },\n \"connection\": {\n \"enabled\": true\n },\n \"dns\": {\n \"enabled\": true\n }\n }\n },\n \"status\": \"Non-Compliant\",\n \"started\": \"2000-01-01 00:00:00\",\n \"finished\": \"2000-01-01 00:30:00\",\n \"tests\": {\n \"total\": 40,\n \"results\": [\n {\n \"name\": \"protocol.valid_bacnet\",\n \"description\": \"BACnet device could not be discovered\",\n \"expected_behavior\": \"BACnet traffic can be seen on the network and packets are valid and not malformed\",\n \"required_result\": \"Recommended\",\n \"result\": \"Feature Not Detected\"\n },\n {\n \"name\": \"protocol.bacnet.version\",\n \"description\": \"Device did not respond to BACnet discovery\",\n \"expected_behavior\": \"The BACnet client implements an up to date version of BACnet\",\n \"required_result\": \"Recommended\",\n \"result\": \"Feature Not Detected\"\n },\n {\n \"name\": \"protocol.valid_modbus\",\n \"description\": \"Device did not respond to Modbus connection\",\n \"expected_behavior\": \"Any Modbus functionality works as expected and valid Modbus traffic can be observed\",\n \"required_result\": \"Recommended\",\n \"result\": \"Feature Not Detected\"\n },\n {\n \"name\": \"security.services.ftp\",\n \"description\": \"No FTP server found\",\n \"expected_behavior\": \"There is no FTP service running on any port\",\n \"required_result\": \"Required\",\n \"result\": \"Compliant\"\n },\n {\n \"name\": \"security.ssh.version\",\n \"description\": \"SSH server found running protocol 2.0\",\n \"expected_behavior\": \"SSH server is not running or server is SSHv2\",\n \"required_result\": \"Required\",\n \"result\": \"Compliant\"\n },\n {\n \"name\": \"security.services.telnet\",\n \"description\": \"No telnet server found\",\n \"expected_behavior\": \"There is no Telnet service running on any port\",\n \"required_result\": \"Required\",\n \"result\": \"Compliant\"\n },\n {\n \"name\": \"security.services.smtp\",\n \"description\": \"No SMTP server found\",\n \"expected_behavior\": \"There is no SMTP service running on any port\",\n \"required_result\": \"Required\",\n \"result\": \"Compliant\"\n },\n {\n \"name\": \"security.services.http\",\n \"description\": \"Found HTTP server running on port 80/tcp\",\n \"expected_behavior\": \"Device is unreachable on port 80 (or any other port) and only responds to HTTPS requests on port 443 (or any other port if HTTP is used at all)\",\n \"required_result\": \"Required\",\n \"result\": \"Non-Compliant\",\n \"recommendations\": [\n \"Disable all unsecure HTTP servers\",\n \"Setup TLS on the web server\"\n ]\n },\n {\n \"name\": \"security.services.pop\",\n \"description\": \"No POP server found\",\n \"expected_behavior\": \"There is no POP service running on any port\",\n \"required_result\": \"Required\",\n \"result\": \"Compliant\"\n },\n {\n \"name\": \"security.services.imap\",\n \"description\": \"No IMAP server found\",\n \"expected_behavior\": \"There is no IMAP service running on any port\",\n \"required_result\": \"Required\",\n \"result\": \"Compliant\"\n },\n {\n \"name\": \"security.services.snmpv3\",\n \"description\": \"No SNMP server found\",\n \"expected_behavior\": \"Device is unreachable on port 161 (or any other port) and device is unreachable on port 162 (or any other port) unless SNMP is essential in which case it is SNMPv3 is used.\",\n \"required_result\": \"Required\",\n \"result\": \"Compliant\"\n },\n {\n \"name\": \"security.services.vnc\",\n \"description\": \"No VNC server found\",\n \"expected_behavior\": \"Device cannot be accessed / connected to via VNC on any port\",\n \"required_result\": \"Required\",\n \"result\": \"Compliant\"\n },\n {\n \"name\": \"security.services.tftp\",\n \"description\": \"No TFTP server found\",\n \"expected_behavior\": \"There is no TFTP service running on any port\",\n \"required_result\": \"Required\",\n \"result\": \"Compliant\"\n },\n {\n \"name\": \"ntp.network.ntp_server\",\n \"description\": \"No NTP server found\",\n \"expected_behavior\": \"The device does not respond to NTP requests when it's IP is set as the NTP server on another device\",\n \"required_result\": \"Required\",\n \"result\": \"Compliant\"\n },\n {\n \"name\": \"connection.port_link\",\n \"description\": \"No port errors detected\",\n \"expected_behavior\": \"When the etherent cable is connected to the port, the device triggers the port to its enabled \\\"Link UP\\\" (LEDs illuminate on device and switch ports if present) state, and the switch shows no errors with the LEDs and when interrogated with a \\\"show interface\\\" command on most network switches.\",\n \"required_result\": \"Required\",\n \"result\": \"Compliant\"\n },\n {\n \"name\": \"connection.port_speed\",\n \"description\": \"Succesfully auto-negotiated speeds above 10 Mbps\",\n \"expected_behavior\": \"When the ethernet cable is connected to the port, the device autonegotiates a speed that can be checked with the \\\"show interface\\\" command on most network switches. The output of this command must also show that the \\\"configured speed\\\" is set to \\\"auto\\\".\",\n \"required_result\": \"Required\",\n \"result\": \"Compliant\"\n },\n {\n \"name\": \"connection.port_duplex\",\n \"description\": \"Succesfully auto-negotiated full duplex\",\n \"expected_behavior\": \"When the ethernet cable is connected to the port, the device autonegotiates a full-duplex connection.\",\n \"required_result\": \"Required\",\n \"result\": \"Compliant\"\n },\n {\n \"name\": \"connection.switch.arp_inspection\",\n \"description\": \"Device uses ARP\",\n \"expected_behavior\": \"Device continues to operate correctly when ARP inspection is enabled on the switch. No functionality is lost with ARP inspection enabled.\",\n \"required_result\": \"Required\",\n \"result\": \"Compliant\"\n },\n {\n \"name\": \"connection.switch.dhcp_snooping\",\n \"description\": \"Device does not act as a DHCP server\",\n \"expected_behavior\": \"Device continues to operate correctly when DHCP snooping is enabled on the switch.\",\n \"required_result\": \"Required\",\n \"result\": \"Compliant\"\n },\n {\n \"name\": \"connection.dhcp_address\",\n \"description\": \"Device responded to leased ip address\",\n \"expected_behavior\": \"The device is not setup with a static IP address. The device accepts an IP address from a DHCP server (RFC 2131) and responds succesfully to an ICMP echo (ping) request.\",\n \"required_result\": \"Required\",\n \"result\": \"Compliant\"\n },\n {\n \"name\": \"connection.mac_address\",\n \"description\": \"MAC address found: 00:1e:42:28:9e:4a\",\n \"expected_behavior\": \"N/A\",\n \"required_result\": \"Required\",\n \"result\": \"Compliant\"\n },\n {\n \"name\": \"connection.mac_oui\",\n \"description\": \"OUI Manufacturer found: Teltonika\",\n \"expected_behavior\": \"The MAC address prefix is registered in the IEEE Organizationally Unique Identifier database.\",\n \"required_result\": \"Required\",\n \"result\": \"Compliant\"\n },\n {\n \"name\": \"connection.private_address\",\n \"description\": \"All subnets are supported\",\n \"expected_behavior\": \"The device under test accepts IP addresses within all ranges specified in RFC 1918 and communicates using these addresses. The Internet Assigned Numbers Authority (IANA) has reserved the following three blocks of the IP address space for private internets. 10.0.0.0 - 10.255.255.255.255 (10/8 prefix). 172.16.0.0 - 172.31.255.255 (172.16/12 prefix). 192.168.0.0 - 192.168.255.255 (192.168/16 prefix)\",\n \"required_result\": \"Required\",\n \"result\": \"Compliant\"\n },\n {\n \"name\": \"connection.shared_address\",\n \"description\": \"All subnets are supported\",\n \"expected_behavior\": \"The device under test accepts IP addresses within the ranges specified in RFC 6598 and communicates using these addresses\",\n \"required_result\": \"Required\",\n \"result\": \"Compliant\"\n },\n {\n \"name\": \"connection.dhcp_disconnect\",\n \"description\": \"An error occured whilst running this test\",\n \"expected_behavior\": \"A client SHOULD use DHCP to reacquire or verify its IP address and network parameters whenever the local network parameters may have changed; e.g., at system boot time or after a disconnection from the local network, as the local network configuration may change without the client's or user's knowledge. If a client has knowledge ofa previous network address and is unable to contact a local DHCP server, the client may continue to use the previous network addres until the lease for that address expires. If the lease expires before the client can contact a DHCP server, the client must immediately discontinue use of the previous network address and may inform local users of the problem.\",\n \"required_result\": \"Required\",\n \"result\": \"Error\"\n },\n {\n \"name\": \"connection.single_ip\",\n \"description\": \"Device is using multiple IP addresses\",\n \"expected_behavior\": \"The device under test does not behave as a network switch and only requets one IP address. This test is to avoid that devices implement network switches that allow connecting strings of daisy chained devices to one single network port, as this would not make 802.1x port based authentication possible.\",\n \"required_result\": \"Required\",\n \"result\": \"Non-Compliant\",\n \"recommendations\": [\n \"Ensure that all ports on the device are isolated\",\n \"Ensure only one DHCP client is running\"\n ]\n },\n {\n \"name\": \"connection.target_ping\",\n \"description\": \"Device responds to ping\",\n \"expected_behavior\": \"The device under test responds to an ICMP echo (ping) request.\",\n \"required_result\": \"Required\",\n \"result\": \"Compliant\"\n },\n {\n \"name\": \"connection.ipaddr.ip_change\",\n \"description\": \"Device has accepted an IP address change\",\n \"expected_behavior\": \"If the lease expires before the client receiveds a DHCPACK, the client moves to INIT state, MUST immediately stop any other network processing and requires network initialization parameters as if the client were uninitialized. If the client then receives a DHCPACK allocating the client its previous network addres, the client SHOULD continue network processing. If the client is given a new network address, it MUST NOT continue using the previous network address and SHOULD notify the local users of the problem.\",\n \"required_result\": \"Required\",\n \"result\": \"Compliant\"\n },\n {\n \"name\": \"connection.ipaddr.dhcp_failover\",\n \"description\": \"Secondary DHCP server lease confirmed active in device\",\n \"expected_behavior\": \"\",\n \"required_result\": \"Required\",\n \"result\": \"Compliant\"\n },\n {\n \"name\": \"connection.ipv6_slaac\",\n \"description\": \"Device does not support IPv6 SLAAC\",\n \"expected_behavior\": \"The device under test complies with RFC4862 and forms a valid IPv6 SLAAC address\",\n \"required_result\": \"Required\",\n \"result\": \"Non-Compliant\",\n \"recommendations\": [\n \"Install a network manager that supports IPv6\",\n \"Disable DHCPv6\"\n ]\n },\n {\n \"name\": \"connection.ipv6_ping\",\n \"description\": \"No IPv6 SLAAC address found. Cannot ping\",\n \"expected_behavior\": \"The device responds to the ping as per RFC4443\",\n \"required_result\": \"Required\",\n \"result\": \"Non-Compliant\",\n \"recommendations\": [\n \"Enable ping response to IPv6 ICMP requests in network manager settings\",\n \"Create a firewall exception to allow ICMPv6 via LAN\"\n ]\n },\n {\n \"name\": \"security.tls.v1_2_server\",\n \"description\": \"TLS 1.2 certificate is invalid\",\n \"expected_behavior\": \"TLS 1.2 certificate is issued to the web browser client when accessed\",\n \"required_result\": \"Required if Applicable\",\n \"result\": \"Non-Compliant\",\n \"recommendations\": [\n \"Enable TLS 1.2 support in the web server configuration\",\n \"Disable TLS 1.0 and 1.1\",\n \"Sign the certificate used by the web server\"\n ]\n },\n {\n \"name\": \"security.tls.v1_2_client\",\n \"description\": \"No outbound connections were found\",\n \"expected_behavior\": \"The packet indicates a TLS connection with at least TLS 1.2 and support for ECDH and ECDSA ciphers\",\n \"required_result\": \"Required if Applicable\",\n \"result\": \"Feature Not Detected\"\n },\n {\n \"name\": \"security.tls.v1_3_server\",\n \"description\": \"TLS 1.3 certificate is invalid\",\n \"expected_behavior\": \"TLS 1.3 certificate is issued to the web browser client when accessed\",\n \"required_result\": \"Informational\",\n \"result\": \"Informational\"\n },\n {\n \"name\": \"security.tls.v1_3_client\",\n \"description\": \"No outbound connections were found\",\n \"expected_behavior\": \"The packet indicates a TLS connection with at least TLS 1.3\",\n \"required_result\": \"Informational\",\n \"result\": \"Informational\"\n },\n {\n \"name\": \"ntp.network.ntp_support\",\n \"description\": \"Device has not sent any NTP requests\",\n \"expected_behavior\": \"The device sends an NTPv4 request to the configured NTP server.\",\n \"required_result\": \"Required\",\n \"result\": \"Non-Compliant\",\n \"recommendations\": [\n \"Set the NTP version to v4 in the NTP client\",\n \"Install an NTP client that supports NTPv4\"\n ]\n },\n {\n \"name\": \"ntp.network.ntp_dhcp\",\n \"description\": \"Device has not sent any NTP requests\",\n \"expected_behavior\": \"Device can accept NTP server address, provided by the DHCP server (DHCP OFFER PACKET)\",\n \"required_result\": \"Roadmap\",\n \"result\": \"Feature Not Detected\"\n },\n {\n \"name\": \"dns.network.hostname_resolution\",\n \"description\": \"DNS traffic detected from device\",\n \"expected_behavior\": \"The device sends DNS requests.\",\n \"required_result\": \"Required\",\n \"result\": \"Compliant\"\n },\n {\n \"name\": \"dns.network.from_dhcp\",\n \"description\": \"DNS traffic detected only to DHCP provided server\",\n \"expected_behavior\": \"The device sends DNS requests to the DNS server provided by the DHCP server\",\n \"required_result\": \"Informational\",\n \"result\": \"Informational\"\n },\n {\n \"name\": \"dns.mdns\",\n \"description\": \"No MDNS traffic detected from the device\",\n \"expected_behavior\": \"Device may send MDNS requests\",\n \"required_result\": \"Informational\",\n \"result\": \"Informational\"\n }\n ]\n },\n \"report\": \"http://localhost:8000/report/Teltonika TRB140/2024-09-10T13:19:24\"\n }\n]"
},
{
- "name": "Invalid JSON",
+ "name": "No Test Reports (200)",
"originalRequest": {
- "method": "POST",
- "header": [],
- "body": {
- "mode": "raw",
- "raw": "{\n \"manufacturer\": \"Delta\",\n \"model\": \"O3-DIN-CPU\",\n \"mac_addr\": \"00:1e:42:35:73:c4\"\n \"test_modules\": {\n \"dns\": {\n \"enabled\": false\n },\n \"nmap\": {\n \"enabled\": true\n },\n \"dhcp\": {\n \"enabled\": false\n }\n }\n}",
- "options": {
- "raw": {
- "language": "json"
- }
+ "method": "GET",
+ "header": [
+ {
+ "key": "Origin",
+ "value": "http://localhost:8000",
+ "type": "text"
}
- },
+ ],
"url": {
- "raw": "localhost:8000/device",
+ "raw": "localhost:8000/reports",
"host": [
"localhost"
],
"port": "8000",
"path": [
- "device"
+ "reports"
]
}
},
- "status": "Bad Request",
- "code": 400,
- "_postman_previewlanguage": null,
- "header": null,
+ "status": "OK",
+ "code": 200,
+ "_postman_previewlanguage": "json",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json",
+ "description": "",
+ "type": "text"
+ }
+ ],
"cookie": [],
- "body": "{\n \"error\": \"Invalid JSON received\"\n}"
+ "body": "[]"
}
]
},
{
- "name": "Edit Device",
+ "name": "Delete Report",
"request": {
- "method": "POST",
+ "method": "DELETE",
"header": [],
"body": {
"mode": "raw",
- "raw": "{\n \"mac_addr\": \"00:1e:42:35:73:c4\",\n \"device\": {\n \"mac_addr\": \"00:1e:42:35:73:c1\",\n \"manufacturer\": \"Teltonika\",\n \"model\": \"TRB140\",\n \"test_modules\": {\n \"connection\": {\n \"enabled\": true\n },\n \"nmap\": {\n \"enabled\": false\n },\n \"dns\": {\n \"enabled\": false\n },\n \"ntp\": {\n \"enabled\": false\n },\n \"baseline\": {\n \"enabled\": false\n },\n \"tls\": {\n \"enabled\": false\n }\n }\n }\n}",
+ "raw": "{\n \"mac_addr\": \"00:1e:42:35:73:c4\",\n \"timestamp\": \"2023-10-10 16:53:47\"\n}",
"options": {
"raw": {
"language": "json"
@@ -812,26 +1123,27 @@
}
},
"url": {
- "raw": "localhost:8000/device/edit",
+ "raw": "http://localhost:8000/report",
+ "protocol": "http",
"host": [
"localhost"
],
"port": "8000",
"path": [
- "device",
- "edit"
+ "report"
]
- }
+ },
+ "description": "Delete a previous Testrun report"
},
"response": [
{
- "name": "Create Device",
+ "name": "Report Deleted (200)",
"originalRequest": {
- "method": "POST",
+ "method": "DELETE",
"header": [],
"body": {
"mode": "raw",
- "raw": "{\n \"manufacturer\": \"Delta\",\n \"model\": \"O3-DIN-CPU\",\n \"mac_addr\": \"00:1e:42:35:73:c4\",\n \"test_modules\": {\n \"dns\": {\n \"enabled\": false\n },\n \"nmap\": {\n \"enabled\": true\n },\n \"dhcp\": {\n \"enabled\": false\n }\n }\n}",
+ "raw": "{\n \"mac_addr\": \"00:1e:42:35:73:c4\",\n \"timestamp\": \"2024-08-05 13:37:53\"\n}",
"options": {
"raw": {
"language": "json"
@@ -839,31 +1151,39 @@
}
},
"url": {
- "raw": "localhost:8000/device",
+ "raw": "http://localhost:8000/report",
+ "protocol": "http",
"host": [
"localhost"
],
"port": "8000",
"path": [
- "device"
+ "report"
]
}
},
"status": "OK",
"code": 200,
- "_postman_previewlanguage": null,
- "header": null,
+ "_postman_previewlanguage": "json",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json",
+ "description": "",
+ "type": "text"
+ }
+ ],
"cookie": [],
- "body": "{\n \"manufacturer\": \"Delta\",\n \"model\": \"O3-DIN-CPU\",\n \"mac_addr\": \"00:1e:42:35:73:c4\",\n \"test_modules\": [\n \"dns\": {\n \"enabled\": false\n },\n \"nmap\": {\n \"enabled\": true\n },\n \"dhcp\": {\n \"enabled\": false\n }\n ]\n}"
+ "body": "{\n \"success\": \"Deleted report\"\n}"
},
{
- "name": "Invalid JSON",
+ "name": "Invalid JSON (400)",
"originalRequest": {
- "method": "POST",
+ "method": "DELETE",
"header": [],
"body": {
"mode": "raw",
- "raw": "{\n \"manufacturer\": \"Delta\",\n \"model\": \"O3-DIN-CPU\",\n \"mac_addr\": \"00:1e:42:35:73:c4\"\n \"test_modules\": {\n \"dns\": {\n \"enabled\": false\n },\n \"nmap\": {\n \"enabled\": true\n },\n \"dhcp\": {\n \"enabled\": false\n }\n }\n}",
+ "raw": "{\n \n}",
"options": {
"raw": {
"language": "json"
@@ -871,60 +1191,1343 @@
}
},
"url": {
- "raw": "localhost:8000/device",
+ "raw": "http://localhost:8000/report",
+ "protocol": "http",
"host": [
"localhost"
],
"port": "8000",
"path": [
- "device"
+ "report"
]
}
},
"status": "Bad Request",
"code": 400,
- "_postman_previewlanguage": null,
- "header": null,
+ "_postman_previewlanguage": "json",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json",
+ "description": "",
+ "type": "text"
+ }
+ ],
"cookie": [],
- "body": "{\n \"error\": \"Invalid JSON received\"\n}"
+ "body": "{\n \"error\": \"Invalid request received\"\n}"
+ },
+ {
+ "name": "Report Not Found (404))",
+ "originalRequest": {
+ "method": "DELETE",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"mac_addr\": \"00:1e:42:35:73:c4\",\n \"timestamp\": \"2023-10-10 16:53:47\"\n}",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "http://localhost:8000/report",
+ "protocol": "http",
+ "host": [
+ "localhost"
+ ],
+ "port": "8000",
+ "path": [
+ "report"
+ ]
+ }
+ },
+ "status": "Not Found",
+ "code": 404,
+ "_postman_previewlanguage": "json",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json",
+ "description": "",
+ "type": "text"
+ }
+ ],
+ "cookie": [],
+ "body": "{\n \"error\": \"Report not found\"\n}"
}
]
},
{
- "name": "Delete Report",
+ "name": "Get Report",
"request": {
- "method": "DELETE",
+ "method": "GET",
"header": [],
- "body": {
- "mode": "raw",
- "raw": "{\n \"mac_addr\": \"00:1e:42:35:73:c4\",\n \"timestamp\": \"2023-10-10 16:53:47\"\n}",
- "options": {
- "raw": {
- "language": "json"
- }
- }
- },
"url": {
- "raw": "http://localhost:8000/report",
+ "raw": "http://localhost:8000/report/{device_name}/{timestamp}",
"protocol": "http",
"host": [
"localhost"
],
"port": "8000",
"path": [
- "report"
+ "report",
+ "{device_name}",
+ "{timestamp}"
]
- }
+ },
+ "description": "Get the PDF report for a specific device and timestamp"
},
- "response": []
- },
- {
- "name": "Get Version",
+ "response": [
+ {
+ "name": "Get Report (200)",
+ "originalRequest": {
+ "method": "GET",
+ "header": [],
+ "url": {
+ "raw": "http://localhost:8000/report/Teltonika TRB140/2000-01-01T00:00:00",
+ "protocol": "http",
+ "host": [
+ "localhost"
+ ],
+ "port": "8000",
+ "path": [
+ "report",
+ "Teltonika TRB140",
+ "2000-01-01T00:00:00"
+ ]
+ }
+ },
+ "status": "OK",
+ "code": 200,
+ "_postman_previewlanguage": "text",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/pdf",
+ "description": "",
+ "type": "text"
+ }
+ ],
+ "cookie": [],
+ "body": "report.pdf"
+ },
+ {
+ "name": "Report Not Found (404)",
+ "originalRequest": {
+ "method": "GET",
+ "header": [],
+ "url": {
+ "raw": "http://localhost:8000/report/Teltonika TRB140/2024-05-05 13:37:53",
+ "protocol": "http",
+ "host": [
+ "localhost"
+ ],
+ "port": "8000",
+ "path": [
+ "report",
+ "Teltonika TRB140",
+ "2024-05-05 13:37:53"
+ ]
+ }
+ },
+ "status": "Not Found",
+ "code": 404,
+ "_postman_previewlanguage": "json",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json",
+ "description": "",
+ "type": "text"
+ }
+ ],
+ "cookie": [],
+ "body": "{\n \"error\": \"Report could not be found\"\n}"
+ }
+ ]
+ },
+ {
+ "name": "Export",
+ "request": {
+ "method": "POST",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"profile\": \"Primary profile\"\n}",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "http://localhost:8000/export/{device_name}/{timestamp}",
+ "protocol": "http",
+ "host": [
+ "localhost"
+ ],
+ "port": "8000",
+ "path": [
+ "export",
+ "{device_name}",
+ "{timestamp}"
+ ],
+ "query": [
+ {
+ "key": "",
+ "value": null,
+ "disabled": true
+ }
+ ]
+ },
+ "description": "Export the ZIP file of a specific device testing report"
+ },
+ "response": [
+ {
+ "name": "Export without profile (200)",
+ "originalRequest": {
+ "method": "POST",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "http://localhost:8000/export/Teltonika TRB140/2024-08-05T13:37:53",
+ "protocol": "http",
+ "host": [
+ "localhost"
+ ],
+ "port": "8000",
+ "path": [
+ "export",
+ "Teltonika TRB140",
+ "2024-08-05T13:37:53"
+ ],
+ "query": [
+ {
+ "key": "",
+ "value": null,
+ "disabled": true
+ }
+ ]
+ }
+ },
+ "status": "OK",
+ "code": 200,
+ "_postman_previewlanguage": "text",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/zip",
+ "description": "",
+ "type": "text"
+ }
+ ],
+ "cookie": [],
+ "body": "report.zip"
+ },
+ {
+ "name": "Export with profile (200)",
+ "originalRequest": {
+ "method": "POST",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "http://localhost:8000/export/Teltonika TRB140/2024-08-05T13:37:53",
+ "protocol": "http",
+ "host": [
+ "localhost"
+ ],
+ "port": "8000",
+ "path": [
+ "export",
+ "Teltonika TRB140",
+ "2024-08-05T13:37:53"
+ ],
+ "query": [
+ {
+ "key": "",
+ "value": null,
+ "disabled": true
+ }
+ ]
+ }
+ },
+ "status": "OK",
+ "code": 200,
+ "_postman_previewlanguage": "Text",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/zip",
+ "description": "",
+ "type": "text"
+ }
+ ],
+ "cookie": [],
+ "body": "report.zip"
+ },
+ {
+ "name": "Profile Not Found (404)",
+ "originalRequest": {
+ "method": "POST",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"profile\": \"Non-existing profile\"\n}",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "http://localhost:8000/export/Teltonika TRB140/2024-06-13T11:31:28",
+ "protocol": "http",
+ "host": [
+ "localhost"
+ ],
+ "port": "8000",
+ "path": [
+ "export",
+ "Teltonika TRB140",
+ "2024-06-13T11:31:28"
+ ],
+ "query": [
+ {
+ "key": "",
+ "value": null,
+ "disabled": true
+ }
+ ]
+ }
+ },
+ "status": "Not Found",
+ "code": 404,
+ "_postman_previewlanguage": "json",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json",
+ "description": "",
+ "type": "text"
+ }
+ ],
+ "cookie": [],
+ "body": "{\n \"error\": \"A profile with that name could not be found\"\n}"
+ },
+ {
+ "name": "Report Not Found (404)",
+ "originalRequest": {
+ "method": "POST",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "http://localhost:8000/export/Teltonika TRB140/2020-08-05T13:37:53",
+ "protocol": "http",
+ "host": [
+ "localhost"
+ ],
+ "port": "8000",
+ "path": [
+ "export",
+ "Teltonika TRB140",
+ "2020-08-05T13:37:53"
+ ],
+ "query": [
+ {
+ "key": "",
+ "value": null,
+ "disabled": true
+ }
+ ]
+ }
+ },
+ "status": "Not Found",
+ "code": 404,
+ "_postman_previewlanguage": "json",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json",
+ "description": "",
+ "type": "text"
+ }
+ ],
+ "cookie": [],
+ "body": "{\n \"error\": \"Report not found\"\n}"
+ }
+ ]
+ },
+ {
+ "name": "Get Devices",
+ "request": {
+ "method": "GET",
+ "header": [],
+ "url": {
+ "raw": "localhost:8000/devices",
+ "host": [
+ "localhost"
+ ],
+ "port": "8000",
+ "path": [
+ "devices"
+ ]
+ },
+ "description": "Obtain the list of devices from the device repository"
+ },
+ "response": [
+ {
+ "name": "Get Devices (200)",
+ "originalRequest": {
+ "method": "GET",
+ "header": [],
+ "url": {
+ "raw": "localhost:8000/devices",
+ "host": [
+ "localhost"
+ ],
+ "port": "8000",
+ "path": [
+ "devices"
+ ]
+ }
+ },
+ "status": "OK",
+ "code": 200,
+ "_postman_previewlanguage": "json",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json",
+ "description": "",
+ "type": "text"
+ }
+ ],
+ "cookie": [],
+ "body": "[\n {\n \"folder_url\": \"local/devices/Teltonika TRB140\",\n \"mac_addr\": \"00:1e:42:35:73:c4\",\n \"manufacturer\": \"Teltonika\",\n \"model\": \"TRB140\",\n \"test_modules\": {\n \"protocol\": {\n \"enabled\": true\n },\n \"services\": {\n \"enabled\": false\n },\n \"connection\": {\n \"enabled\": false\n },\n \"tls\": {\n \"enabled\": true\n },\n \"ntp\": {\n \"enabled\": true\n },\n \"dns\": {\n \"enabled\": true\n }\n },\n \"ip_addr\": null,\n \"firmware\": null,\n \"device_folder\": \"Teltonika TRB140\",\n \"reports\": [\n {\n \"_device\": {\n \"mac_addr\": \"00:1e:42:35:73:c4\",\n \"manufacturer\": \"Teltonika\",\n \"model\": \"TRB140\",\n \"firmware\": \"1.2.3\",\n \"test_modules\": {\n \"protocol\": {\n \"enabled\": true\n },\n \"services\": {\n \"enabled\": false\n },\n \"connection\": {\n \"enabled\": false\n },\n \"tls\": {\n \"enabled\": true\n },\n \"ntp\": {\n \"enabled\": true\n },\n \"dns\": {\n \"enabled\": true\n }\n }\n },\n \"_mac_addr\": \"00:1e:42:35:73:c4\",\n \"_status\": \"Non-Compliant\",\n \"_started\": \"2024-08-05T13:37:53\",\n \"_finished\": \"2024-08-05T13:39:35\",\n \"_total_tests\": 12,\n \"_results\": [\n {\n \"name\": \"protocol.valid_bacnet\",\n \"description\": \"BACnet device could not be discovered\",\n \"expected_behavior\": \"BACnet traffic can be seen on the network and packets are valid and not malformed\",\n \"required_result\": \"Recommended\",\n \"result\": \"Feature Not Detected\",\n \"recommendations\": []\n },\n {\n \"name\": \"protocol.bacnet.version\",\n \"description\": \"Device did not respond to BACnet discovery\",\n \"expected_behavior\": \"The BACnet client implements an up to date version of BACnet\",\n \"required_result\": \"Recommended\",\n \"result\": \"Feature Not Detected\",\n \"recommendations\": []\n },\n {\n \"name\": \"protocol.valid_modbus\",\n \"description\": \"Device did not respond to Modbus connection\",\n \"expected_behavior\": \"Any Modbus functionality works as expected and valid Modbus traffic can be observed\",\n \"required_result\": \"Recommended\",\n \"result\": \"Feature Not Detected\",\n \"recommendations\": []\n },\n {\n \"name\": \"security.tls.v1_2_server\",\n \"description\": \"TLS 1.2 certificate is invalid\",\n \"expected_behavior\": \"TLS 1.2 certificate is issued to the web browser client when accessed\",\n \"required_result\": \"Required if Applicable\",\n \"result\": \"Non-Compliant\",\n \"recommendations\": [\n \"Enable TLS 1.2 support in the web server configuration\",\n \"Disable TLS 1.0 and 1.1\",\n \"Sign the certificate used by the web server\"\n ]\n },\n {\n \"name\": \"security.tls.v1_2_client\",\n \"description\": \"TLS 1.2 client connections valid\",\n \"expected_behavior\": \"The packet indicates a TLS connection with at least TLS 1.2 and support for ECDH and ECDSA ciphers\",\n \"required_result\": \"Required if Applicable\",\n \"result\": \"Compliant\",\n \"recommendations\": []\n },\n {\n \"name\": \"security.tls.v1_3_server\",\n \"description\": \"TLS 1.3 certificate is invalid\",\n \"expected_behavior\": \"TLS 1.3 certificate is issued to the web browser client when accessed\",\n \"required_result\": \"Informational\",\n \"result\": \"Informational\",\n \"recommendations\": []\n },\n {\n \"name\": \"security.tls.v1_3_client\",\n \"description\": \"TLS 1.3 client connections valid\",\n \"expected_behavior\": \"The packet indicates a TLS connection with at least TLS 1.3\",\n \"required_result\": \"Informational\",\n \"result\": \"Informational\",\n \"recommendations\": []\n },\n {\n \"name\": \"ntp.network.ntp_support\",\n \"description\": \"Device sent NTPv3 packets. NTPv3 is not allowed\",\n \"expected_behavior\": \"The device sends an NTPv4 request to the configured NTP server.\",\n \"required_result\": \"Required\",\n \"result\": \"Non-Compliant\",\n \"recommendations\": [\n \"Set the NTP version to v4 in the NTP client\",\n \"Install an NTP client that supports NTPv4\"\n ]\n },\n {\n \"name\": \"ntp.network.ntp_dhcp\",\n \"description\": \"Device sent NTP request to non-DHCP provided server\",\n \"expected_behavior\": \"Device can accept NTP server address, provided by the DHCP server (DHCP OFFER PACKET)\",\n \"required_result\": \"Roadmap\",\n \"result\": \"Feature Not Detected\",\n \"recommendations\": []\n },\n {\n \"name\": \"dns.network.hostname_resolution\",\n \"description\": \"DNS traffic detected from device\",\n \"expected_behavior\": \"The device sends DNS requests.\",\n \"required_result\": \"Required\",\n \"result\": \"Compliant\",\n \"recommendations\": []\n },\n {\n \"name\": \"dns.network.from_dhcp\",\n \"description\": \"DNS traffic detected only to DHCP provided server\",\n \"expected_behavior\": \"The device sends DNS requests to the DNS server provided by the DHCP server\",\n \"required_result\": \"Informational\",\n \"result\": \"Informational\",\n \"recommendations\": []\n },\n {\n \"name\": \"dns.mdns\",\n \"description\": \"No MDNS traffic detected from the device\",\n \"expected_behavior\": \"Device may send MDNS requests\",\n \"required_result\": \"Informational\",\n \"result\": \"Informational\",\n \"recommendations\": []\n }\n ],\n \"_module_reports\": [],\n \"_report_url\": \"http://localhost:8000/report/Teltonika TRB140/2024-08-05T13:37:53\",\n \"_cur_page\": 0,\n \"_version\": \"1.3.1\"\n }\n ],\n \"max_device_reports\": null\n },\n {\n \"folder_url\": \"local/devices/Google First\",\n \"mac_addr\": \"00:0e:12:15:13:c8\",\n \"manufacturer\": \"Google\",\n \"model\": \"First\",\n \"test_modules\": null,\n \"ip_addr\": null,\n \"firmware\": null,\n \"device_folder\": \"Google First\",\n \"reports\": [],\n \"max_device_reports\": null\n },\n {\n \"folder_url\": \"local/devices/Delta O3-DIN-CPU\",\n \"mac_addr\": \"00:1e:42:35:73:c4\",\n \"manufacturer\": \"Delta\",\n \"model\": \"O3-DIN-CPU\",\n \"test_modules\": {\n \"dns\": {\n \"enabled\": true\n },\n \"connection\": {\n \"enabled\": true\n },\n \"ntp\": {\n \"enabled\": false\n },\n \"baseline\": {\n \"enabled\": true\n },\n \"nmap\": {\n \"enabled\": false\n }\n },\n \"ip_addr\": null,\n \"firmware\": null,\n \"device_folder\": \"Delta O3-DIN-CPU\",\n \"reports\": [],\n \"max_device_reports\": null\n }\n]"
+ },
+ {
+ "name": "No Devices (200)",
+ "originalRequest": {
+ "method": "GET",
+ "header": [],
+ "url": {
+ "raw": "localhost:8000/devices",
+ "host": [
+ "localhost"
+ ],
+ "port": "8000",
+ "path": [
+ "devices"
+ ]
+ }
+ },
+ "status": "OK",
+ "code": 200,
+ "_postman_previewlanguage": "json",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json",
+ "description": "",
+ "type": "text"
+ }
+ ],
+ "cookie": [],
+ "body": "[]"
+ }
+ ]
+ },
+ {
+ "name": "Create Device",
+ "request": {
+ "method": "POST",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"mac_addr\": \"00:1e:42:35:73:55\",\n \"manufacturer\": \"TEST\",\n \"model\": \"Stest2\",\n \"type\": \"IoT Gateway\",\n \"technology\": \"Hardware - Access Control\",\n \"test_pack\": \"Device Qualification\",\n \"additional_info\": [\n {\n \"question\": \"What type of device is this?\",\n \"answer\": \"IoT Gateway\"\n },\n {\n \"question\": \"Please select the technology this device falls into\",\n \"answer\": \"Hardware - Access Control\"\n },\n {\n \"question\": \"Does your device process any sensitive information? \",\n \"answer\": \"Yes\"\n },\n {\n \"question\": \"Can all non-essential services be disabled on your device?\",\n \"answer\": \"Yes\"\n },\n {\n \"question\": \"Is there a second IP port on the device?\",\n \"answer\": \"Yes\"\n },\n {\n \"question\": \"Can the second IP port on your device be disabled?\",\n \"answer\": \"Yes\"\n }\n ],\n \"test_modules\": {\n \"protocol\": { \"enabled\": true },\n \"services\": { \"enabled\": true },\n \"ntp\": { \"enabled\": true },\n \"tls\": { \"enabled\": true },\n \"connection\": { \"enabled\": true },\n \"dns\": { \"enabled\": true }\n }}",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "localhost:8000/device",
+ "host": [
+ "localhost"
+ ],
+ "port": "8000",
+ "path": [
+ "device"
+ ]
+ },
+ "description": "Create a new device to test"
+ },
+ "response": [
+ {
+ "name": "Create Device (201)",
+ "originalRequest": {
+ "method": "POST",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"manufacturer\": \"Delta\",\n \"model\": \"O3-DIN-CPU\",\n \"mac_addr\": \"00:1e:42:35:73:c4\"\n}",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "localhost:8000/device",
+ "host": [
+ "localhost"
+ ],
+ "port": "8000",
+ "path": [
+ "device"
+ ]
+ }
+ },
+ "status": "Created",
+ "code": 201,
+ "_postman_previewlanguage": "json",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json",
+ "description": "",
+ "type": "text"
+ }
+ ],
+ "cookie": [],
+ "body": "{\n \"mac_addr\": \"00:1e:42:35:73:c4\",\n \"manufacturer\": \"Delta\",\n \"model\": \"O3-DIN-CPU\",\n \"test_modules\": null\n}"
+ },
+ {
+ "name": "Already Exists (409)",
+ "originalRequest": {
+ "method": "POST",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"manufacturer\": \"Delta\",\n \"model\": \"O3-DIN-CPU\",\n \"mac_addr\": \"00:1e:42:35:73:c4\"\n}",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "localhost:8000/device",
+ "host": [
+ "localhost"
+ ],
+ "port": "8000",
+ "path": [
+ "device"
+ ]
+ }
+ },
+ "status": "Conflict",
+ "code": 409,
+ "_postman_previewlanguage": "json",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json",
+ "description": "",
+ "type": "text"
+ }
+ ],
+ "cookie": [],
+ "body": "{\n \"error\": \"A device with that MAC address already exists\"\n}"
+ },
+ {
+ "name": "Invalid JSON (400)",
+ "originalRequest": {
+ "method": "POST",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"manufacturer\": \"Delta\",\n \"model\": \"O3-DIN-CPU\"\n \"mac_addr\": \"00:1e:42:35:73:c4\"\n}",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "localhost:8000/device",
+ "host": [
+ "localhost"
+ ],
+ "port": "8000",
+ "path": [
+ "device"
+ ]
+ }
+ },
+ "status": "Bad Request",
+ "code": 400,
+ "_postman_previewlanguage": "json",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json",
+ "description": "",
+ "type": "text"
+ }
+ ],
+ "cookie": [],
+ "body": "{\n \"error\": \"Invalid JSON received\"\n}"
+ },
+ {
+ "name": "Invalid Request (400)",
+ "originalRequest": {
+ "method": "POST",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n}",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "localhost:8000/device",
+ "host": [
+ "localhost"
+ ],
+ "port": "8000",
+ "path": [
+ "device"
+ ]
+ }
+ },
+ "status": "Bad Request",
+ "code": 400,
+ "_postman_previewlanguage": "json",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json",
+ "description": "",
+ "type": "text"
+ }
+ ],
+ "cookie": [],
+ "body": "{\n \"error\": \"Invalid JSON received\"\n}"
+ }
+ ]
+ },
+ {
+ "name": "Delete device",
+ "request": {
+ "method": "DELETE",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"mac_addr\": \"00:1e:42:35:73:c4\"\n}",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "localhost:8000/device",
+ "host": [
+ "localhost"
+ ],
+ "port": "8000",
+ "path": [
+ "device"
+ ]
+ },
+ "description": "Create a new device to test"
+ },
+ "response": [
+ {
+ "name": "Delete device (200)",
+ "originalRequest": {
+ "method": "DELETE",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"mac_addr\": \"00:1e:42:35:73:c4\"\n}",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "localhost:8000/device",
+ "host": [
+ "localhost"
+ ],
+ "port": "8000",
+ "path": [
+ "device"
+ ]
+ }
+ },
+ "status": "OK",
+ "code": 200,
+ "_postman_previewlanguage": "json",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json",
+ "description": "",
+ "type": "text"
+ }
+ ],
+ "cookie": [],
+ "body": "{\n \"success\": \"Successfully deleted the device\"\n}"
+ },
+ {
+ "name": "Not Found (404)",
+ "originalRequest": {
+ "method": "DELETE",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"mac_addr\": \"non-existing\"\n}",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "localhost:8000/device",
+ "host": [
+ "localhost"
+ ],
+ "port": "8000",
+ "path": [
+ "device"
+ ]
+ }
+ },
+ "status": "Not Found",
+ "code": 404,
+ "_postman_previewlanguage": "json",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json",
+ "description": "",
+ "type": "text"
+ }
+ ],
+ "cookie": [],
+ "body": "{\n \"error\": \"Device not found\"\n}"
+ },
+ {
+ "name": "No Mac (400)",
+ "originalRequest": {
+ "method": "DELETE",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n\n}",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "localhost:8000/device",
+ "host": [
+ "localhost"
+ ],
+ "port": "8000",
+ "path": [
+ "device"
+ ]
+ }
+ },
+ "status": "Bad Request",
+ "code": 400,
+ "_postman_previewlanguage": "json",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json",
+ "description": "",
+ "type": "text"
+ }
+ ],
+ "cookie": [],
+ "body": "{\n \"error\": \"Invalid request received\"\n}"
+ },
+ {
+ "name": "Testrun in Progress (403)",
+ "originalRequest": {
+ "method": "DELETE",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"mac_addr\": \"00:1e:42:35:73:c4\"\n}",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "localhost:8000/device",
+ "host": [
+ "localhost"
+ ],
+ "port": "8000",
+ "path": [
+ "device"
+ ]
+ }
+ },
+ "status": "Forbidden",
+ "code": 403,
+ "_postman_previewlanguage": "json",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json",
+ "description": "",
+ "type": "text"
+ }
+ ],
+ "cookie": [],
+ "body": "{\n \"error\": \"Cannot delete this device whilst it is being tested\"\n}"
+ }
+ ]
+ },
+ {
+ "name": "Edit Device",
+ "request": {
+ "method": "POST",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"mac_addr\": \"00:1e:42:35:73:c4\",\n \"device\": {\n \"mac_addr\": \"00:1e:42:35:73:c1\",\n \"manufacturer\": \"Teltonika\",\n \"model\": \"TRB140\",\n \"test_modules\": {\n \"connection\": {\n \"enabled\": true\n },\n \"nmap\": {\n \"enabled\": false\n },\n \"dns\": {\n \"enabled\": false\n },\n \"ntp\": {\n \"enabled\": false\n },\n \"baseline\": {\n \"enabled\": false\n },\n \"tls\": {\n \"enabled\": false\n }\n }\n }\n}",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "localhost:8000/device/edit",
+ "host": [
+ "localhost"
+ ],
+ "port": "8000",
+ "path": [
+ "device",
+ "edit"
+ ]
+ },
+ "description": "Update the configuration for a device"
+ },
+ "response": [
+ {
+ "name": "Edit Device (200)",
+ "originalRequest": {
+ "method": "POST",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"mac_addr\": \"00:1e:42:35:73:c4\",\n \"device\": {\n \"mac_addr\": \"00:1e:42:35:73:c4\",\n \"manufacturer\": \"Delta\",\n \"model\": \"O3-DIN-CPU\",\n \"test_modules\": {\n \"dns\": {\"enabled\": true},\n \"connection\": {\"enabled\": true},\n \"ntp\": {\"enabled\": false},\n \"baseline\": {\"enabled\": true},\n \"nmap\": {\"enabled\": false}\n }\n }\n}",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "localhost:8000/device/edit",
+ "host": [
+ "localhost"
+ ],
+ "port": "8000",
+ "path": [
+ "device",
+ "edit"
+ ]
+ }
+ },
+ "status": "OK",
+ "code": 200,
+ "_postman_previewlanguage": "json",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json",
+ "description": "",
+ "type": "text"
+ }
+ ],
+ "cookie": [],
+ "body": "{\n \"mac_addr\": \"00:1e:42:35:73:c4\",\n \"manufacturer\": \"Delta\",\n \"model\": \"O3-DIN-CPU\",\n \"test_modules\": {\n \"dns\": {\n \"enabled\": true\n },\n \"connection\": {\n \"enabled\": true\n },\n \"ntp\": {\n \"enabled\": false\n },\n \"baseline\": {\n \"enabled\": true\n },\n \"nmap\": {\n \"enabled\": false\n }\n }\n}"
+ },
+ {
+ "name": "Not Found (404)",
+ "originalRequest": {
+ "method": "POST",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"mac_addr\": \"non-existing\",\n \"device\": {\n \"mac_addr\": \"00:1e:42:35:73:c4\",\n \"manufacturer\": \"Delta\",\n \"model\": \"O3-DIN-CPU\",\n \"test_modules\": {\n \"dns\": {\"enabled\": true},\n \"connection\": {\"enabled\": true},\n \"ntp\": {\"enabled\": false},\n \"baseline\": {\"enabled\": true},\n \"nmap\": {\"enabled\": false}\n }\n }\n}",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "localhost:8000/device/edit",
+ "host": [
+ "localhost"
+ ],
+ "port": "8000",
+ "path": [
+ "device",
+ "edit"
+ ]
+ }
+ },
+ "status": "Not Found",
+ "code": 404,
+ "_postman_previewlanguage": "json",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json",
+ "description": "",
+ "type": "text"
+ }
+ ],
+ "cookie": [],
+ "body": "{\n \"error\": \"A device with that MAC address could not be found\"\n}"
+ },
+ {
+ "name": "Mac Address in Use (409)",
+ "originalRequest": {
+ "method": "POST",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"mac_addr\": \"00:1e:42:35:73:c4\",\n \"device\": {\n \"mac_addr\": \"00:0e:12:15:13:c8\",\n \"manufacturer\": \"Delta\",\n \"model\": \"O3-DIN-CPU\",\n \"test_modules\": {\n \"dns\": {\"enabled\": true},\n \"connection\": {\"enabled\": true},\n \"ntp\": {\"enabled\": false},\n \"baseline\": {\"enabled\": true},\n \"nmap\": {\"enabled\": false}\n }\n }\n}",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "localhost:8000/device/edit",
+ "host": [
+ "localhost"
+ ],
+ "port": "8000",
+ "path": [
+ "device",
+ "edit"
+ ]
+ }
+ },
+ "status": "Conflict",
+ "code": 409,
+ "_postman_previewlanguage": "json",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json",
+ "description": "",
+ "type": "text"
+ }
+ ],
+ "cookie": [],
+ "body": "{\n \"error\": \"A device with that MAC address already exists\"\n}"
+ },
+ {
+ "name": "Device is Being Tested (403)",
+ "originalRequest": {
+ "method": "POST",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"mac_addr\": \"00:1e:42:35:73:c4\",\n \"device\": {\n \"mac_addr\": \"00:0e:12:15:13:c8\",\n \"manufacturer\": \"Delta\",\n \"model\": \"O3-DIN-CPU\",\n \"test_modules\": {\n \"dns\": {\"enabled\": true},\n \"connection\": {\"enabled\": true},\n \"ntp\": {\"enabled\": false},\n \"baseline\": {\"enabled\": true},\n \"nmap\": {\"enabled\": false}\n }\n }\n}",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "localhost:8000/device/edit",
+ "host": [
+ "localhost"
+ ],
+ "port": "8000",
+ "path": [
+ "device",
+ "edit"
+ ]
+ }
+ },
+ "status": "Forbidden",
+ "code": 403,
+ "_postman_previewlanguage": "json",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json",
+ "description": "",
+ "type": "text"
+ }
+ ],
+ "cookie": [],
+ "body": "{\n \"error\": \"Cannot edit this device whilst it is being tested\"\n}"
+ },
+ {
+ "name": "Invalid JSON (400)",
+ "originalRequest": {
+ "method": "POST",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"mac_addr\": \"00:1e:42:35:73:c4\",\n \"device\": {\n \"mac_addr\": \"00:1e:42:35:73:c4\"\n \"model\": \"O3-DIN-CPU\"\n}",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "localhost:8000/device/edit",
+ "host": [
+ "localhost"
+ ],
+ "port": "8000",
+ "path": [
+ "device",
+ "edit"
+ ]
+ }
+ },
+ "status": "Bad Request",
+ "code": 400,
+ "_postman_previewlanguage": "json",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json",
+ "description": "",
+ "type": "text"
+ }
+ ],
+ "cookie": [],
+ "body": "{\n \"error\": \"Invalid request received\"\n}"
+ }
+ ]
+ },
+ {
+ "name": "Get Devices Format",
+ "request": {
+ "method": "GET",
+ "header": [],
+ "url": {
+ "raw": "localhost:8000/devices/format",
+ "host": [
+ "localhost"
+ ],
+ "port": "8000",
+ "path": [
+ "devices",
+ "format"
+ ]
+ },
+ "description": "Obtain the list of devices from the device repository"
+ },
+ "response": [
+ {
+ "name": "Get Devices Format (200)",
+ "originalRequest": {
+ "method": "GET",
+ "header": [],
+ "url": {
+ "raw": "localhost:8000/devices/format",
+ "host": [
+ "localhost"
+ ],
+ "port": "8000",
+ "path": [
+ "devices",
+ "format"
+ ]
+ }
+ },
+ "status": "OK",
+ "code": 200,
+ "_postman_previewlanguage": "json",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json",
+ "name": "Content-Type",
+ "description": "",
+ "type": "text"
+ }
+ ],
+ "cookie": [],
+ "body": "[\n {\n \"step\": 1,\n \"title\": \"Select device type & technology\",\n \"description\": \"Before your device can go through testing, tell us more about your device and its functionality. It is important that we fully understand your device before a thorough assessment can be made.\",\n \"questions\": [\n {\n \"id\": 1,\n \"question\": \"What type of device is this?\",\n \"validation\": {\n \"required\": true\n },\n \"type\": \"select\",\n \"options\": [\n {\n \"text\": \"Building Automation Gateway\",\n \"risk\": \"High\",\n \"id\": 1\n },\n {\n \"text\": \"IoT Gateway\",\n \"risk\": \"High\",\n \"id\": 2\n },\n {\n \"text\": \"Controller - AHU\",\n \"risk\": \"High\",\n \"id\": 3\n },\n {\n \"text\": \"Controller - Boiler\",\n \"risk\": \"High\",\n \"id\": 4\n },\n {\n \"text\": \"Controller - Chiller\",\n \"risk\": \"High\",\n \"id\": 5\n },\n {\n \"text\": \"Controller - FCU\",\n \"risk\": \"Limited\",\n \"id\": 6\n },\n {\n \"text\": \"Controller - Pump\",\n \"risk\": \"Limited\",\n \"id\": 7\n },\n {\n \"text\": \"Controller - CRAC\",\n \"risk\": \"High\",\n \"id\": 8\n },\n {\n \"text\": \"Controller - VAV\",\n \"risk\": \"Limited\",\n \"id\": 9\n },\n {\n \"text\": \"Controller - VRF\",\n \"risk\": \"Limited\",\n \"id\": 10\n },\n {\n \"text\": \"Controller - Multiple\",\n \"risk\": \"High\",\n \"id\": 11\n },\n {\n \"text\": \"Controller - Other\",\n \"risk\": \"High\",\n \"id\": 12\n },\n {\n \"text\": \"Controller - Lighting\",\n \"risk\": \"Limited\",\n \"id\": 13\n },\n {\n \"text\": \"Controller - Blinds/Facades\",\n \"risk\": \"High\",\n \"id\": 14\n },\n {\n \"text\": \"Controller - Lifts/Elevators\",\n \"risk\": \"High\",\n \"id\": 15\n },\n {\n \"text\": \"Controller - UPS\",\n \"risk\": \"High\",\n \"id\": 16\n },\n {\n \"text\": \"Sensor - Air Quality\",\n \"risk\": \"Limited\",\n \"id\": 17\n },\n {\n \"text\": \"Sensor - Vibration\",\n \"risk\": \"Limited\",\n \"id\": 18\n },\n {\n \"text\": \"Sensor - Humidity\",\n \"risk\": \"Limited\",\n \"id\": 19\n },\n {\n \"text\": \"Sensor - Water\",\n \"risk\": \"Limited\",\n \"id\": 20\n },\n {\n \"text\": \"Sensor - Occupancy\",\n \"risk\": \"High\",\n \"id\": 21\n },\n {\n \"text\": \"Sensor - Volume\",\n \"risk\": \"Limited\",\n \"id\": 22\n },\n {\n \"text\": \"Sensor - Weight\",\n \"risk\": \"Limited\",\n \"id\": 23\n },\n {\n \"text\": \"Sensor - Weather\",\n \"risk\": \"Limited\",\n \"id\": 24\n },\n {\n \"text\": \"Sensor - Steam\",\n \"risk\": \"High\",\n \"id\": 25\n },\n {\n \"text\": \"Sensor - Air Flow\",\n \"risk\": \"Limited\",\n \"id\": 26\n },\n {\n \"text\": \"Sensor - Lighting\",\n \"risk\": \"Limited\",\n \"id\": 27\n },\n {\n \"text\": \"Sensor - Other\",\n \"risk\": \"High\",\n \"id\": 28\n },\n {\n \"text\": \"Sensor - Air Quality\",\n \"risk\": \"Limited\",\n \"id\": 29\n },\n {\n \"text\": \"Monitoring - Fire System\",\n \"risk\": \"Limited\",\n \"id\": 30\n },\n {\n \"text\": \"Monitoring - Emergency Lighting\",\n \"risk\": \"Limited\",\n \"id\": 31\n },\n {\n \"text\": \"Monitoring - Other\",\n \"risk\": \"High\",\n \"id\": 32\n },\n {\n \"text\": \"Monitoring - UPS\",\n \"risk\": \"Limited\",\n \"id\": 33\n },\n {\n \"text\": \"Meter - Water\",\n \"risk\": \"Limited\",\n \"id\": 34\n },\n {\n \"text\": \"Meter - Gas\",\n \"risk\": \"Limited\",\n \"id\": 35\n },\n {\n \"text\": \"Meter - Electricity\",\n \"risk\": \"Limited\",\n \"id\": 36\n },\n {\n \"text\": \"Meter - Other\",\n \"risk\": \"High\",\n \"id\": 37\n },\n {\n \"text\": \"Other\",\n \"risk\": \"High\",\n \"id\": 38\n },\n {\n \"text\": \"Data - Storage\",\n \"risk\": \"High\",\n \"id\": 39\n },\n {\n \"text\": \"Data - Processing\",\n \"risk\": \"High\",\n \"id\": 40\n },\n {\n \"text\": \"Tablet\",\n \"risk\": \"High\",\n \"id\": 41\n }\n ]\n },\n {\n \"id\": 2,\n \"question\": \"Please select the technology this device falls into\",\n \"validation\": {\n \"required\": true\n },\n \"type\": \"select\",\n \"options\": [\n {\n \"id\": 1,\n \"text\": \"Hardware - Access Control\"\n },\n {\n \"id\": 2,\n \"text\": \"Hardware - Air quality\"\n },\n {\n \"id\": 3,\n \"text\": \"Hardware - Asset location tracking\"\n },\n {\n \"id\": 4,\n \"text\": \"Hardware - Audio Visual\"\n },\n {\n \"id\": 5,\n \"text\": \"Hardware - Blinds/Facade\"\n },\n {\n \"id\": 6,\n \"text\": \"Hardware - Cameras\"\n },\n {\n \"id\": 7,\n \"text\": \"Hardware - Catering\"\n },\n {\n \"id\": 8,\n \"text\": \"Hardware - Data Ingestion/Managment\"\n },\n {\n \"id\": 9,\n \"text\": \"Hardware - EV Charging\"\n },\n {\n \"id\": 10,\n \"text\": \"Hardware - Fitness\"\n },\n {\n \"id\": 11,\n \"text\": \"Hardware - HVAC\"\n },\n {\n \"id\": 12,\n \"text\": \"Hardware - Irrigation\"\n },\n {\n \"id\": 13,\n \"text\": \"Hardware - Leak Detection\"\n },\n {\n \"id\": 14,\n \"text\": \"Hardware - Lifts/Elevators\"\n },\n {\n \"id\": 15,\n \"text\": \"Hardware - Lighting\"\n },\n {\n \"id\": 16,\n \"text\": \"Hardware - Metering\"\n },\n {\n \"id\": 17,\n \"text\": \"Hardware - Monitoring\"\n },\n {\n \"id\": 18,\n \"text\": \"Hardware - Occupancy\"\n },\n {\n \"id\": 19,\n \"text\": \"Hardware - System Integration\"\n },\n {\n \"id\": 20,\n \"text\": \"Hardware - Time Management\"\n },\n {\n \"id\": 21,\n \"text\": \"Hardware - UPS\"\n },\n {\n \"id\": 22,\n \"text\": \"Hardware - Waste Management\"\n },\n {\n \"id\": 23,\n \"text\": \"Building Automation\"\n },\n {\n \"id\": 24,\n \"text\": \"Other\"\n }\n ]\n },\n {\n \"id\": 3,\n \"question\": \"Does your device process any sensitive information? \",\n \"validation\": {\n \"required\": true\n },\n \"type\": \"select\",\n \"options\": [\n {\n \"id\": 1,\n \"text\": \"Yes\",\n \"risk\": \"Limited\"\n },\n {\n \"id\": 2,\n \"text\": \"No\",\n \"risk\": \"High\"\n },\n {\n \"id\": 3,\n \"text\": \"I don't know\",\n \"risk\": \"High\"\n }\n ]\n }\n ]\n },\n {\n \"step\": 2,\n \"title\": \"Tell us more about your device\",\n \"description\": \"Before your device can go through testing, tell us more about your device and its functionality. It is important that we fully understand your device before a thorough assessment can be made.\",\n \"questions\": [\n {\n \"id\": 1,\n \"question\": \"Can all non-essential services be disabled on your device?\",\n \"validation\": {\n \"required\": true\n },\n \"type\": \"select\",\n \"options\": [\n {\n \"text\": \"Yes\",\n \"id\": 1\n },\n {\n \"text\": \"No\",\n \"id\": 2\n }\n ]\n },\n {\n \"id\": 2,\n \"question\": \"Is there a second IP port on the device?\",\n \"validation\": {\n \"required\": true\n },\n \"type\": \"select\",\n \"options\": [\n {\n \"text\": \"Yes\",\n \"id\": 1\n },\n {\n \"text\": \"No\",\n \"id\": 2\n }\n ]\n },\n {\n \"id\": 3,\n \"question\": \"Can the second IP port on your device be disabled?\",\n \"validation\": {\n \"required\": true\n },\n \"type\": \"select\",\n \"options\": [\n {\n \"text\": \"Yes\",\n \"id\": 1\n },\n {\n \"text\": \"No\",\n \"id\": 2\n },\n {\n \"text\": \"N/A\",\n \"id\": 3\n }\n ]\n }\n ]\n }\n]"
+ }
+ ]
+ },
+ {
+ "name": "Get System Modules",
+ "request": {
+ "method": "GET",
+ "header": [],
+ "url": {
+ "raw": "http://localhost:8000/system/modules",
+ "protocol": "http",
+ "host": [
+ "localhost"
+ ],
+ "port": "8000",
+ "path": [
+ "system",
+ "modules"
+ ]
+ },
+ "description": "Get a list of all root CA certificates that have been loaded into Testrun"
+ },
+ "response": [
+ {
+ "name": "Get System Modules (200)",
+ "originalRequest": {
+ "method": "GET",
+ "header": [],
+ "url": {
+ "raw": "http://localhost:8000/system/modules",
+ "protocol": "http",
+ "host": [
+ "localhost"
+ ],
+ "port": "8000",
+ "path": [
+ "system",
+ "modules"
+ ]
+ }
+ },
+ "status": "OK",
+ "code": 200,
+ "_postman_previewlanguage": "json",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json",
+ "description": "",
+ "type": "text"
+ }
+ ],
+ "cookie": [],
+ "body": "[\n \"Protocol\",\n \"Services\",\n \"Connection\",\n \"TLS\",\n \"NTP\",\n \"DNS\"\n]"
+ }
+ ]
+ },
+ {
+ "name": "Get System Testpacks",
+ "request": {
+ "method": "GET",
+ "header": [],
+ "url": {
+ "raw": "localhost:8000/system/testpacks",
+ "host": [
+ "localhost"
+ ],
+ "port": "8000",
+ "path": [
+ "system",
+ "testpacks"
+ ]
+ },
+ "description": "Obtain a list of applicable and available network adapters for use in testing"
+ },
+ "response": [
+ {
+ "name": "System Testpacks (200)",
+ "originalRequest": {
+ "method": "GET",
+ "header": [],
+ "url": {
+ "raw": "localhost:8000/system/testpacks",
+ "host": [
+ "localhost"
+ ],
+ "port": "8000",
+ "path": [
+ "system",
+ "testpacks"
+ ]
+ }
+ },
+ "status": "OK",
+ "code": 200,
+ "_postman_previewlanguage": "json",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json",
+ "description": "",
+ "type": "text"
+ }
+ ],
+ "cookie": [],
+ "body": "[\n \"Pilot Assessment\",\n \"Device Qualification\"\n]"
+ }
+ ]
+ },
+ {
+ "name": "List Certificates",
+ "request": {
+ "method": "GET",
+ "header": [],
+ "url": {
+ "raw": "http://localhost:8000/system/config/certs",
+ "protocol": "http",
+ "host": [
+ "localhost"
+ ],
+ "port": "8000",
+ "path": [
+ "system",
+ "config",
+ "certs"
+ ]
+ },
+ "description": "Get a list of all root CA certificates that have been loaded into Testrun"
+ },
+ "response": [
+ {
+ "name": "List Certificates (200)",
+ "originalRequest": {
+ "method": "GET",
+ "header": [],
+ "url": {
+ "raw": "http://localhost:8000/system/config/certs",
+ "protocol": "http",
+ "host": [
+ "localhost"
+ ],
+ "port": "8000",
+ "path": [
+ "system",
+ "config",
+ "certs"
+ ]
+ }
+ },
+ "status": "OK",
+ "code": 200,
+ "_postman_previewlanguage": "json",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json",
+ "description": "",
+ "type": "text"
+ }
+ ],
+ "cookie": [],
+ "body": "[\n {\n \"name\": \"GTS Root R1\",\n \"status\": \"Valid\",\n \"organisation\": \"Google Trust Services LLC\",\n \"expires\": \"2036-06-22T00:00:00+00:00\",\n \"filename\": \"crt.pem\"\n },\n {\n \"name\": \"WR2\",\n \"status\": \"Valid\",\n \"organisation\": \"Google Trust Services LLC\",\n \"expires\": \"2029-02-20T14:00:00+00:00\",\n \"filename\": \"WR2.pem\"\n }\n]"
+ },
+ {
+ "name": "No Certificates (200)",
+ "originalRequest": {
+ "method": "GET",
+ "header": [],
+ "url": {
+ "raw": "http://localhost:8000/system/config/certs",
+ "protocol": "http",
+ "host": [
+ "localhost"
+ ],
+ "port": "8000",
+ "path": [
+ "system",
+ "config",
+ "certs"
+ ]
+ }
+ },
+ "status": "OK",
+ "code": 200,
+ "_postman_previewlanguage": "json",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json",
+ "name": "Content-Type",
+ "description": "",
+ "type": "text"
+ }
+ ],
+ "cookie": [],
+ "body": "[]"
+ }
+ ]
+ },
+ {
+ "name": "Upload Certificate",
"request": {
- "method": "GET",
+ "method": "POST",
"header": [],
+ "body": {
+ "mode": "formdata",
+ "formdata": [
+ {
+ "key": "file",
+ "type": "file",
+ "src": "NH1mNzqEQ/1c3.pem"
+ }
+ ]
+ },
"url": {
- "raw": "http://localhost:8000/system/version",
+ "raw": "http://localhost:8000/system/config/certs",
"protocol": "http",
"host": [
"localhost"
@@ -932,110 +2535,643 @@
"port": "8000",
"path": [
"system",
- "version"
+ "config",
+ "certs"
]
- }
+ },
+ "description": "Upload a new root CA certificate into Testrun"
},
- "response": []
+ "response": [
+ {
+ "name": "Upload Certificate (201)",
+ "originalRequest": {
+ "method": "POST",
+ "header": [],
+ "body": {
+ "mode": "formdata",
+ "formdata": [
+ {
+ "key": "file",
+ "type": "file",
+ "src": "NH1mNzqEQ/1c3.pem"
+ }
+ ]
+ },
+ "url": {
+ "raw": "http://localhost:8000/system/config/certs",
+ "protocol": "http",
+ "host": [
+ "localhost"
+ ],
+ "port": "8000",
+ "path": [
+ "system",
+ "config",
+ "certs"
+ ]
+ }
+ },
+ "status": "Created",
+ "code": 201,
+ "_postman_previewlanguage": "json",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json",
+ "name": "Content-Type",
+ "description": "",
+ "type": "text"
+ }
+ ],
+ "cookie": [],
+ "body": "{\n \"name\": \"GTS Root R1\",\n \"status\": \"Valid\",\n \"organisation\": \"Google Trust Services LLC\",\n \"expires\": \"2036-06-22T00:00:00+00:00\",\n \"filename\": \"crt.pem\"\n}"
+ },
+ {
+ "name": "Duplicate (409)",
+ "originalRequest": {
+ "method": "POST",
+ "header": [],
+ "body": {
+ "mode": "formdata",
+ "formdata": [
+ {
+ "key": "file",
+ "type": "file",
+ "src": "NH1mNzqEQ/1c3.pem"
+ }
+ ]
+ },
+ "url": {
+ "raw": "http://localhost:8000/system/config/certs",
+ "protocol": "http",
+ "host": [
+ "localhost"
+ ],
+ "port": "8000",
+ "path": [
+ "system",
+ "config",
+ "certs"
+ ]
+ }
+ },
+ "status": "Conflict",
+ "code": 409,
+ "_postman_previewlanguage": "json",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json",
+ "name": "Content-Type",
+ "description": "",
+ "type": "text"
+ }
+ ],
+ "cookie": [],
+ "body": "{\n \"error\": \"A certificate with that file name already exists.\"\n}"
+ },
+ {
+ "name": "Invalid Cert (400)",
+ "originalRequest": {
+ "method": "POST",
+ "header": [],
+ "body": {
+ "mode": "formdata",
+ "formdata": [
+ {
+ "key": "file",
+ "type": "file",
+ "src": "postman-cloud:///1ef5fb1a-6342-44e0-b35f-700d496e0cc5"
+ }
+ ]
+ },
+ "url": {
+ "raw": "http://localhost:8000/system/config/certs",
+ "protocol": "http",
+ "host": [
+ "localhost"
+ ],
+ "port": "8000",
+ "path": [
+ "system",
+ "config",
+ "certs"
+ ]
+ }
+ },
+ "status": "Bad Request",
+ "code": 400,
+ "_postman_previewlanguage": "json",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json",
+ "name": "Content-Type",
+ "description": "",
+ "type": "text"
+ }
+ ],
+ "cookie": [],
+ "body": "{\n \"error\": \"Failed to upload certificate. Is it in the correct format?\"\n}"
+ }
+ ]
},
{
- "name": "Get Report",
+ "name": "Delete Certificate",
"request": {
- "method": "GET",
+ "method": "DELETE",
"header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"name\": \"GTS CA 1C3\"\n}",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
"url": {
- "raw": "http://localhost:8000/report/{device_name}/{timestamp}",
+ "raw": "http://localhost:8000/system/config/certs",
"protocol": "http",
"host": [
"localhost"
],
"port": "8000",
"path": [
- "report",
- "{device_name}",
- "{timestamp}"
+ "system",
+ "config",
+ "certs"
]
- }
+ },
+ "description": "Delete a root CA certificate"
},
- "response": []
+ "response": [
+ {
+ "name": "Delete Certificate (200)",
+ "originalRequest": {
+ "method": "DELETE",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"name\": \"GTS Root R1\"\n}",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "http://localhost:8000/system/config/certs",
+ "protocol": "http",
+ "host": [
+ "localhost"
+ ],
+ "port": "8000",
+ "path": [
+ "system",
+ "config",
+ "certs"
+ ]
+ }
+ },
+ "status": "OK",
+ "code": 200,
+ "_postman_previewlanguage": "json",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json",
+ "name": "Content-Type",
+ "description": "",
+ "type": "text"
+ }
+ ],
+ "cookie": [],
+ "body": "{\n \"success\": \"Successfully deleted the certificate\"\n}"
+ },
+ {
+ "name": "Not Found (404)",
+ "originalRequest": {
+ "method": "DELETE",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"name\": \"non-existing name\"\n}",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "http://localhost:8000/system/config/certs",
+ "protocol": "http",
+ "host": [
+ "localhost"
+ ],
+ "port": "8000",
+ "path": [
+ "system",
+ "config",
+ "certs"
+ ]
+ }
+ },
+ "status": "Not Found",
+ "code": 404,
+ "_postman_previewlanguage": "json",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json",
+ "name": "Content-Type",
+ "description": "",
+ "type": "text"
+ }
+ ],
+ "cookie": [],
+ "body": "{\n \"error\": \"A certificate with that name could not be found\"\n}"
+ },
+ {
+ "name": "Invalid JSON (400)",
+ "originalRequest": {
+ "method": "DELETE",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n\n}",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "http://localhost:8000/system/config/certs",
+ "protocol": "http",
+ "host": [
+ "localhost"
+ ],
+ "port": "8000",
+ "path": [
+ "system",
+ "config",
+ "certs"
+ ]
+ }
+ },
+ "status": "Bad Request",
+ "code": 400,
+ "_postman_previewlanguage": "json",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json",
+ "name": "Content-Type",
+ "description": "",
+ "type": "text"
+ }
+ ],
+ "cookie": [],
+ "body": "{\n \"error\": \"Received a bad request\"\n}"
+ }
+ ]
},
{
- "name": "Get Export",
+ "name": "Get Profile Format",
"request": {
"method": "GET",
"header": [],
"url": {
- "raw": "http://localhost:8000/export/{device_name}/{timestamp}",
+ "raw": "http://localhost:8000/profiles/format",
"protocol": "http",
"host": [
"localhost"
],
"port": "8000",
"path": [
- "export",
- "{device_name}",
- "{timestamp}"
+ "profiles",
+ "format"
]
- }
+ },
+ "description": "Obtain the current format of the risk assessment questionnaire"
},
- "response": []
+ "response": [
+ {
+ "name": "Get Profile Format (200)",
+ "originalRequest": {
+ "method": "GET",
+ "header": [],
+ "url": {
+ "raw": "http://localhost:8000/profiles/format",
+ "protocol": "http",
+ "host": [
+ "localhost"
+ ],
+ "port": "8000",
+ "path": [
+ "profiles",
+ "format"
+ ]
+ }
+ },
+ "status": "OK",
+ "code": 200,
+ "_postman_previewlanguage": "json",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json",
+ "description": ""
+ }
+ ],
+ "cookie": [],
+ "body": "[\n {\n \"question\": \"How will this device be used at Google?\",\n \"description\": \"Desribe your use case. Add links to user journey diagrams and TDD if available.\",\n \"type\": \"text-long\",\n \"validation\": {\n \"max\": \"512\",\n \"required\": true\n }\n },\n {\n \"question\": \"Is this device going to be managed by Google or a third party?\",\n \"description\": \"A manufacturer or supplier is considered third party in this case\",\n \"type\": \"select\",\n \"options\": [\n \"Google\",\n \"Third Party\"\n ],\n \"validation\": {\n \"required\": true\n }\n },\n {\n \"question\": \"Will the third-party device administrator be able to grant access to authorized Google personnel upon request?\",\n \"type\": \"select\",\n \"options\": [\n \"Yes\",\n \"No\",\n \"N/A\"\n ],\n \"default\": \"N/A\",\n \"validation\": {\n \"required\": true\n }\n },\n {\n \"category\": \"Data Transmission\",\n \"question\": \"Which of the following statements are true about this device?\",\n \"description\": \"This tells us about the types of data that are transmitted from this device and how the transmission is performed from a technical standpoint.\",\n \"type\": \"select-multiple\",\n \"options\": [\n \"PII/PHI, confidential/sensitive business data, Intellectual Property and Trade Secrets, Critical Infrastructure and Identity Assets to a domain outside Alphabet's ownership\",\n \"Data transmission occurs across less-trusted networks (e.g. the internet).\",\n \"A failure in data transmission would likely have a substantial negative impact (https://www.rra.rocks/docs/standard_levels#levels-definitions)\",\n \"A confidentiality breach during transmission would have a substantial negative impact\",\n \"The device does not encrypt data during transmission\",\n \"None of the above\"\n ],\n \"validation\": {\n \"required\": true\n }\n },\n {\n \"category\": \"Data Transmission\",\n \"question\": \"Does the network protocol assure server-to-client identity verification?\",\n \"type\": \"select\",\n \"options\": [\n \"Yes\",\n \"No\",\n \"I don't know\"\n ],\n \"validation\": {\n \"required\": true\n }\n },\n {\n \"category\": \"Remote Operation\",\n \"question\": \"Click the statements that best describe the characteristics of this device.\",\n \"description\": \"This tells us about how this device is managed remotely.\",\n \"type\": \"select-multiple\",\n \"options\": [\n \"PII/PHI, or confidential business data is accessible from the device without authentication\",\n \"Unrecoverable actions (e.g. disk wipe) can be performed remotely\",\n \"Authentication is not required for remote access\",\n \"The management interface is accessible from the public internet\",\n \"Static credentials are used for administration\",\n \"None of the above\"\n ],\n \"validation\": {\n \"required\": true\n }\n },\n {\n \"category\": \"Operating Environment\",\n \"question\": \"Are any of the following statements true about this device?\",\n \"description\": \"This informs us about what other systems and processes this device is a part of.\",\n \"type\": \"select-multiple\",\n \"options\": [\n \"The device monitors an environment for active risks to human life.\",\n \"The device is used to convey people, or critical property.\",\n \"The device controls robotics in human-accessible spaces.\",\n \"The device controls physical access systems.\",\n \"The device is involved in processes required by regulations, or compliance. (ex. privacy, security, safety regulations)\",\n \"The device's failure would cause faults in other high-criticality processes.\",\n \"None of the above\"\n ],\n \"validation\": {\n \"required\": true\n }\n },\n {\n \"question\": \"Comments\",\n \"description\": \"Anything else to share?\",\n \"type\": \"text-long\",\n \"validation\": {\n \"max\": \"512\"\n }\n }\n]"
+ }
+ ]
},
{
- "name": "List Certificates",
+ "name": "List Profiles",
"request": {
"method": "GET",
"header": [],
"url": {
- "raw": "http://localhost:8000/system/config/certs/list",
+ "raw": "http://localhost:8000/profiles",
"protocol": "http",
"host": [
"localhost"
],
- "port": "8000",
- "path": [
- "system",
- "config",
- "certs",
- "list"
- ]
+ "port": "8000",
+ "path": [
+ "profiles"
+ ]
+ },
+ "description": "Get a list of risk assessment profiles saved by the user"
+ },
+ "response": [
+ {
+ "name": "List Profiles (200)",
+ "originalRequest": {
+ "method": "GET",
+ "header": [],
+ "url": {
+ "raw": "http://localhost:8000/profiles",
+ "protocol": "http",
+ "host": [
+ "localhost"
+ ],
+ "port": "8000",
+ "path": [
+ "profiles"
+ ]
+ }
+ },
+ "status": "OK",
+ "code": 200,
+ "_postman_previewlanguage": "json",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json",
+ "description": ""
+ }
+ ],
+ "cookie": [],
+ "body": "[\n {\n \"name\": \"New Profile\",\n \"version\": \"2.0\",\n \"created\": \"2024-10-08\",\n \"status\": \"Valid\",\n \"risk\": \"High\",\n \"questions\": [\n {\n \"question\": \"How will this device be used at Google?\",\n \"answer\": \"Yes\"\n },\n {\n \"question\": \"Is this device going to be managed by Google or a third party?\",\n \"answer\": \"Google\",\n \"risk\": \"Limited\"\n },\n {\n \"question\": \"Will the third-party device administrator be able to grant access to authorized Google personnel upon request?\",\n \"answer\": \"No\",\n \"risk\": \"Limited\"\n },\n {\n \"question\": \"Which of the following statements are true about this device?\",\n \"answer\": [\n 0,\n 1,\n 2\n ],\n \"risk\": \"High\"\n },\n {\n \"question\": \"Does the network protocol assure server-to-client identity verification?\",\n \"answer\": \"Yes\",\n \"risk\": \"Limited\"\n },\n {\n \"question\": \"Click the statements that best describe the characteristics of this device.\",\n \"answer\": [\n 0\n ],\n \"risk\": \"High\"\n },\n {\n \"question\": \"Are any of the following statements true about this device?\",\n \"answer\": [\n 0\n ],\n \"risk\": \"High\"\n },\n {\n \"question\": \"Comments\",\n \"answer\": \"\"\n }\n ]\n }\n]"
+ },
+ {
+ "name": "No Profiles (200)",
+ "originalRequest": {
+ "method": "GET",
+ "header": [],
+ "url": {
+ "raw": "http://localhost:8000/profiles",
+ "protocol": "http",
+ "host": [
+ "localhost"
+ ],
+ "port": "8000",
+ "path": [
+ "profiles"
+ ]
+ }
+ },
+ "status": "OK",
+ "code": 200,
+ "_postman_previewlanguage": "json",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json",
+ "description": ""
+ }
+ ],
+ "cookie": [],
+ "body": "[\n \n]"
}
- },
- "response": []
+ ]
},
{
- "name": "Upload Certificate",
+ "name": "Update Profile",
"request": {
"method": "POST",
"header": [],
"body": {
- "mode": "file",
- "file": {}
+ "mode": "raw",
+ "raw": "{\n \"name\": \"Testing\",\n \"status\": \"Valid\",\n \"questions\": [\n {\n \"question\": \"What type of device is this?\",\n \"answer\": \"IoT Gateway\"\n },\n {\n \"question\": \"How will this device be used at Google?\",\n \"answer\": \"asdasd\"\n },\n {\n \"question\": \"Is this device going to be managed by Google or a third party?\",\n \"answer\": \"Google\"\n },\n {\n \"question\": \"Will the third-party device administrator be able to grant access to authorized Google personnel upon request?\",\n \"answer\": \"N/A\"\n },\n {\n \"question\": \"Are any of the following statements true about your device?\",\n \"answer\": [\n 3\n ]\n },\n {\n \"question\": \"Which of the following statements are true about this device?\",\n \"answer\": [\n 5\n ]\n },\n {\n \"question\": \"Does the network protocol assure server-to-client identity verification?\",\n \"answer\": \"Yes\"\n },\n {\n \"question\": \"Click the statements that best describe the characteristics of this device.\",\n \"answer\": [\n 5\n ]\n },\n {\n \"question\": \"Are any of the following statements true about this device?\",\n \"answer\": [\n 6\n ]\n },\n {\n \"question\": \"Comments\",\n \"answer\": \"\"\n }\n ]\n}",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
},
"url": {
- "raw": "http://localhost:8000/system/config/certs/upload",
+ "raw": "http://localhost:8000/profiles",
"protocol": "http",
"host": [
"localhost"
],
"port": "8000",
"path": [
- "system",
- "config",
- "certs",
- "upload"
+ "profiles"
]
- }
+ },
+ "description": "Create or update a risk assessment questionnaire response"
},
- "response": []
+ "response": [
+ {
+ "name": "Update Profile (200)",
+ "originalRequest": {
+ "method": "POST",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "[\n {\n \"name\": \"New Profile\",\n \"version\": \"2.0\",\n \"created\": \"2024-10-08\",\n \"status\": \"Valid\",\n \"risk\": \"High\",\n \"questions\": [\n {\n \"question\": \"How will this device be used at Google?\",\n \"answer\": \"Yes\"\n },\n {\n \"question\": \"Is this device going to be managed by Google or a third party?\",\n \"answer\": \"Google\",\n \"risk\": \"Limited\"\n },\n {\n \"question\": \"Will the third-party device administrator be able to grant access to authorized Google personnel upon request?\",\n \"answer\": \"No\",\n \"risk\": \"Limited\"\n },\n {\n \"question\": \"Which of the following statements are true about this device?\",\n \"answer\": [\n 0,\n 1,\n 2\n ],\n \"risk\": \"High\"\n },\n {\n \"question\": \"Does the network protocol assure server-to-client identity verification?\",\n \"answer\": \"Yes\",\n \"risk\": \"Limited\"\n },\n {\n \"question\": \"Click the statements that best describe the characteristics of this device.\",\n \"answer\": [\n 0\n ],\n \"risk\": \"High\"\n },\n {\n \"question\": \"Are any of the following statements true about this device?\",\n \"answer\": [\n 0\n ],\n \"risk\": \"High\"\n },\n {\n \"question\": \"Comments\",\n \"answer\": \"\"\n }\n ]\n }\n]",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "http://localhost:8000/profiles",
+ "protocol": "http",
+ "host": [
+ "localhost"
+ ],
+ "port": "8000",
+ "path": [
+ "profiles"
+ ]
+ }
+ },
+ "status": "OK",
+ "code": 200,
+ "_postman_previewlanguage": "json",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json",
+ "name": "Content-Type",
+ "description": "",
+ "type": "text"
+ }
+ ],
+ "cookie": [],
+ "body": "{\n \"success\": \"Successfully updated that profile\"\n}"
+ },
+ {
+ "name": "Create Profile (201)",
+ "originalRequest": {
+ "method": "POST",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "[\n {\n \"name\": \"New Profile\",\n \"version\": \"2.0\",\n \"created\": \"2024-10-08\",\n \"status\": \"Valid\",\n \"risk\": \"High\",\n \"questions\": [\n {\n \"question\": \"How will this device be used at Google?\",\n \"answer\": \"Yes\"\n },\n {\n \"question\": \"Is this device going to be managed by Google or a third party?\",\n \"answer\": \"Google\",\n \"risk\": \"Limited\"\n },\n {\n \"question\": \"Will the third-party device administrator be able to grant access to authorized Google personnel upon request?\",\n \"answer\": \"No\",\n \"risk\": \"Limited\"\n },\n {\n \"question\": \"Which of the following statements are true about this device?\",\n \"answer\": [\n 0,\n 1,\n 2\n ],\n \"risk\": \"High\"\n },\n {\n \"question\": \"Does the network protocol assure server-to-client identity verification?\",\n \"answer\": \"Yes\",\n \"risk\": \"Limited\"\n },\n {\n \"question\": \"Click the statements that best describe the characteristics of this device.\",\n \"answer\": [\n 0\n ],\n \"risk\": \"High\"\n },\n {\n \"question\": \"Are any of the following statements true about this device?\",\n \"answer\": [\n 0\n ],\n \"risk\": \"High\"\n },\n {\n \"question\": \"Comments\",\n \"answer\": \"\"\n }\n ]\n }\n]",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "http://localhost:8000/profiles",
+ "protocol": "http",
+ "host": [
+ "localhost"
+ ],
+ "port": "8000",
+ "path": [
+ "profiles"
+ ]
+ }
+ },
+ "status": "Created",
+ "code": 201,
+ "_postman_previewlanguage": "json",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json",
+ "name": "Content-Type",
+ "description": "",
+ "type": "text"
+ }
+ ],
+ "cookie": [],
+ "body": "{\n \"success\": \"Successfully created a new profile\"\n}"
+ },
+ {
+ "name": "Invalid JSON (400)",
+ "originalRequest": {
+ "method": "POST",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n\n}",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "http://localhost:8000/profiles",
+ "protocol": "http",
+ "host": [
+ "localhost"
+ ],
+ "port": "8000",
+ "path": [
+ "profiles"
+ ]
+ }
+ },
+ "status": "Bad Request",
+ "code": 400,
+ "_postman_previewlanguage": "json",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json",
+ "name": "Content-Type",
+ "description": "",
+ "type": "text"
+ }
+ ],
+ "cookie": [],
+ "body": "{\n \"error\": \"Invalid request received\"\n}"
+ },
+ {
+ "name": "Not Implemented (501)",
+ "originalRequest": {
+ "method": "POST",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "[\n {\n \"name\": \"New Profile\",\n \"version\": \"2.0\",\n \"created\": \"2024-10-08\",\n \"status\": \"Valid\",\n \"risk\": \"High\",\n \"questions\": [\n {\n \"question\": \"How will this device be used at Google?\",\n \"answer\": \"Yes\"\n },\n {\n \"question\": \"Is this device going to be managed by Google or a third party?\",\n \"answer\": \"Google\",\n \"risk\": \"Limited\"\n },\n {\n \"question\": \"Will the third-party device administrator be able to grant access to authorized Google personnel upon request?\",\n \"answer\": \"No\",\n \"risk\": \"Limited\"\n },\n {\n \"question\": \"Which of the following statements are true about this device?\",\n \"answer\": [\n 0,\n 1,\n 2\n ],\n \"risk\": \"High\"\n },\n {\n \"question\": \"Does the network protocol assure server-to-client identity verification?\",\n \"answer\": \"Yes\",\n \"risk\": \"Limited\"\n },\n {\n \"question\": \"Click the statements that best describe the characteristics of this device.\",\n \"answer\": [\n 0\n ],\n \"risk\": \"High\"\n },\n {\n \"question\": \"Are any of the following statements true about this device?\",\n \"answer\": [\n 0\n ],\n \"risk\": \"High\"\n },\n {\n \"question\": \"Comments\",\n \"answer\": \"\"\n }\n ]\n }\n]",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "http://localhost:8000/profiles",
+ "protocol": "http",
+ "host": [
+ "localhost"
+ ],
+ "port": "8000",
+ "path": [
+ "profiles"
+ ]
+ }
+ },
+ "status": "Not Implemented",
+ "code": 501,
+ "_postman_previewlanguage": "json",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json",
+ "description": "",
+ "type": "text"
+ }
+ ],
+ "cookie": [],
+ "body": "{\n \"error\": \"Risk profiles are not available right now\"\n}"
+ }
+ ]
},
{
- "name": "Delete Certificate",
+ "name": "Delete Profile",
"request": {
"method": "DELETE",
"header": [],
"body": {
"mode": "raw",
- "raw": "{\n \"name\": \"iot.bms.google.com\"\n}",
+ "raw": "{\n \"name\": \"New Profile\"\n}",
"options": {
"raw": {
"language": "json"
@@ -1043,21 +3179,184 @@
}
},
"url": {
- "raw": "http://localhost:8000/system/config/certs/delete",
+ "raw": "http://localhost:8000/profiles",
"protocol": "http",
"host": [
"localhost"
],
"port": "8000",
"path": [
- "system",
- "config",
- "certs",
- "delete"
+ "profiles"
]
- }
+ },
+ "description": "Delete an existing risk assessment questionaire response"
},
- "response": []
+ "response": [
+ {
+ "name": "Delete Profile (200)",
+ "originalRequest": {
+ "method": "DELETE",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"name\": \"New Profile\"\n}",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "http://localhost:8000/profiles",
+ "protocol": "http",
+ "host": [
+ "localhost"
+ ],
+ "port": "8000",
+ "path": [
+ "profiles"
+ ]
+ }
+ },
+ "status": "OK",
+ "code": 200,
+ "_postman_previewlanguage": "json",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json",
+ "name": "Content-Type",
+ "description": "",
+ "type": "text"
+ }
+ ],
+ "cookie": [],
+ "body": "{\n \"success\": \"Successfully deleted that profile\"\n}"
+ },
+ {
+ "name": "Not Found (404)",
+ "originalRequest": {
+ "method": "DELETE",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"name\": \"non-existing profile\"\n}",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "http://localhost:8000/profiles",
+ "protocol": "http",
+ "host": [
+ "localhost"
+ ],
+ "port": "8000",
+ "path": [
+ "profiles"
+ ]
+ }
+ },
+ "status": "Not Found",
+ "code": 404,
+ "_postman_previewlanguage": "json",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json",
+ "name": "Content-Type",
+ "description": "",
+ "type": "text"
+ }
+ ],
+ "cookie": [],
+ "body": "{\n \"error\": \"A profile with that name could not be found\"\n}"
+ },
+ {
+ "name": "Internal Serves Error (500)",
+ "originalRequest": {
+ "method": "DELETE",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"name\": \"non-existing profile\"\n}",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "http://localhost:8000/profiles",
+ "protocol": "http",
+ "host": [
+ "localhost"
+ ],
+ "port": "8000",
+ "path": [
+ "profiles"
+ ]
+ }
+ },
+ "status": "Internal Server Error",
+ "code": 500,
+ "_postman_previewlanguage": "json",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json",
+ "name": "Content-Type",
+ "description": "",
+ "type": "text"
+ }
+ ],
+ "cookie": [],
+ "body": "{\n \"error\": \"An error occurred whilst deleting that profile\"\n}"
+ },
+ {
+ "name": "Invalid JSON (400)",
+ "originalRequest": {
+ "method": "DELETE",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n\n}",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "http://localhost:8000/profiles",
+ "protocol": "http",
+ "host": [
+ "localhost"
+ ],
+ "port": "8000",
+ "path": [
+ "profiles"
+ ]
+ }
+ },
+ "status": "Bad Request",
+ "code": 400,
+ "_postman_previewlanguage": "json",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json",
+ "name": "Content-Type",
+ "description": "",
+ "type": "text"
+ }
+ ],
+ "cookie": [],
+ "body": "{\n \"error\": \"Invalid request received\"\n}"
+ }
+ ]
}
]
}
\ No newline at end of file
diff --git a/docs/get_started.md b/docs/get_started.md
index dbe5eab43..370f98a77 100644
--- a/docs/get_started.md
+++ b/docs/get_started.md
@@ -7,6 +7,7 @@ This page covers the following topics:
- [Prerequisites](#prerequisites)
- [Installation](#installation)
- [Testing](#testing)
+- [Additional Configuration Options](#additional-configuration-options)
- [Troubleshooting](#troubleshooting)
- [Review the report](#review-the-report)
- [Uninstall](#uninstall)
@@ -20,7 +21,7 @@ We recommend that you run Testrun on a stand-alone machine that has a fresh inst
Before you start, ensure you have the following hardware:
- PC running Ubuntu LTS (laptop or desktop)
-- 2x USB Ethernet adapter (one may be a built-in Ethernet port)
+- 2x ethernet ports (USB ethernet adapters work too)
- Internet connection

@@ -53,8 +54,6 @@ Follow these steps to install Testrun:
Testrun installs under the `/usr/local/testrun` directory. Testing data is available in the `local/devices/{device}/reports` folders.
-Note: Local CA certificates should be uploaded within Testrun to run TLS server testing.
-

# Testing
@@ -66,10 +65,10 @@ Follow these steps to start Testrun:
- Connect one USB Ethernet adapter to the internet source (e.g., router or switch) using an Ethernet cable.
- Connect the other USB Ethernet adapter directly to the IoT device you want to test using an Ethernet cable.
-Notes:
-
-- Disable both adapters in the host system (IPv4, IPv6, and general) by opening **Settings**, then **Network**.
-- Keep the DUT powered off until prompted.
+ Notes:
+
+ - Disable both adapters in the host system (IPv4, IPv6, and general) by opening **Settings**, then **Network**.
+ - Keep the DUT powered off until prompted.
1. Start Testrun with the command `sudo testrun`
- To run Testrun in network-only mode (without running any tests), use the `--net-only` option.
@@ -83,24 +82,88 @@ Follow these steps to test your IoT device:
1. Open Testrun by navigating to [http://localhost:8080](http://localhost:8080/) in your browser.
2. Select the **Settings** menu in the top-right corner, then select your network interfaces. You can change the settings at any time.

-
-3. Select the **device repository** icon on the left panel to add a new device for testing.
+3. Select the **Certificates** menu in the top-right corner, then upload your local CA certificates for TLS server testing.
+ 
+5. Select the **Device Repository** icon on the left panel to add a new device for testing.

-
-4. Select the **Add Device** button.
-5. Enter the MAC address, manufacturer name, and model number.
-6. Select the test modules you want to enable for this device.
+6. Select the **Add Device** button.
+7. Enter the MAC address, manufacturer name, and model number.
+8. Select the test modules you want to enable for this device.
Note: For qualification purposes, you must select all.
-7. Select **Save**.
-8. Select the Testrun progress icon, then select the **Testing** button.
+9. Select **Save**.
+10. Select the Testrun progress icon, then select the **Testing** button.
-9. Select the device you want to test.
-10. Enter the version number of the firmware running on the device.
-11. Select **Start Testrun**.
+11. Select the device you want to test.
+12. Enter the version number of the firmware running on the device.
+13. Select **Start Testrun**.
- If you need to stop Testrun during testing, select **Stop** next to the test name.
12. Once the Waiting for Device notification appears, power on the device under test. A report appears under the Reports icon once the test sequence is complete.

+# Additional Configuration Options
+
+Some configuration options are available but not exposed through the user interface and requires direct access.
+Modification of various configuration files is necessary to access these options.
+
+## Override test module timeout at the system level
+
+Testrun attempts to set reasonable timeouts for test modules to prevent overly long test times but sometimes
+a device or series of device may require longer than these default values. These can be overridden at
+the test module configuration level but is not preferred since these changes will be undone during every
+version upgrade. To modify these values:
+
+1. Navigate to the testrun installation directory. By default, this will be at:
+ `/usr/local/testrun`
+
+2. Open the system.json file and add the following section:
+ `"test_modules":{}`
+
+3. Add the module name(s) and timeout property into this test_modules section you wish to
+set the timeout property for:
+ ```
+ "test_modules":{
+ "connection":{
+ "timeout": 500
+ }
+ }
+ ```
+
+Before timeout options:
+```
+{
+ "network": {
+ "device_intf": "ens0",
+ "internet_intf": "ens1"
+ },
+ "log_level": "DEBUG",
+ "startup_timeout": 60,
+ "monitor_period": 60,
+ "max_device_reports": 5,
+ "org_name": "",
+ "single_intf": false
+ }
+```
+
+After timeout options:
+```
+{
+ "network": {
+ "device_intf": "ens0",
+ "internet_intf": "ens1"
+ },
+ "log_level": "DEBUG",
+ "startup_timeout": 60,
+ "monitor_period": 60,
+ "max_device_reports": 5,
+ "org_name": "",
+ "single_intf": false,
+ "test_modules":{
+ "connection":{
+ "timeout": 500
+ }
+ }
+```
+
# Troubleshooting
If you encounter any issues, try the following:
@@ -118,4 +181,4 @@ Once you complete a test attempt, you can review the test report provided by Tes
# Uninstall
-To uninstall Testrun correctly, use the built-in dpkg uninstall command: `sudo apt-get remove testrun`
\ No newline at end of file
+To uninstall Testrun correctly, use the built-in dpkg uninstall command: `sudo apt-get remove testrun`
diff --git a/docs/roadmap.pdf b/docs/roadmap.pdf
deleted file mode 100644
index 0566598e0..000000000
Binary files a/docs/roadmap.pdf and /dev/null differ
diff --git a/docs/rooadmap.pdf b/docs/rooadmap.pdf
new file mode 100644
index 000000000..6107370e6
Binary files /dev/null and b/docs/rooadmap.pdf differ
diff --git a/docs/test/statuses.md b/docs/test/statuses.md
index 25c3d77b2..8268b3e88 100644
--- a/docs/test/statuses.md
+++ b/docs/test/statuses.md
@@ -21,6 +21,7 @@ Testrun determines whether the device needs each test to receive an overall comp
- Recommended: The device should implement the feature but won't receive an overall Non-Compliant if it's not implemented.
- Roadmap: The device should implement this feature in the future, but it's not required at the moment.
- Required If Applicable: If the device implements this feature, it must be implemented correctly (per the test requirements).
+- Informational: Regardless whether the device implements the feature or not, the test will receive an Informational result.
# Testrun statuses
diff --git a/docs/ui/getstarted--j21skepmx1.png b/docs/ui/getstarted--j21skepmx1.png
new file mode 100644
index 000000000..7a4de661a
Binary files /dev/null and b/docs/ui/getstarted--j21skepmx1.png differ
diff --git a/framework/python/src/api/api.py b/framework/python/src/api/api.py
index 0d633987c..2bba5e62f 100644
--- a/framework/python/src/api/api.py
+++ b/framework/python/src/api/api.py
@@ -277,7 +277,9 @@ async def start_testrun(self, request: Request, response: Response):
if self._testrun.get_session().get_status() in [
TestrunStatus.IN_PROGRESS,
TestrunStatus.WAITING_FOR_DEVICE,
- TestrunStatus.MONITORING
+ TestrunStatus.MONITORING,
+ TestrunStatus.VALIDATING
+
]:
LOGGER.debug("Testrun is already running. Cannot start another instance")
response.status_code = status.HTTP_409_CONFLICT
@@ -338,7 +340,8 @@ async def stop_testrun(self, response: Response):
if (self._testrun.get_session().get_status()
not in [TestrunStatus.IN_PROGRESS,
TestrunStatus.WAITING_FOR_DEVICE,
- TestrunStatus.MONITORING]):
+ TestrunStatus.MONITORING,
+ TestrunStatus.VALIDATING]):
response.status_code = 404
return self._generate_msg(False, "Testrun is not currently running")
@@ -975,6 +978,20 @@ async def upload_cert(self, file: UploadFile, response: Response):
False, "A certificate with that common name already exists."
)
+ # Returned when organization name is missing
+ elif str(e) == "Certificate is missing the organization name":
+ response.status_code = status.HTTP_400_BAD_REQUEST
+ return self._generate_msg(
+ False, "The certificate must contain the organization name"
+ )
+
+ # Returned when common name is missing
+ elif str(e) == "Certificate is missing the common name":
+ response.status_code = status.HTTP_400_BAD_REQUEST
+ return self._generate_msg(
+ False, "The certificate must contain the common name"
+ )
+
# Returned when unable to load PEM file
else:
response.status_code = status.HTTP_400_BAD_REQUEST
diff --git a/framework/python/src/common/mqtt.py b/framework/python/src/common/mqtt.py
index fc0458e7d..b98b4ab1b 100644
--- a/framework/python/src/common/mqtt.py
+++ b/framework/python/src/common/mqtt.py
@@ -32,8 +32,6 @@ class MQTT:
def __init__(self) -> None:
self._host = WEBSOCKETS_HOST
self._client = mqtt_client.Client(mqtt_client.CallbackAPIVersion.VERSION2)
- self._client.enable_logger(LOGGER)
- LOGGER.setLevel(logger.logging.INFO)
def _connect(self):
"""Establish connection to MQTT broker"""
@@ -46,6 +44,7 @@ def _connect(self):
def disconnect(self):
"""Disconnect the local client from the MQTT broker"""
if self._client.is_connected():
+ LOGGER.debug("Disconnecting from broker")
self._client.disconnect()
def send_message(self, topic: str, message: t.Union[str, dict]) -> None:
diff --git a/framework/python/src/common/risk_profile.py b/framework/python/src/common/risk_profile.py
index eeae44db7..559117aec 100644
--- a/framework/python/src/common/risk_profile.py
+++ b/framework/python/src/common/risk_profile.py
@@ -22,6 +22,7 @@
import os
from jinja2 import Template
from copy import deepcopy
+import math
PROFILES_PATH = 'local/risk_profiles'
LOGGER = logger.get_logger('risk_profile')
@@ -366,7 +367,25 @@ def to_html(self, device):
)
def _generate_report_pages(self):
- max_page_height = 350
+
+ # Text block heght
+ block_height = 18
+ # Table row padding
+ block_padding = 30
+ # Margin bottom list answer
+ margin_list = 14
+ # margin after table row
+ margin_row = 8
+
+ height_first_page = 760
+ height_page = 980
+
+ # Average text block width in characters
+ # for a 14px font size (average width of one character is 8px).
+ letters_in_line_str = 38
+ letters_in_line_q = 40
+ letters_in_line_list = 36
+
height = 0
pages = []
current_page = []
@@ -377,47 +396,55 @@ def _generate_report_pages(self):
for question in questions:
- if height > max_page_height:
- pages.append(current_page)
- height = 0
- current_page = []
-
page_item = deepcopy(question)
+ answer_height = 0
+ # Question height calculation
+ question_height = math.ceil(len(page_item['question'])
+ / letters_in_line_q
+ ) * block_height
+ question_height += block_padding + margin_row
if isinstance(page_item['answer'], str):
-
- if len(page_item['answer']) > 400:
- height += 160
- elif len(page_item['answer']) > 300:
- height += 140
- elif len(page_item['answer']) > 200:
- height += 120
- elif len(page_item['answer']) > 100:
- height += 70
- else:
- height += 53
-
- # Select multiple answers
- elif isinstance(page_item['answer'], list):
+ # Answer height for string
+ answer_height = math.ceil(len(page_item['answer'])
+ / letters_in_line_str
+ ) * block_height
+ answer_height += block_padding + margin_row
+ else:
text_answers = []
-
options = self._get_format_question(
question=page_item['question'],
profile_format=self._profile_format)['options']
-
options_dict = dict(enumerate(options))
-
for answer_index in page_item['answer']:
height += 40
text_answers.append(options_dict[answer_index]['text'])
page_item['answer'] = text_answers
+ # Answer height for list
+ for answer in options:
+ answer_height += math.ceil(len(answer)
+ / letters_in_line_list
+ ) * block_height
+ answer_height += block_padding + margin_row + margin_list
page_item['index'] = index
+ row_height = max(question_height, answer_height)
+
+ if (
+ (len(pages) == 0 and row_height + height > height_first_page)
+ or (len(pages) > 0 and row_height + height > height_page)
+ ):
+ pages.append(current_page)
+ height = 0
+ current_page = [page_item]
+ else:
+ height += row_height
+ current_page.append(page_item)
index += 1
- current_page.append(page_item)
pages.append(current_page)
return pages
+
def to_pdf(self, device):
"""Returns the current risk profile in PDF format"""
diff --git a/framework/python/src/common/statuses.py b/framework/python/src/common/statuses.py
index 4817d7cf8..c7487868a 100644
--- a/framework/python/src/common/statuses.py
+++ b/framework/python/src/common/statuses.py
@@ -23,6 +23,7 @@ class TestrunStatus:
COMPLIANT = "Compliant"
NON_COMPLIANT = "Non-Compliant"
STOPPING = "Stopping"
+ VALIDATING = "Validating Network"
class TestResult:
diff --git a/framework/python/src/common/testreport.py b/framework/python/src/common/testreport.py
index f9401fe80..990a3427a 100644
--- a/framework/python/src/common/testreport.py
+++ b/framework/python/src/common/testreport.py
@@ -23,6 +23,9 @@
from test_orc.test_case import TestCase
from jinja2 import Environment, FileSystemLoader
from collections import OrderedDict
+import re
+from bs4 import BeautifulSoup
+
DATE_TIME_FORMAT = '%Y-%m-%d %H:%M:%S'
RESOURCES_DIR = 'resources/report'
@@ -330,12 +333,43 @@ def _get_optional_steps_to_resolve(self, json_data):
return tests_with_recommendations
+
+ def _split_module_report_to_pages(self, reports):
+ """Split report to pages by headers"""
+ reports_transformed = []
+
+ for report in reports:
+ if len(re.findall('
1:
+ indices = []
+ index = report.find('' in line and data_table_active:
- data_table_active=False
-
- # Add module-data header size, ignore rows, should
- # only be one so only care about a header existence
- elif '' in line and data_table_active:
- content_size += 41.333
-
- # Track module-data table state
- elif '' in line and data_table_active:
- data_rows_active = True
- elif '' in line and data_rows_active:
- data_rows_active = False
-
- # Add appropriate content size for each data row
- # update if CSS changes for this element
- elif '' in line and data_rows_active:
- content_size += 42
-
- # If the current line is within the content size limit
- # we'll add it to this page, otherweise, we'll put it on the next
- # page. Also make sure that if there is less than 40 pixels
- # left after a data row, start a new page or the row will get cut off.
- # Current row size is 42 # adjust if we update the
- # "module-data tbody tr" element.
- if content_size >= content_max_size or (
- data_rows_active and content_max_size - content_size < 42):
- # If in the middle of a table, close the table
- if data_rows_active:
- page_content += '
'
- reports.append(page_content)
- content_size = 0
- # If in the middle of a data table, restart
- # it for the rest of the rows
- page_content = ('\n'
- if data_rows_active else '')
- page_content += line + '\n'
- if len(page_content) > 0:
- reports.append(page_content)
+
+ # Convert module report to list of html tags
+ soup = BeautifulSoup(module_report, features='html5lib')
+ children = list(
+ filter(lambda el: el.name is not None, soup.body.children)
+ )
+
+ for index, el in enumerate(children):
+ current_size = 0
+ if el.name == 'h1':
+ current_size += 40 + h1_padding
+ # Calculating the height of paired tables
+ elif (el.name == 'div'
+ and el.get('id') == 'tls_table'):
+ tables = el.findChildren('table', recursive=True)
+ current_size = max(
+ map(lambda t: len(
+ t.findChildren('tr', recursive=True)
+ ), tables)
+ ) * 42
+ # Table height
+ elif el.name == 'table':
+ if el['class'] == 'module-summary':
+ current_size = 85 + module_summary_padding
+ else:
+ current_size = len(el.findChildren('tr', recursive=True)) * 42
+ # Other elements height
+ else:
+ current_size = 50
+ # Moving tables to the next page.
+ # Completely transfer tables that are within the maximum
+ # allowable size, while splitting those that exceed the page size.
+ if (content_size + current_size) >= content_max_size:
+ str_el = ''
+ if current_size > (content_max_size - 85 - module_summary_padding):
+ rows = el.findChildren('tr', recursive=True)
+ table_header = str(rows.pop(0))
+ table_1 = table_2 = f'''
+
+ {table_header}'''
+ rows_count = (content_max_size - 85 - module_summary_padding) // 42
+ table_1 += ''.join(map(str, rows[:rows_count-1]))
+ table_1 += '
'
+ table_2 += ''.join(map(str, rows[rows_count-1:]))
+ table_2 += '
'
+ page_content += table_1
+ reports.append(page_content)
+ page_content = table_2
+ current_size = len(rows[rows_count:]) * 42
+ else:
+ if el.name == 'table':
+ el_header = children[index-1]
+ if el_header.name.startswith('h'):
+ page_content = ''.join(page_content.rsplit(str(el_header), 1))
+ str_el = str(el_header) + str(el)
+ content_size = current_size + 50
+ else:
+ str_el = str(el)
+ content_size = current_size
+ reports.append(page_content)
+ page_content = str_el
+ else:
+ page_content += str(el)
+ content_size += current_size
+ reports.append(page_content)
return reports
diff --git a/framework/python/src/common/util.py b/framework/python/src/common/util.py
index ba1b23e81..7bb7ea73f 100644
--- a/framework/python/src/common/util.py
+++ b/framework/python/src/common/util.py
@@ -24,7 +24,7 @@
LOGGER = logger.get_logger('util')
-def run_command(cmd, output=True, timeout=None):
+def run_command(cmd, output=True, timeout=None, supress_error=False):
"""Runs a process at the os level
By default, returns the standard output and error output
If the caller sets optional output parameter to False,
@@ -38,7 +38,7 @@ def run_command(cmd, output=True, timeout=None):
stderr=subprocess.PIPE) as process:
stdout, stderr = process.communicate(timeout)
- if process.returncode != 0 and output:
+ if process.returncode != 0 and output and not supress_error:
err_msg = f'{stderr.strip()}. Code: {process.returncode}'
LOGGER.error('Command failed: ' + cmd)
LOGGER.error('Error: ' + err_msg)
diff --git a/framework/python/src/core/docker/test_docker_module.py b/framework/python/src/core/docker/test_docker_module.py
index 4bbf72594..3198ef1ba 100644
--- a/framework/python/src/core/docker/test_docker_module.py
+++ b/framework/python/src/core/docker/test_docker_module.py
@@ -27,11 +27,13 @@
class TestModule(Module):
"""Represents a test module."""
- def __init__(self, module_config_file, session, extra_hosts):
+ def __init__(self, module_config_file, test_orc, session, extra_hosts):
super().__init__(module_config_file=module_config_file,
session=session,
extra_hosts=extra_hosts)
+ self._test_orc = test_orc
+
# Set IP Index for all test modules
self.ip_index = 9
@@ -39,11 +41,9 @@ def setup_module(self, module_json):
# Set the defaults
self.network = True
self.total_tests = 0
- self.time = DEFAULT_TIMEOUT
self.tests: list = []
- if 'timeout' in module_json['config']['docker']:
- self.timeout = module_json['config']['docker']['timeout']
+ self.timeout = self._get_module_timeout(module_json)
# Determine if this module needs network access
if 'network' in module_json['config']:
@@ -93,12 +93,17 @@ def _setup_runtime(self, device):
util.run_command(f'chown -R {host_user} {self.device_monitor_capture}')
def get_environment(self, device):
+
+ # Obtain the test pack
+ test_pack = self._test_orc.get_test_pack(device.test_pack)
+
environment = {
'TZ': self.get_session().get_timezone(),
'HOST_USER': self.get_session().get_host_user(),
'DEVICE_MAC': device.mac_addr,
'IPV4_ADDR': device.ip_addr,
'DEVICE_TEST_MODULES': json.dumps(device.test_modules),
+ 'DEVICE_TEST_PACK': json.dumps(test_pack.to_dict()),
'IPV4_SUBNET': self.get_session().get_ipv4_subnet(),
'IPV6_SUBNET': self.get_session().get_ipv6_subnet(),
'DEV_IFACE': self.get_session().get_device_interface(),
@@ -133,3 +138,20 @@ def get_mounts(self):
read_only=True)
]
return mounts
+
+ def _get_module_timeout(self, module_json):
+ timeout = DEFAULT_TIMEOUT
+ try:
+ timeout = DEFAULT_TIMEOUT
+ test_modules = self.get_session().get_config().get('test_modules', {})
+ test_config = test_modules.get(self.name, {})
+ sys_timeout = test_config.get('timeout', None)
+
+ if sys_timeout is not None:
+ timeout = sys_timeout
+ elif 'timeout' in module_json['config']['docker']:
+ timeout = module_json['config']['docker']['timeout']
+ except Exception: # pylint: disable=W0718
+ # Ignore errors, just use default
+ timeout = DEFAULT_TIMEOUT
+ return timeout # pylint: disable=W0150
diff --git a/framework/python/src/core/session.py b/framework/python/src/core/session.py
index c3fbc83c5..17d583219 100644
--- a/framework/python/src/core/session.py
+++ b/framework/python/src/core/session.py
@@ -38,6 +38,7 @@
API_PORT_KEY = 'api_port'
MAX_DEVICE_REPORTS_KEY = 'max_device_reports'
ORG_NAME_KEY = 'org_name'
+TEST_CONFIG_KEY = 'test_modules'
CERTS_PATH = 'local/root_certs'
CONFIG_FILE_PATH = 'local/system.json'
STATUS_TOPIC = 'status'
@@ -241,6 +242,11 @@ def _load_config(self):
ORG_NAME_KEY
)
+ if TEST_CONFIG_KEY in config_file_json:
+ self._config[TEST_CONFIG_KEY] = config_file_json.get(
+ TEST_CONFIG_KEY
+ )
+
def _load_version(self):
version_cmd = util.run_command(
'dpkg-query --showformat=\'${Version}\' --show testrun')
@@ -450,10 +456,12 @@ def add_test_result(self, result):
if not updated:
self._results.append(result)
- def set_test_result_error(self, result):
+ def set_test_result_error(self, result, description=None):
"""Set test result error"""
result.result = TestResult.ERROR
result.recommendations = None
+ if description is not None:
+ result.description = description
self._results.append(result)
def add_module_report(self, module_report):
@@ -826,17 +834,30 @@ def upload_cert(self, filename, content):
# Parse bytes into x509 object
cert = x509.load_pem_x509_certificate(content, default_backend())
- # Extract required properties
- common_name = cert.subject.get_attributes_for_oid(
- NameOID.COMMON_NAME)[0].value
+ # Retrieve the common name attributes from the subject
+ common_name_attr = cert.subject.get_attributes_for_oid(NameOID.COMMON_NAME)
+
+ # Raise an error if the common name attribute is missing
+ if not common_name_attr:
+ raise ValueError('Certificate is missing the common name')
+
+ # Extract the organization name value
+ common_name = common_name_attr[0].value
# Check if any existing certificates have the same common name
for cur_cert in self._certs:
if common_name == cur_cert['name']:
raise ValueError('A certificate with that name already exists')
- issuer = cert.issuer.get_attributes_for_oid(
- NameOID.ORGANIZATION_NAME)[0].value
+ # Retrieve the organization name attributes from issuer
+ issuer_attr = cert.issuer.get_attributes_for_oid(NameOID.ORGANIZATION_NAME)
+
+ # Raise an error if the organization name attribute is missing
+ if not issuer_attr:
+ raise ValueError('Certificate is missing the organization name')
+
+ # Extract the organization name value
+ issuer = issuer_attr[0].value
status = 'Valid'
if now > cert.not_valid_after_utc:
diff --git a/framework/python/src/core/test_runner.py b/framework/python/src/core/test_runner.py
index a295f47e1..b15e5e899 100644
--- a/framework/python/src/core/test_runner.py
+++ b/framework/python/src/core/test_runner.py
@@ -11,7 +11,6 @@
# 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.
-
"""Wrapper for the Testrun that simplifies
virtual testing procedure by allowing direct calling
from the command line.
@@ -20,11 +19,20 @@
E.g sudo cmd/start
"""
+# Disable warning about TripleDES being removed from cryptography in 48.0.0
+# Scapy 2.5.0 uses TripleDES
+# Scapy 2.6.0 causes a bug in testrun when the device intf is being restarted
+import warnings
+from cryptography.utils import CryptographyDeprecationWarning
+warnings.filterwarnings("ignore", category=CryptographyDeprecationWarning)
+
+# pylint: disable=wrong-import-position
import argparse
import sys
from testrun import Testrun
from common import logger
import signal
+import io
LOGGER = logger.get_logger("runner")
@@ -37,13 +45,17 @@ def __init__(self,
validate=False,
net_only=False,
single_intf=False,
- no_ui=False):
+ no_ui=False,
+ target=None,
+ firmware=None):
self._register_exits()
self._testrun = Testrun(config_file=config_file,
validate=validate,
net_only=net_only,
single_intf=single_intf,
- no_ui=no_ui)
+ no_ui=no_ui,
+ target_mac=target,
+ firmware=firmware)
def _register_exits(self):
signal.signal(signal.SIGINT, self._exit_handler)
@@ -73,8 +85,7 @@ def parse_args():
"-f",
"--config-file",
default=None,
- help="Define the configuration file for Testrun and Network Orchestrator"
- )
+ help="Define the configuration file for Testrun and Network Orchestrator")
parser.add_argument(
"--validate",
default=False,
@@ -91,7 +102,38 @@ def parse_args():
default=False,
action="store_true",
help="Do not launch the user interface")
+ parser.add_argument("--target",
+ default=None,
+ type=str,
+ help="MAC address of the target device")
+ parser.add_argument("-fw",
+ "--firmware",
+ default=None,
+ type=str,
+ help="Firmware version to be tested")
+
parsed_args = parser.parse_known_args()[0]
+
+ if (parsed_args.no_ui and not parsed_args.net_only
+ and (parsed_args.target is None or parsed_args.firmware is None)):
+ # Capture help text
+ help_text = io.StringIO()
+ parser.print_help(file=help_text)
+
+ # Get help text as lines and find where "Testrun" starts (skip usage)
+ help_lines = help_text.getvalue().splitlines()
+ start_index = next(
+ (i for i, line in enumerate(help_lines) if "Testrun" in line), 0)
+
+ # Join only lines starting from "Testrun" and print without extra newlines
+ help_message = "\n".join(line.rstrip() for line in help_lines[start_index:])
+ print(help_message)
+
+ print(
+ "Error: --target and --firmware are required when --no-ui is specified",
+ file=sys.stderr)
+ sys.exit(1)
+
return parsed_args
@@ -101,4 +143,6 @@ def parse_args():
validate=args.validate,
net_only=args.net_only,
single_intf=args.single_intf,
- no_ui=args.no_ui)
+ no_ui=args.no_ui,
+ target=args.target,
+ firmware=args.firmware)
diff --git a/framework/python/src/core/testrun.py b/framework/python/src/core/testrun.py
index 1855b71b5..5d4e78e9c 100644
--- a/framework/python/src/core/testrun.py
+++ b/framework/python/src/core/testrun.py
@@ -11,7 +11,6 @@
# 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.
-
"""The overall control of the Testrun application.
This file provides the integration between all of the
Testrun components, such as net_orc, test_orc and test_ui.
@@ -55,6 +54,7 @@
MAX_DEVICE_REPORTS_KEY = 'max_device_reports'
+
class Testrun: # pylint: disable=too-few-public-methods
"""Testrun controller.
@@ -67,15 +67,17 @@ def __init__(self,
validate=False,
net_only=False,
single_intf=False,
- no_ui=False):
+ no_ui=False,
+ target_mac=None,
+ firmware=None):
# Locate parent directory
current_dir = os.path.dirname(os.path.realpath(__file__))
# Locate the test-run root directory, 4 levels,
# src->python->framework->test-run
- self._root_dir = os.path.dirname(os.path.dirname(
- os.path.dirname(os.path.dirname(current_dir))))
+ self._root_dir = os.path.dirname(
+ os.path.dirname(os.path.dirname(os.path.dirname(current_dir))))
# Determine config file
if config_file is None:
@@ -103,15 +105,26 @@ def __init__(self,
if validate:
self._session.add_runtime_param('validate')
- self._net_orc = net_orc.NetworkOrchestrator(
- session=self._session)
- self._test_orc = test_orc.TestOrchestrator(
- self._session,
- self._net_orc)
+ self._net_orc = net_orc.NetworkOrchestrator(session=self._session)
+ self._test_orc = test_orc.TestOrchestrator(self._session, self._net_orc)
# Load device repository
self.load_all_devices()
+ # If no_ui selected and not network only mode,
+ # load the target device into the session
+ if self._no_ui and not net_only:
+ target_device = self._session.get_device(target_mac)
+ if target_device is not None:
+ target_device.firmware = firmware
+ self._session.set_target_device(target_device)
+ else:
+ print(
+ f'Target device specified does not exist in device registry: '
+ f'{target_mac}',
+ file=sys.stderr)
+ sys.exit(1)
+
# Load test modules
self._test_orc.start()
@@ -165,8 +178,7 @@ def _load_devices(self, device_dir):
for device_folder in os.listdir(device_dir):
- device_config_file_path = os.path.join(device_dir,
- device_folder,
+ device_config_file_path = os.path.join(device_dir, device_folder,
DEVICE_CONFIG)
# Check if device config file exists before loading
@@ -178,7 +190,14 @@ def _load_devices(self, device_dir):
# Open device config file
with open(device_config_file_path,
encoding='utf-8') as device_config_file:
- device_config_json = json.load(device_config_file)
+
+ try:
+ device_config_json = json.load(device_config_file)
+ except json.decoder.JSONDecodeError as e:
+ LOGGER.error('Invalid JSON found in ' +
+ f'device configuration {device_config_file_path}')
+ LOGGER.debug(e)
+ continue
device_manufacturer = device_config_json.get(DEVICE_MANUFACTURER)
device_model = device_config_json.get(DEVICE_MODEL)
@@ -212,11 +231,11 @@ def _load_devices(self, device_dir):
if DEVICE_ADDITIONAL_INFO_KEY in device_config_json:
device.additional_info = device_config_json.get(
- DEVICE_ADDITIONAL_INFO_KEY)
+ DEVICE_ADDITIONAL_INFO_KEY)
if None in [device.type, device.technology, device.test_pack]:
LOGGER.warning(
- 'Device is outdated and requires further configuration')
+ 'Device is outdated and requires further configuration')
device.status = 'Invalid'
self._load_test_reports(device)
@@ -243,26 +262,20 @@ def _load_test_reports(self, device):
for report_folder in os.listdir(reports_folder):
# 1.3 file path
- report_json_file_path = os.path.join(
- reports_folder,
- report_folder,
- 'test',
- device.mac_addr.replace(':',''),
- 'report.json')
+ report_json_file_path = os.path.join(reports_folder, report_folder,
+ 'test',
+ device.mac_addr.replace(':', ''),
+ 'report.json')
if not os.path.isfile(report_json_file_path):
# Revert to pre 1.3 file path
- report_json_file_path = os.path.join(
- reports_folder,
- report_folder,
- 'report.json')
+ report_json_file_path = os.path.join(reports_folder, report_folder,
+ 'report.json')
if not os.path.isfile(report_json_file_path):
# Revert to pre 1.3 file path
- report_json_file_path = os.path.join(
- reports_folder,
- report_folder,
- 'report.json')
+ report_json_file_path = os.path.join(reports_folder, report_folder,
+ 'report.json')
# Check if the report.json file exists
if not os.path.isfile(report_json_file_path):
@@ -278,9 +291,8 @@ def _load_test_reports(self, device):
def get_reports_folder(self, device):
"""Return the reports folder path for the device"""
- return os.path.join(self._root_dir,
- LOCAL_DEVICES_DIR,
- device.device_folder, 'reports')
+ return os.path.join(self._root_dir, LOCAL_DEVICES_DIR, device.device_folder,
+ 'reports')
def delete_report(self, device: Device, timestamp):
LOGGER.debug(f'Deleting test report for device {device.model} ' +
@@ -301,15 +313,13 @@ def delete_report(self, device: Device, timestamp):
def create_device(self, device: Device):
# Define the device folder location
- device_folder_path = os.path.join(self._root_dir,
- LOCAL_DEVICES_DIR,
+ device_folder_path = os.path.join(self._root_dir, LOCAL_DEVICES_DIR,
device.device_folder)
# Create the directory
os.makedirs(device_folder_path)
- config_file_path = os.path.join(device_folder_path,
- DEVICE_CONFIG)
+ config_file_path = os.path.join(device_folder_path, DEVICE_CONFIG)
with open(config_file_path, 'w', encoding='utf-8') as config_file:
config_file.writelines(json.dumps(device.to_config_json(), indent=4))
@@ -326,10 +336,8 @@ def save_device(self, device: Device):
"""Edit and save an existing device config."""
# Obtain the config file path
- config_file_path = os.path.join(self._root_dir,
- LOCAL_DEVICES_DIR,
- device.device_folder,
- DEVICE_CONFIG)
+ config_file_path = os.path.join(self._root_dir, LOCAL_DEVICES_DIR,
+ device.device_folder, DEVICE_CONFIG)
with open(config_file_path, 'w+', encoding='utf-8') as config_file:
config_file.writelines(json.dumps(device.to_config_json(), indent=4))
@@ -342,9 +350,8 @@ def save_device(self, device: Device):
def delete_device(self, device: Device):
# Obtain the config file path
- device_folder = os.path.join(self._root_dir,
- LOCAL_DEVICES_DIR,
- device.device_folder)
+ device_folder = os.path.join(self._root_dir, LOCAL_DEVICES_DIR,
+ device.device_folder)
# Delete the device directory
shutil.rmtree(device_folder)
@@ -359,17 +366,13 @@ def start(self):
self._start_network()
self.get_net_orc().get_listener().register_callback(
- self._device_discovered,
- [NetworkEvent.DEVICE_DISCOVERED]
- )
+ self._device_discovered, [NetworkEvent.DEVICE_DISCOVERED])
if self._net_only:
LOGGER.info('Network only option configured, no tests will be run')
else:
self.get_net_orc().get_listener().register_callback(
- self._device_stable,
- [NetworkEvent.DEVICE_STABLE]
- )
+ self._device_stable, [NetworkEvent.DEVICE_STABLE])
self.get_net_orc().start_listener()
LOGGER.info('Waiting for devices on the network...')
@@ -504,16 +507,12 @@ def start_ui(self):
client = docker.from_env()
try:
- client.containers.run(
- image='testrun/ui',
- auto_remove=True,
- name='tr-ui',
- hostname='testrun.io',
- detach=True,
- ports={
- '80': 8080
- }
- )
+ client.containers.run(image='testrun/ui',
+ auto_remove=True,
+ name='tr-ui',
+ hostname='testrun.io',
+ detach=True,
+ ports={'80': 8080})
except ImageNotFound as ie:
LOGGER.error('An error occured whilst starting the UI. ' +
'Please investigate and try again.')
@@ -542,16 +541,14 @@ def start_ws(self):
client = docker.from_env()
try:
- client.containers.run(
- image='testrun/ws',
- auto_remove=True,
- name='tr-ws',
- detach=True,
- ports={
- '9001': 9001,
- '1883': 1883
- }
- )
+ client.containers.run(image='testrun/ws',
+ auto_remove=True,
+ name='tr-ws',
+ detach=True,
+ ports={
+ '9001': 9001,
+ '1883': 1883
+ })
except ImageNotFound as ie:
LOGGER.error('An error occured whilst starting the websockets server. ' +
'Please investigate and try again.')
diff --git a/framework/python/src/net_orc/ip_control.py b/framework/python/src/net_orc/ip_control.py
index aa07283af..73a6aceeb 100644
--- a/framework/python/src/net_orc/ip_control.py
+++ b/framework/python/src/net_orc/ip_control.py
@@ -259,7 +259,7 @@ def configure_container_interface(self,
def ping_via_gateway(self, host):
"""Ping the host trough the gateway container"""
command = f'timeout 3 docker exec tr-ct-gateway ping -W 1 -c 1 {host}'
- output = util.run_command(command)
+ output = util.run_command(command, supress_error=True)
if '0% packet loss' in output[0]:
return True
return False
diff --git a/framework/python/src/net_orc/listener.py b/framework/python/src/net_orc/listener.py
index 03fcaaaf8..af79f1cf3 100644
--- a/framework/python/src/net_orc/listener.py
+++ b/framework/python/src/net_orc/listener.py
@@ -16,6 +16,7 @@
under test."""
import threading
from scapy.all import AsyncSniffer, DHCP, get_if_hwaddr
+from scapy.error import Scapy_Exception
from net_orc.network_event import NetworkEvent
from common import logger
@@ -46,7 +47,7 @@ def start_listener(self):
"""Start sniffing packets on the device interface."""
# Don't start the listener if it is already running
- if self._sniffer.running:
+ if self.is_running():
LOGGER.debug('Listener was already running')
return
@@ -58,8 +59,12 @@ def reset(self):
def stop_listener(self):
"""Stop sniffing packets on the device interface."""
- if self._sniffer.running:
- self._sniffer.stop()
+ try:
+ if self.is_running():
+ self._sniffer.stop()
+ LOGGER.debug('Stopped the network listener')
+ except Scapy_Exception as e:
+ LOGGER.error(f'Error stopping the listener: {e}')
def is_running(self):
"""Determine whether the sniffer is running."""
diff --git a/framework/python/src/net_orc/network_orchestrator.py b/framework/python/src/net_orc/network_orchestrator.py
index b8e7befd2..37858c4e1 100644
--- a/framework/python/src/net_orc/network_orchestrator.py
+++ b/framework/python/src/net_orc/network_orchestrator.py
@@ -136,10 +136,16 @@ def start_network(self):
self.create_net()
self.start_network_services()
- if 'validate' in self._session.get_runtime_params():
- # Start the validator after network is ready
- self.validator.start()
-
+ try:
+ if 'validate' in self._session.get_runtime_params():
+ # Start the validator after network is ready
+ self._session.set_status(TestrunStatus.VALIDATING)
+ self.validator.start()
+ self.validator.stop()
+ except Exception as e:
+ LOGGER.error(f'Validation failed {e}')
+
+ self._session.set_status('Waiting for Device')
# Get network ready (via Network orchestrator)
LOGGER.debug('Network is ready')
@@ -416,9 +422,7 @@ def create_net(self):
# a use case is determined
#self._create_private_net()
- # Listener may have already been created. Only create if not
- if self._listener is None:
- self._listener = Listener(self._session)
+ self._listener = Listener(self._session)
self.get_listener().register_callback(self._device_discovered,
[NetworkEvent.DEVICE_DISCOVERED])
diff --git a/framework/python/src/net_orc/network_validator.py b/framework/python/src/net_orc/network_validator.py
index d760970a3..c50d2463d 100644
--- a/framework/python/src/net_orc/network_validator.py
+++ b/framework/python/src/net_orc/network_validator.py
@@ -22,6 +22,7 @@
import getpass
from common import logger
from common import util
+from net_orc.ovs_control import OVSControl
LOGGER = logger.get_logger('validator')
OUTPUT_DIR = 'runtime/validation'
@@ -46,6 +47,8 @@ def __init__(self):
shutil.rmtree(os.path.join(self._path, OUTPUT_DIR), ignore_errors=True)
+ self._ovs = OVSControl(session=None)
+
def start(self):
"""Start the network validator."""
LOGGER.debug('Starting validator')
@@ -87,6 +90,8 @@ def _build_device(self, net_device):
def _load_devices(self):
LOGGER.info(f'Loading validators from {self._device_dir}')
+ # Reset device list before loading
+ self._net_devices = []
loaded_devices = 'Loaded the following validators: '
@@ -286,7 +291,6 @@ def _stop_network_device(self, net_device, kill=False):
LOGGER.error(e)
def _get_device_container(self, net_device):
- LOGGER.debug('Resolving device container: ' + net_device.container_name)
container = None
try:
client = docker.from_env()
@@ -305,6 +309,9 @@ def _stop_network_devices(self, kill=False):
if not net_device.enable_container:
continue
self._stop_network_device(net_device, kill)
+ # Remove the device port form the ovs bridge once validation is done
+ bridge_intf = DEVICE_BRIDGE + 'i-' + net_device.dir_name
+ self._ovs.delete_port(DEVICE_BRIDGE,bridge_intf)
class FauxDevice: # pylint: disable=too-few-public-methods,too-many-instance-attributes
diff --git a/framework/python/src/net_orc/ovs_control.py b/framework/python/src/net_orc/ovs_control.py
index 08faa52c1..2c1e17776 100644
--- a/framework/python/src/net_orc/ovs_control.py
+++ b/framework/python/src/net_orc/ovs_control.py
@@ -59,6 +59,14 @@ def delete_flow(self, bridge_name, flow):
success = util.run_command(f'ovs-ofctl del-flows {bridge_name} \'{flow}\'')
return success
+ def delete_port(self, bridge_name, port):
+ # Delete a port from the bridge using ovs-ofctl commands
+ success=True
+ if self.port_exists(bridge_name, port):
+ LOGGER.debug(f'Deleting port {port} from bridge: {bridge_name}')
+ success = util.run_command(f'ovs-vsctl del-port {bridge_name} \'{port}\'')
+ return success
+
def get_bridge_ports(self, bridge_name):
# Get a list of all the ports on a bridge
response = util.run_command(f'ovs-vsctl list-ports {bridge_name}',
diff --git a/framework/python/src/test_orc/test_orchestrator.py b/framework/python/src/test_orc/test_orchestrator.py
index 4085e91ad..8e275b2cf 100644
--- a/framework/python/src/test_orc/test_orchestrator.py
+++ b/framework/python/src/test_orc/test_orchestrator.py
@@ -120,6 +120,8 @@ def run_test_modules(self):
if not self._is_module_enabled(module, device):
continue
+ num_tests = 0
+
# Add module to list of modules to run
test_modules.append(module)
@@ -128,6 +130,10 @@ def run_test_modules(self):
# Duplicate test obj so we don't alter the source
test_copy = copy.deepcopy(test)
+ # Do not add test if it is not enabled
+ if not self._is_test_enabled(test_copy.name, device):
+ continue
+
# Set result to Not Started
test_copy.result = TestResult.NOT_STARTED
@@ -143,10 +149,13 @@ def run_test_modules(self):
# Add test result to the session
self.get_session().add_test_result(test_copy)
+ # Increment number of tests being run by this module
+ num_tests += 1
+
# Increment number of tests that will be run
- self.get_session().add_total_tests(len(module.tests))
+ self.get_session().add_total_tests(num_tests)
- # Store enabled test modules in the TestsOrchectrator object
+ # Store enabled test modules in the TestOrchectrator object
self._test_modules_running = test_modules
self._current_module = 0
@@ -399,6 +408,7 @@ 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:
@@ -410,6 +420,13 @@ def _is_module_enabled(self, module, device):
return enabled
+ def _is_test_enabled(self, test, device):
+
+ test_pack_name = device.test_pack
+ test_pack = self.get_test_pack(test_pack_name)
+
+ return test_pack.get_test(test) is not None
+
def _run_test_module(self, module):
"""Start the test container and extract the results."""
@@ -441,7 +458,9 @@ def _run_test_module(self, module):
if hasattr(test_copy, "recommendations"):
test_copy.recommendations = None
- self.get_session().add_test_result(test_copy)
+ # Only add/update the test if it is enabled
+ if self._is_test_enabled(test_copy.name, device):
+ self.get_session().add_test_result(test_copy)
# Start the test module
module.start(device)
@@ -637,7 +656,10 @@ def _load_test_module(self, module_dir):
module_conf_file = os.path.join(self._root_path, modules_dir, module_dir,
MODULE_CONFIG)
- module = TestModule(module_conf_file, self.get_session(), extra_hosts)
+ module = TestModule(module_conf_file,
+ self,
+ self.get_session(),
+ extra_hosts)
if module.depends_on is not None:
self._load_test_module(module.depends_on)
self._test_modules.append(module)
@@ -697,5 +719,6 @@ def _set_test_modules_error(self, current_test):
start_idx = current_test if i == self._current_module else 0
for j in range(start_idx, len(self._test_modules_running[i].tests)):
self.get_session().set_test_result_error(
- self._test_modules_running[i].tests[j]
+ self._test_modules_running[i].tests[j],
+ "Test did not run, the device was disconnected"
)
diff --git a/framework/python/src/test_orc/test_pack.py b/framework/python/src/test_orc/test_pack.py
index b1e3d5d3c..a2e7c5f97 100644
--- a/framework/python/src/test_orc/test_pack.py
+++ b/framework/python/src/test_orc/test_pack.py
@@ -27,12 +27,20 @@ class TestPack: # pylint: disable=too-few-public-methods,too-many-instance-attr
tests: List[dict] = field(default_factory=lambda: [])
language: Dict = field(default_factory=lambda: defaultdict(dict))
- def get_required_result(self, test_name: str) -> str:
+ def get_test(self, test_name: str) -> str:
+ """Get details of a test from the test pack"""
for test in self.tests:
if "name" in test and test["name"].lower() == test_name.lower():
- if "required_result" in test:
- return test["required_result"]
+ return test
+
+ def get_required_result(self, test_name: str) -> str:
+ """Fetch the required result of the test"""
+
+ test = self.get_test(test_name)
+
+ if test is not None and "required_result" in test:
+ return test["required_result"]
return "Informational"
@@ -40,3 +48,11 @@ def get_message(self, name: str) -> str:
if name in self.language:
return self.language[name]
return "Message not found"
+
+ def to_dict(self):
+ return {
+ "name": self.name,
+ "description": self.description,
+ "tests": self.tests,
+ "language": self.language
+ }
diff --git a/framework/requirements.txt b/framework/requirements.txt
index fab17b071..6299ef6d4 100644
--- a/framework/requirements.txt
+++ b/framework/requirements.txt
@@ -15,7 +15,7 @@ pydyf==0.8.0
fastapi==0.109.1
psutil==5.9.8
uvicorn==0.27.0
-python-multipart==0.0.9
+python-multipart==0.0.19
pydantic==2.7.1
# Requirements for testing
@@ -28,7 +28,7 @@ responses==0.25.3
markdown==3.5.2
# Requirements for the session
-cryptography==42.0.7
+cryptography==44.0.0
pytz==2024.1
# Requirements for the risk profile
@@ -42,3 +42,4 @@ APScheduler==3.10.4
# Requirements for reports generation
Jinja2==3.1.4
+beautifulsoup4==4.12.3
diff --git a/make/DEBIAN/control b/make/DEBIAN/control
index c822e65fd..d4024828e 100644
--- a/make/DEBIAN/control
+++ b/make/DEBIAN/control
@@ -1,5 +1,5 @@
Package: Testrun
-Version: 2.0.1
+Version: 2.1
Architecture: amd64
Maintainer: Google
Homepage: https://github.com/google/testrun
diff --git a/modules/network/base/bin/start_module b/modules/network/base/bin/start_module
index 3b1c092cd..fb2823afd 100644
--- a/modules/network/base/bin/start_module
+++ b/modules/network/base/bin/start_module
@@ -22,7 +22,7 @@ DEFAULT_IFACE=veth0
# Create a local user that matches the same as the host
# to be used for correct file ownership for various logs
-# HOST_USER mapped in via docker container environemnt variables
+# HOST_USER mapped in via docker container environment variables
useradd $HOST_USER
# Enable IPv6 for all containers
diff --git a/modules/network/base/python/requirements.txt b/modules/network/base/python/requirements.txt
index 9d9473d74..01abf05ce 100644
--- a/modules/network/base/python/requirements.txt
+++ b/modules/network/base/python/requirements.txt
@@ -1,3 +1,10 @@
-grpcio
-grpcio-tools
-netifaces
\ No newline at end of file
+# Dependencies to user defined packages
+# Package dependencies should always be defined before the user defined
+# packages to prevent auto-upgrades of stable dependencies
+protobuf==5.28.3
+
+# User defined packages
+grpcio==1.67.1
+grpcio-tools==1.67.1
+netifaces==0.11.0
+
diff --git a/modules/network/dhcp-1/python/src/grpc_server/dhcp_leases.py b/modules/network/dhcp-1/python/src/grpc_server/dhcp_leases.py
index aa2945759..e2318ac02 100644
--- a/modules/network/dhcp-1/python/src/grpc_server/dhcp_leases.py
+++ b/modules/network/dhcp-1/python/src/grpc_server/dhcp_leases.py
@@ -68,8 +68,8 @@ def get_leases(self):
leases.append(lease)
except Exception as e: # pylint: disable=W0718
# Let non lease lines file without extra checks
- LOGGER.error('Making Lease Error: ' + str(e))
- LOGGER.error('Not a valid lease line: ' + line)
+ LOGGER.info('Not a valid lease line: ' + line)
+ LOGGER.error('Get lease error: ' + str(e))
return leases
def delete_lease(self, ip_addr):
diff --git a/modules/network/dhcp-2/python/src/grpc_server/dhcp_leases.py b/modules/network/dhcp-2/python/src/grpc_server/dhcp_leases.py
index 08e6feabe..f6db83094 100644
--- a/modules/network/dhcp-2/python/src/grpc_server/dhcp_leases.py
+++ b/modules/network/dhcp-2/python/src/grpc_server/dhcp_leases.py
@@ -58,9 +58,9 @@ def get_leases(self):
leases = []
lease_list_raw = self._get_lease_list()
LOGGER.info('Raw Leases:\n' + str(lease_list_raw) + '\n')
- lease_list_start = lease_list_raw.find('=========',0)
- lease_list_start = lease_list_raw.find('\n',lease_list_start)
- lease_list = lease_list_raw[lease_list_start+1:]
+ lease_list_start = lease_list_raw.find('=========', 0)
+ lease_list_start = lease_list_raw.find('\n', lease_list_start)
+ lease_list = lease_list_raw[lease_list_start + 1:]
lines = lease_list.split('\n')
for line in lines:
try:
@@ -68,8 +68,8 @@ def get_leases(self):
leases.append(lease)
except Exception as e: # pylint: disable=W0718
# Let non lease lines file without extra checks
- LOGGER.error('Making Lease Error: ' + str(e))
- LOGGER.error('Not a valid lease line: ' + line)
+ LOGGER.info('Not a valid lease line: ' + line)
+ LOGGER.error('Get lease error: ' + str(e))
return leases
def delete_lease(self, ip_addr):
diff --git a/modules/network/ntp/ntp.Dockerfile b/modules/network/ntp/ntp.Dockerfile
index 1d2a52494..d047770ef 100644
--- a/modules/network/ntp/ntp.Dockerfile
+++ b/modules/network/ntp/ntp.Dockerfile
@@ -21,6 +21,8 @@ ARG MODULE_DIR=modules/network/$MODULE_NAME
# Set DEBIAN_FRONTEND to noninteractive mode
ENV DEBIAN_FRONTEND=noninteractive
+RUN apt-get update
+
# Install all necessary packages
RUN apt-get install -y chrony
diff --git a/modules/network/radius/bin/start_network_service b/modules/network/radius/bin/start_network_service
index d285c20d9..aad840c3a 100644
--- a/modules/network/radius/bin/start_network_service
+++ b/modules/network/radius/bin/start_network_service
@@ -27,7 +27,7 @@ cp $CONF_DIR/ca.crt /etc/ssl/certs/ca-certificates.crt
python3 -u $PYTHON_SRC_DIR/authenticator.py &
-#Create and set permissions on the log file
+# Create and set permissions on the log file
touch $LOG_FILE
chown $HOST_USER $LOG_FILE
diff --git a/modules/network/radius/python/requirements.txt b/modules/network/radius/python/requirements.txt
index 37d126cb1..c814da515 100644
--- a/modules/network/radius/python/requirements.txt
+++ b/modules/network/radius/python/requirements.txt
@@ -1,3 +1,11 @@
-eventlet
-pbr
-transitions
\ No newline at end of file
+# Dependencies to user defined packages
+# Package dependencies should always be defined before the user defined
+# packages to prevent auto-upgrades of stable dependencies
+dnspython==2.6.1
+greenlet==3.0.3
+six==1.16.0
+
+# User defined packages
+eventlet==0.36.1
+pbr==6.1.0
+transitions==0.9.2
diff --git a/modules/test/base/base.Dockerfile b/modules/test/base/base.Dockerfile
index cc78a934c..253270ea9 100644
--- a/modules/test/base/base.Dockerfile
+++ b/modules/test/base/base.Dockerfile
@@ -68,7 +68,7 @@ RUN wget https://standards-oui.ieee.org/oui.txt -O /usr/local/etc/oui.txt || ech
FROM python:3.10-slim
# Install common software
-RUN DEBIAN_FRONTEND=noninteractive apt-get update && apt-get install -yq net-tools iputils-ping tzdata tcpdump iproute2 jq dos2unix nmap wget --fix-missing
+RUN DEBIAN_FRONTEND=noninteractive apt-get update && apt-get install -yq net-tools iputils-ping tzdata tcpdump iproute2 jq dos2unix nmap wget procps --fix-missing
# Get the virtual environment from builder stage
COPY --from=builder /opt/venv /opt/venv
diff --git a/modules/test/base/bin/setup b/modules/test/base/bin/setup
index 5b74790e5..514466548 100644
--- a/modules/test/base/bin/setup
+++ b/modules/test/base/bin/setup
@@ -23,10 +23,20 @@ export BIN_DIR="/testrun/bin"
# Default interface should be veth0 for all containers
export IFACE=veth0
+# Assign the current host user
+export HOST_USER=$(whoami)
+
# Create a local user that matches the same as the host
# to be used for correct file ownership for various logs
-# HOST_USER mapped in via docker container environemnt variables
-useradd $HOST_USER
+# HOST_USER mapped in via docker container environment variables
+if ! id "$HOST_USER" &>/dev/null; then
+ useradd "$HOST_USER"
+else
+ echo User $HOST_USER already exists
+fi
+
+# Create the output directory
+mkdir -p "$OUTPUT_DIR"
# Set permissions on the output files
chown -R $HOST_USER $OUTPUT_DIR
diff --git a/modules/test/base/python/requirements.txt b/modules/test/base/python/requirements.txt
index 9d9473d74..0ed8a792d 100644
--- a/modules/test/base/python/requirements.txt
+++ b/modules/test/base/python/requirements.txt
@@ -1,3 +1,9 @@
-grpcio
-grpcio-tools
-netifaces
\ No newline at end of file
+# Dependencies to user defined packages
+# Package dependencies should always be defined before the user defined
+# packages to prevent auto-upgrades of stable dependencies
+protobuf==5.28.0
+
+# User defined packages
+grpcio==1.67.1
+grpcio-tools==1.67.1
+netifaces==0.11.0
\ No newline at end of file
diff --git a/modules/test/base/python/src/test_module.py b/modules/test/base/python/src/test_module.py
index 1487fb786..21de78143 100644
--- a/modules/test/base/python/src/test_module.py
+++ b/modules/test/base/python/src/test_module.py
@@ -32,7 +32,6 @@ class TestModule:
def __init__(self,
module_name,
log_name,
- log_dir=None,
conf_file=CONF_FILE,
results_dir=RESULTS_DIR):
self._module_name = module_name
@@ -42,19 +41,16 @@ def __init__(self,
self._ipv4_subnet = os.environ.get('IPV4_SUBNET', '')
self._ipv6_subnet = os.environ.get('IPV6_SUBNET', '')
self._dev_iface_mac = os.environ.get('DEV_IFACE_MAC', '')
- self._add_logger(log_name=log_name,
- module_name=module_name,
- log_dir=log_dir)
+ self._device_test_pack = json.loads(os.environ.get('DEVICE_TEST_PACK', ''))
+ self._add_logger(log_name=log_name)
self._config = self._read_config(
conf_file=conf_file if conf_file is not None else CONF_FILE)
self._device_ipv4_addr = None
self._device_ipv6_addr = None
- def _add_logger(self, log_name, module_name, log_dir=None):
+ def _add_logger(self, log_name):
global LOGGER
- LOGGER = logger.get_logger(name=log_name, # pylint: disable=E1123
- log_file=module_name,
- log_dir=log_dir)
+ LOGGER = logger.get_logger(name=log_name)
def generate_module_report(self):
pass
@@ -68,22 +64,48 @@ def _get_tests(self):
def _get_device_tests(self, device_test_module):
module_tests = self._config['config']['tests']
- if device_test_module is None:
- return module_tests
- elif not device_test_module['enabled']:
- return []
- else:
- for test in module_tests:
- # Resolve device specific configurations for the test if it exists
- # and update module test config with device config options
- if 'tests' in device_test_module:
- if test['name'] in device_test_module['tests']:
- dev_test_config = device_test_module['tests'][test['name']]
- if 'enabled' in dev_test_config:
- test['enabled'] = dev_test_config['enabled']
- if 'config' in test and 'config' in dev_test_config:
- test['config'].update(dev_test_config['config'])
- return module_tests
+ tests_to_run = module_tests
+
+ # If no device specific tests have been provided, add all
+ if device_test_module is not None:
+ # Do not run any tests if module is disabled for this device
+ if not device_test_module['enabled']:
+ return []
+
+ # Tests that will be removed because they are not in the test pack
+ remove_tests = []
+
+ # Check if all tests are in the test pack and enabled for the device
+ for test in tests_to_run:
+
+ # Resolve device specific configurations for the test if it exists
+ # and update module test config with device config options
+ if 'tests' in device_test_module:
+
+ if test['name'] in device_test_module['tests']:
+ dev_test_config = device_test_module['tests'][test['name']]
+
+ # Check if the test is enabled in the device config
+ if 'enabled' in dev_test_config:
+ test['enabled'] = dev_test_config['enabled']
+
+ # Copy over any device specific test configuration
+ if 'config' in test and 'config' in dev_test_config:
+ test['config'].update(dev_test_config['config'])
+
+ # Search for the module test in the test pack
+ found = False
+ for test_pack_test in self._device_test_pack['tests']:
+ if test_pack_test['name'] == test['name']:
+ # Test is in the test pack
+ found = True
+
+ if not found:
+ remove_tests.append(test)
+ for test in remove_tests:
+ tests_to_run.remove(test)
+
+ return tests_to_run
def _get_device_test_module(self):
if 'DEVICE_TEST_MODULES' in os.environ:
diff --git a/modules/test/baseline/bin/start_test_module b/modules/test/baseline/bin/start_test_module
index a529c2fcf..c3209261a 100644
--- a/modules/test/baseline/bin/start_test_module
+++ b/modules/test/baseline/bin/start_test_module
@@ -41,11 +41,8 @@ else
fi
# Create and set permissions on the log files
-LOG_FILE=/runtime/output/$MODULE_NAME.log
RESULT_FILE=/runtime/output/$MODULE_NAME-result.json
-touch $LOG_FILE
touch $RESULT_FILE
-chown $HOST_USER $LOG_FILE
chown $HOST_USER $RESULT_FILE
# Run the python scrip that will execute the tests for this module
diff --git a/modules/test/conn/README.md b/modules/test/conn/README.md
index c2f6377c6..10c7e6917 100644
--- a/modules/test/conn/README.md
+++ b/modules/test/conn/README.md
@@ -17,11 +17,15 @@ Within the ```python/src``` directory, the below tests are executed. A few dhcp
| connection.port_link | The network switch port connected to the device has an active link without errors | When the etherent cable is connected to the port, the device triggers the port to its enabled \"Link UP\" (LEDs illuminate on device and switch ports if present) state, and the switch shows no errors with the LEDs and when interrogated with a \"show interface\" command on most network switches. | Required |
| connection.port_speed | The network switch port connected to the device has auto-negotiated a speed that is 10 Mbps or higher | When the ethernet cable is connected to the port, the device autonegotiates a speed that can be checked with the \"show interface\" command on most network switches. The output of this command must also show that the \"configured speed\" is set to \"auto\". | Required |
| connection.port_duplex | The network switch port connected to the device has auto-negotiated full-duplex. | When the ethernet cable is connected to the port, the device autonegotiates a full-duplex connection. | Required |
+| connection.switch.arp_inspection | The device implements ARP correctly as per RFC826 | Device continues to operate correctly when ARP inspection is enabled on the switch. No functionality is lost with ARP inspection enabled. | Required |
+| connection.switch.dhcp_snooping | The device operates as a DHCP client and operates correctly when DHCP snooping is enabled on a switch. | Device continues to operate correctly when DHCP snooping is enabled on the switch. | Required |
| connection.dhcp_address | The device under test has received an IP address from the DHCP server and responds to an ICMP echo (ping) request | The device is not set up with a static IP address. The device accepts an IP address from a DHCP server (RFC 2131) and responds successfully to an ICMP echo (ping) request. | Required |
| connection.mac_address | Check and note device physical address. | N/A | Required |
| connection.mac_oui | The device under test has a MAC address prefix that is registered against a known manufacturer. | The MAC address prefix is registered in the IEEE Organizationally Unique Identifier database. | Required |
| connection.private_address | The device under test accepts an IP address that is compliant with RFC 1918 Address Allocation for Private Internets. | The device under test accepts IP addresses within all ranges specified in RFC 1918 and communicates using these addresses. The Internet Assigned Numbers Authority (IANA) has reserved the following three blocks of the IP address space for private internets: 10.0.0.0 - 10.255.255.255 (10/8 prefix), 172.16.0.0 - 172.31.255.255 (172.16/12 prefix), 192.168.0.0 - 192.168.255.255 (192.168/16 prefix). | Required |
| connection.shared_address | Ensure the device supports RFC 6598 IANA-Reserved IPv4 Prefix for Shared Address Space | The device under test accepts IP addresses within the range specified in RFC 6598 and communicates using these addresses. | Required |
+| connection.dhcp_disconnect | The device under test issues a new DHCPREQUEST packet after a port ph ysical disconnection and reconnection. | A client SHOULD use DHCP to reacquire or verify its IP address and network parameters whenever the local network parameters may have changed; e.g., at system boot time or after a disconnection from the local network, as the local network configuration may change without the client's or user's knowledge. If a client has knowledge ofa previous network address and is unable to contact a local DHCP server, the client may continue to use the previous network addres until the lease for that address expires. If the lease expires before the client can contact a DHCP server, the client must immediately discontinue use of the previous network address and may inform local users of the problem. | Required |
+| connection.dhcp_disconnect_ip_change | When device is disconnected, update device IP on the DHCP server and reconnect the device. Ensure device received new IP address. | If IP address for a device was changed on the DHCP server while the device was disconnected then the device should request and update the new IP upon reconnecting to the network | Required |
| connection.single_ip | The network switch port connected to the device reports only one IP address for the device under test. | The device under test does not behave as a network switch and only requests one IP address. This test is to avoid that devices implement network switches that allow connecting strings of daisy-chained devices to one single network port, as this would not make 802.1x port-based authentication possible. | Required |
| connection.target_ping | The device under test responds to an ICMP echo (ping) request. | The device under test responds to an ICMP echo (ping) request. | Required |
| connection.ipaddr.ip_change | The device responds to a ping (ICMP echo request) to the new IP address it has received after the initial DHCP lease has expired. | If the lease expires before the client receives a DHCPACK, the client moves to the INIT state, MUST immediately stop any other network processing, and requires network initialization parameters as if the client were uninitialized. If the client then receives a DHCPACK allocating the client its previous network address, the client SHOULD continue network processing. If the client is given a new network address, it MUST NOT continue using the previous network address and SHOULD notify the local users of the problem. | Required |
diff --git a/modules/test/conn/bin/get_packet_counts.sh b/modules/test/conn/bin/get_packet_counts.sh
new file mode 100644
index 000000000..fab933a08
--- /dev/null
+++ b/modules/test/conn/bin/get_packet_counts.sh
@@ -0,0 +1,52 @@
+#!/bin/bash
+
+# Check if MAC address and pcap file arguments are provided
+if [ -z "$1" ] || [ -z "$2" ]; then
+ echo "Usage: $0 "
+ exit 1
+fi
+
+# Assign MAC address and pcap file from arguments
+PCAP_FILE="$1"
+MAC_ADDRESS="$2"
+
+# Check if the pcap file exists
+if [ ! -f "$PCAP_FILE" ]; then
+ echo "Error: File $PCAP_FILE does not exist."
+ exit 1
+fi
+
+# Count multicast packets from the MAC address
+multicast_from_count=$(tshark -r "$PCAP_FILE" -Y "(eth.dst[0] == 1) && eth.src == $MAC_ADDRESS" -T fields -e frame.number | wc -l)
+
+# Count multicast packets to the MAC address
+multicast_to_count=$(tshark -r "$PCAP_FILE" -Y "(eth.dst[0] == 1) && eth.dst == $MAC_ADDRESS" -T fields -e frame.number | wc -l)
+
+# Count broadcast packets from the MAC address (broadcast MAC address is FF:FF:FF:FF:FF:FF)
+broadcast_from_count=$(tshark -r "$PCAP_FILE" -Y "eth.dst == ff:ff:ff:ff:ff:ff && eth.src == $MAC_ADDRESS" -T fields -e frame.number | wc -l)
+
+# Count broadcast packets to the MAC address
+broadcast_to_count=$(tshark -r "$PCAP_FILE" -Y "eth.dst == ff:ff:ff:ff:ff:ff && eth.dst == $MAC_ADDRESS" -T fields -e frame.number | wc -l)
+
+# Count unicast packets from the MAC address
+unicast_from_count=$(tshark -r "$PCAP_FILE" -Y "eth.dst != ff:ff:ff:ff:ff:ff && (eth.dst[0] & 1) == 0 && eth.src == $MAC_ADDRESS" -T fields -e frame.number | wc -l)
+
+# Count unicast packets to the MAC address
+unicast_to_count=$(tshark -r "$PCAP_FILE" -Y "eth.dst != ff:ff:ff:ff:ff:ff && (eth.dst[0] & 1) == 0 && eth.dst == $MAC_ADDRESS" -T fields -e frame.number | wc -l)
+
+# Output the results as a JSON object
+echo "{
+ \"mac_address\": \"$MAC_ADDRESS\",
+ \"multicast\": {
+ \"from\": $multicast_from_count,
+ \"to\": $multicast_to_count
+ },
+ \"broadcast\": {
+ \"from\": $broadcast_from_count,
+ \"to\": $broadcast_to_count
+ },
+ \"unicast\": {
+ \"from\": $unicast_from_count,
+ \"to\": $unicast_to_count
+ }
+}"
diff --git a/modules/test/conn/bin/start_test_module b/modules/test/conn/bin/start_test_module
index d85ae7d6b..4cdba0ae9 100644
--- a/modules/test/conn/bin/start_test_module
+++ b/modules/test/conn/bin/start_test_module
@@ -40,9 +40,7 @@ fi
# Create and set permissions on the log files
LOG_FILE=/runtime/output/$MODULE_NAME.log
RESULT_FILE=/runtime/output/$MODULE_NAME-result.json
-touch $LOG_FILE
touch $RESULT_FILE
-chown $HOST_USER $LOG_FILE
chown $HOST_USER $RESULT_FILE
# Run the python script that will execute the tests for this module
diff --git a/modules/test/conn/conf/module_config.json b/modules/test/conn/conf/module_config.json
index ca386a5e4..03ae51d89 100644
--- a/modules/test/conn/conf/module_config.json
+++ b/modules/test/conn/conf/module_config.json
@@ -16,7 +16,7 @@
{
"name": "connection.port_link",
"test_description": "The network switch port connected to the device has an active link without errors",
- "expected_behavior": "When the etherent cable is connected to the port, the device triggers the port to its enabled \"Link UP\" (LEDs illuminate on device and switch ports if present) state, and the switch shows no errors with the LEDs and when interrogated with a \"show interface\" command on most network switches."
+ "expected_behavior": "When the ethernet cable is connected to the port, the device triggers the port to its enabled \"Link UP\" (LEDs illuminate on device and switch ports if present) state, and the switch shows no errors with the LEDs and when interrogated with a \"show interface\" command on most network switches."
},
{
"name": "connection.port_speed",
@@ -41,10 +41,11 @@
{
"name": "connection.dhcp_address",
"test_description": "The device under test has received an IP address from the DHCP server and responds to an ICMP echo (ping) request",
- "expected_behavior": "The device is not setup with a static IP address. The device accepts an IP address from a DHCP server (RFC 2131) and responds succesfully to an ICMP echo (ping) request.",
+ "expected_behavior": "The device is not setup with a static IP address. The device accepts an IP address from a DHCP server (RFC 2131) and responds successfully to an ICMP echo (ping) request.",
"recommendations": [
"Enable DHCP",
- "Install a DHCP client"
+ "Install a DHCP client",
+ "Ensure that your DHCP client renews its lease at the correct time"
]
},
{
@@ -108,19 +109,18 @@
},
{
"name": "connection.dhcp_disconnect",
- "test_description": "The device under test issues a new DHCPREQUEST packet after a port ph ysical disconnection and reconnection",
- "expected_behavior": "A client SHOULD use DHCP to reacquire or verify its IP address and network parameters whenever the local network parameters may have changed; e.g., at system boot time or after a disconnection from the local network, as the local network configuration may change without the client's or user's knowledge. If a client has knowledge ofa previous network address and is unable to contact a local DHCP server, the client may continue to use the previous network addres until the lease for that address expires. If the lease expires before the client can contact a DHCP server, the client must immediately discontinue use of the previous network address and may inform local users of the problem."
+ "test_description": "The device under test issues a new DHCPREQUEST packet after a port physical disconnection and reconnection",
+ "expected_behavior": "A client SHOULD use DHCP to reacquire or verify its IP address and network parameters whenever the local network parameters may have changed; e.g., at system boot time or after a disconnection from the local network, as the local network configuration may change without the client's or user's knowledge. If a client has knowledge ofa previous network address and is unable to contact a local DHCP server, the client may continue to use the previous network address until the lease for that address expires. If the lease expires before the client can contact a DHCP server, the client must immediately discontinue use of the previous network address and may inform local users of the problem."
},
{
"name": "connection.dhcp_disconnect_ip_change",
"test_description": "When device is disconnected, update device IP on the DHCP server and reconnect the device. Ensure device received new IP address",
- "expected_behavior": "If IP address for a device was changed on the DHCP server while the device was disconnected then the device shoiuld request and update the new IP upon reconnecting to the network",
- "required_result": "Required"
+ "expected_behavior": "If IP address for a device was changed on the DHCP server while the device was disconnected then the device should request and update the new IP upon reconnecting to the network"
},
{
"name": "connection.single_ip",
"test_description": "The network switch port connected to the device reports only one IP address for the device under test.",
- "expected_behavior": "The device under test does not behave as a network switch and only requets one IP address. This test is to avoid that devices implement network switches that allow connecting strings of daisy chained devices to one single network port, as this would not make 802.1x port based authentication possible.",
+ "expected_behavior": "The device under test does not behave as a network switch and only requests one IP address. This test is to avoid that devices implement network switches that allow connecting strings of daisy chained devices to one single network port, as this would not make 802.1x port based authentication possible.",
"recommendations": [
"Ensure that all ports on the device are isolated",
"Ensure only one DHCP client is running"
@@ -138,7 +138,7 @@
{
"name": "connection.ipaddr.ip_change",
"test_description": "The device responds to a ping (ICMP echo request) to the new IP address it has received after the initial DHCP lease has expired.",
- "expected_behavior": "If the lease expires before the client receiveds a DHCPACK, the client moves to INIT state, MUST immediately stop any other network processing and requires network initialization parameters as if the client were uninitialized. If the client then receives a DHCPACK allocating the client its previous network addres, the client SHOULD continue network processing. If the client is given a new network address, it MUST NOT continue using the previous network address and SHOULD notify the local users of the problem.",
+ "expected_behavior": "If the lease expires before the client receives a DHCPACK, the client moves to INIT state, MUST immediately stop any other network processing and requires network initialization parameters as if the client were uninitialized. If the client then receives a DHCPACK allocating the client its previous network address, the client SHOULD continue network processing. If the client is given a new network address, it MUST NOT continue using the previous network address and SHOULD notify the local users of the problem.",
"config":{
"lease_wait_time_sec": 60
},
@@ -174,6 +174,11 @@
"Enable ping response to IPv6 ICMP requests in network manager settings",
"Create a firewall exception to allow ICMPv6 via LAN"
]
+ },
+ {
+ "name": "communication.network.type",
+ "test_description": "How does the device communicate (flow type) - Unicast, multicast broadcast?",
+ "expected_behavior": "Informational - One or more of these flow types are used"
}
]
}
diff --git a/modules/test/conn/conn.Dockerfile b/modules/test/conn/conn.Dockerfile
index 58aec8048..cda0858c9 100644
--- a/modules/test/conn/conn.Dockerfile
+++ b/modules/test/conn/conn.Dockerfile
@@ -21,7 +21,7 @@ ARG GRPC_PROTO_DIR=/testrun/python/src/grpc/proto/dhcp
ARG GRPC_PROTO_FILE="grpc.proto"
# Install all necessary packages
-RUN apt-get install -y wget
+RUN apt-get install -y wget tshark
# Load the requirements file
COPY $MODULE_DIR/python/requirements.txt /testrun/python
@@ -35,5 +35,11 @@ COPY $MODULE_DIR/conf /testrun/conf
# Copy over all binary files
COPY $MODULE_DIR/bin /testrun/bin
+# Remove incorrect line endings
+RUN dos2unix /testrun/bin/*
+
+# Make sure all the bin files are executable
+RUN chmod u+x /testrun/bin/*
+
# Copy over all python files
COPY $MODULE_DIR/python /testrun/python
\ No newline at end of file
diff --git a/modules/test/conn/python/requirements.txt b/modules/test/conn/python/requirements.txt
index 7244e9e75..4075f79c9 100644
--- a/modules/test/conn/python/requirements.txt
+++ b/modules/test/conn/python/requirements.txt
@@ -1,3 +1,12 @@
-pyOpenSSL
-scapy
-python-dateutil
\ No newline at end of file
+# Dependencies to user defined packages
+# Package dependencies should always be defined before the user defined
+# packages to prevent auto-upgrades of stable dependencies
+cffi==1.17.1
+cryptography==43.0.3
+pycparser==2.22
+six==1.16.0
+
+# User defined packages
+pyOpenSSL==24.3.0
+scapy==2.6.0
+python-dateutil==2.9.0.post0
diff --git a/modules/test/conn/python/src/connection_module.py b/modules/test/conn/python/src/connection_module.py
index 867d8a3ff..fdb1ae5cc 100644
--- a/modules/test/conn/python/src/connection_module.py
+++ b/modules/test/conn/python/src/connection_module.py
@@ -16,6 +16,7 @@
import time
import traceback
import os
+from scapy.error import Scapy_Exception
from scapy.all import rdpcap, DHCP, ARP, Ether, ICMP, IPv6, ICMPv6ND_NS
from test_module import TestModule
from dhcp1.client import Client as DHCPClient1
@@ -23,9 +24,11 @@
from host.client import Client as HostClient
from dhcp_util import DHCPUtil
from port_stats_util import PortStatsUtil
+import json
LOG_NAME = 'test_connection'
OUI_FILE = '/usr/local/etc/oui.txt'
+DEFAULT_BIN_DIR = '/testrun/bin'
STARTUP_CAPTURE_FILE = '/runtime/device/startup.pcap'
MONITOR_CAPTURE_FILE = '/runtime/device/monitor.pcap'
DHCP_CAPTURE_FILE = '/runtime/network/dhcp-1.pcap'
@@ -43,15 +46,14 @@ class ConnectionModule(TestModule):
def __init__(self,
module,
- log_dir=None,
conf_file=None,
results_dir=None,
startup_capture_file=STARTUP_CAPTURE_FILE,
- monitor_capture_file=MONITOR_CAPTURE_FILE):
+ monitor_capture_file=MONITOR_CAPTURE_FILE,
+ bin_dir=DEFAULT_BIN_DIR):
super().__init__(module_name=module,
log_name=LOG_NAME,
- log_dir=log_dir,
conf_file=conf_file,
results_dir=results_dir)
global LOGGER
@@ -64,6 +66,7 @@ def __init__(self,
self.host_client = HostClient()
self._dhcp_util = DHCPUtil(self.dhcp1_client, self.dhcp2_client, LOGGER)
self._lease_wait_time_sec = LEASE_WAIT_TIME_DEFAULT
+ self._bin_dir = bin_dir
# ToDo: Move this into some level of testing, leave for
# reference until tests are implemented with these calls
@@ -146,7 +149,7 @@ def _connection_switch_arp_inspection(self):
if no_arp:
return None, 'No ARP packets from the device found'
- return True, 'Device uses ARP'
+ return True, 'Device uses ARP correctly'
def _connection_switch_dhcp_snooping(self):
LOGGER.info('Running connection.switch.dhcp_snooping')
@@ -197,15 +200,17 @@ def _connection_dhcp_address(self):
ping_success = self._ping(self._device_ipv4_addr)
LOGGER.debug('Ping success: ' + str(ping_success))
if ping_success:
- return True, 'Device responded to leased ip address'
+ return True, 'Device responded to leased IP address'
else:
- return False, 'Device did not respond to leased ip address'
+ return False, 'Device did not respond to leased IP address'
else:
LOGGER.info('No IP information found in lease: ' + self._device_mac)
return False, 'No IP information found in lease: ' + self._device_mac
else:
- LOGGER.info('No DHCP lease could be found: ' + self._device_mac)
- return False, 'No DHCP lease could be found: ' + self._device_mac
+ LOGGER.info('No DHCP lease could be found for MAC ' + self._device_mac +
+ ' at the time of this test')
+ return (False, 'No DHCP lease could be found for MAC ' +
+ self._device_mac + ' at the time of this test')
def _connection_mac_address(self):
LOGGER.info('Running connection.mac_address')
@@ -306,7 +311,7 @@ def _connection_ipaddr_ip_change(self, config):
lease['hw_addr'], ip_address):
self._dhcp_util.wait_for_lease_expire(lease,
self._lease_wait_time_sec)
- LOGGER.info('Checking device accepted new ip')
+ LOGGER.info('Checking device accepted new IP')
for _ in range(5):
LOGGER.info('Pinging device at IP: ' + ip_address)
if self._ping(ip_address):
@@ -323,8 +328,10 @@ def _connection_ipaddr_ip_change(self, config):
else:
result = None, 'Failed to create reserved lease for device'
else:
- LOGGER.info('Device has no current DHCP lease')
- result = None, 'Device has no current DHCP lease'
+ LOGGER.info('Device has no current DHCP lease so ' +
+ 'this test could not be run')
+ result = None, ('Device has no current DHCP lease so ' +
+ 'this test could not be run')
# Restore the network
self._dhcp_util.restore_failover_dhcp_server()
LOGGER.info('Waiting 30 seconds for reserved lease to expire')
@@ -377,7 +384,9 @@ def _connection_ipaddr_dhcp_failover(self, config):
else:
result = False, 'Device did not respond to ping'
else:
- result = None, 'Device has no current DHCP lease'
+ result = (
+ None,
+ 'Device has no current DHCP lease so this test could not be run')
else:
LOGGER.error('Network is not ready for this test. Skipping')
result = None, 'Network is not ready for this test'
@@ -388,66 +397,75 @@ def _connection_dhcp_disconnect(self):
result = None
description = ''
dev_iface = os.getenv('DEV_IFACE')
- iface_status = self.host_client.check_interface_status(dev_iface)
- if iface_status.code == 200:
- LOGGER.info('Successfully resolved iface status')
- if iface_status.status:
- lease = self._dhcp_util.get_cur_lease(mac_address=self._device_mac,
- timeout=self._lease_wait_time_sec)
- if lease is not None:
- LOGGER.info('Current device lease resolved')
- if self._dhcp_util.is_lease_active(lease):
- # Disable the device interface
- iface_down = self.host_client.set_iface_down(dev_iface)
- if iface_down:
- LOGGER.info('Device interface set to down state')
+ try:
+ iface_status = self.host_client.check_interface_status(dev_iface)
+ if iface_status.code == 200:
+ LOGGER.info('Successfully resolved iface status')
+ if iface_status.status:
+ lease = self._dhcp_util.get_cur_lease(mac_address=self._device_mac,
+ timeout=self._lease_wait_time_sec)
+ if lease is not None:
+ LOGGER.info('Current device lease resolved')
+ if self._dhcp_util.is_lease_active(lease):
+
+ # Disable the device interface
+ iface_down = self.host_client.set_iface_down(dev_iface)
+ if iface_down:
+ LOGGER.info('Device interface set to down state')
- # Wait for the lease to expire
- self._dhcp_util.wait_for_lease_expire(lease,
- self._lease_wait_time_sec)
+ # Wait for the lease to expire
+ self._dhcp_util.wait_for_lease_expire(lease,
+ self._lease_wait_time_sec)
- # Wait an additonal 10 seconds to better test a true disconnect
- # state
- LOGGER.info('Waiting 10 seconds before bringing iface back up')
- time.sleep(10)
-
- # Enable the device interface
- iface_up = self.host_client.set_iface_up(dev_iface)
- if iface_up:
- LOGGER.info('Device interface set to up state')
-
- # Confirm device receives a new lease
- if self._dhcp_util.get_cur_lease(
- mac_address=self._device_mac,
- timeout=self._lease_wait_time_sec):
- if self._dhcp_util.is_lease_active(lease):
- result = True
- description = (
- 'Device received a DHCP lease after disconnect')
+ # Wait an additonal 10 seconds to better test a true disconnect
+ # state
+ LOGGER.info('Waiting 10 seconds before bringing iface back up')
+ time.sleep(10)
+
+ # Enable the device interface
+ iface_up = self.host_client.set_iface_up(dev_iface)
+ if iface_up:
+ LOGGER.info('Device interface set to up state')
+
+ # Confirm device receives a new lease
+ if self._dhcp_util.get_cur_lease(
+ mac_address=self._device_mac,
+ timeout=self._lease_wait_time_sec):
+ if self._dhcp_util.is_lease_active(lease):
+ result = True
+ description = (
+ 'Device received a DHCP lease after disconnect')
+ else:
+ result = False
+ description = (
+ 'Could not confirm DHCP lease active after disconnect')
else:
result = False
description = (
- 'Could not confirm DHCP lease active after disconnect')
- else:
- result = False
- description = (
'Device did not recieve a DHCP lease after disconnect')
+ else:
+ result = 'Error'
+ description = 'Failed to set device interface to up state'
else:
result = 'Error'
- description = 'Failed to set device interface to up state'
- else:
- result = 'Error'
- description = 'Failed to set device interface to down state'
+ description = 'Failed to set device interface to down state'
+ else:
+ result = 'Error'
+ description = 'No active lease available for device'
else:
result = 'Error'
- description = 'No active lease available for device'
+ description = 'Device interface is down'
else:
result = 'Error'
- description = 'Device interface is down'
- else:
+ description = 'Device interface could not be resolved'
+
+ except Exception:
+ LOGGER.error('Unable to connect to gRPC server')
result = 'Error'
- description = 'Device interface could not be resolved'
+ description = (
+ 'Unable to connect to gRPC server'
+ )
return result, description
def _connection_dhcp_disconnect_ip_change(self):
@@ -457,86 +475,96 @@ def _connection_dhcp_disconnect_ip_change(self):
reserved_lease = None
dev_iface = os.getenv('DEV_IFACE')
if self._dhcp_util.setup_single_dhcp_server():
- iface_status = self.host_client.check_interface_status(dev_iface)
- if iface_status.code == 200:
- LOGGER.info('Successfully resolved iface status')
- if iface_status.status:
- lease = self._dhcp_util.get_cur_lease(
- mac_address=self._device_mac, timeout=self._lease_wait_time_sec)
- if lease is not None:
- LOGGER.info('Current device lease resolved')
- if self._dhcp_util.is_lease_active(lease):
+ try:
+ iface_status = self.host_client.check_interface_status(dev_iface)
+ if iface_status.code == 200:
+ LOGGER.info('Successfully resolved iface status')
+ if iface_status.status:
+ lease = self._dhcp_util.get_cur_lease(
+ mac_address=self._device_mac, timeout=self._lease_wait_time_sec)
+ if lease is not None:
+ LOGGER.info('Current device lease resolved')
+ if self._dhcp_util.is_lease_active(lease):
- # Add a reserved lease with a different IP
- ip_address = '10.10.10.30'
- reserved_lease = self._dhcp_util.add_reserved_lease(
- lease['hostname'], self._device_mac, ip_address)
+ # Add a reserved lease with a different IP
+ ip_address = '10.10.10.30'
+ reserved_lease = self._dhcp_util.add_reserved_lease(
+ lease['hostname'], self._device_mac, ip_address)
- # Disable the device interface
- iface_down = self.host_client.set_iface_down(dev_iface)
- if iface_down:
- LOGGER.info('Device interface set to down state')
+ # Disable the device interface
+ iface_down = self.host_client.set_iface_down(dev_iface)
+ if iface_down:
+ LOGGER.info('Device interface set to down state')
- # Wait for the lease to expire
- self._dhcp_util.wait_for_lease_expire(lease,
- self._lease_wait_time_sec)
+ # Wait for the lease to expire
+ self._dhcp_util.wait_for_lease_expire(lease,
+ self._lease_wait_time_sec)
- if reserved_lease:
- # Wait an additonal 10 seconds to better test a true
- # disconnect state
- LOGGER.info(
- 'Waiting 10 seconds before bringing iface back up')
- time.sleep(10)
-
- # Enable the device interface
- iface_up = self.host_client.set_iface_up(dev_iface)
- if iface_up:
- LOGGER.info('Device interface set to up state')
- # Confirm device receives a new lease
- reserved_lease_accepted = False
- LOGGER.info('Checking device accepted new ip')
- for _ in range(5):
- LOGGER.info('Pinging device at IP: ' + ip_address)
- if self._ping(ip_address):
- LOGGER.debug('Ping success')
- LOGGER.debug(
- 'Reserved lease confirmed active in device')
- reserved_lease_accepted = True
- break
+ if reserved_lease:
+ # Wait an additonal 10 seconds to better test a true
+ # disconnect state
+ LOGGER.info(
+ 'Waiting 10 seconds before bringing iface back up')
+ time.sleep(10)
+
+ # Enable the device interface
+ iface_up = self.host_client.set_iface_up(dev_iface)
+ if iface_up:
+ LOGGER.info('Device interface set to up state')
+ # Confirm device receives a new lease
+ reserved_lease_accepted = False
+ LOGGER.info('Checking device accepted new IP')
+ for _ in range(5):
+ LOGGER.info('Pinging device at IP: ' + ip_address)
+ if self._ping(ip_address):
+ LOGGER.debug('Ping success')
+ LOGGER.debug(
+ 'Reserved lease confirmed active in device')
+ reserved_lease_accepted = True
+ break
+ else:
+ LOGGER.info('Device did not respond to ping')
+ time.sleep(5) # Wait 5 seconds before trying again
+
+ if reserved_lease_accepted:
+ result = True
+ description = ('Device received expected IP address '
+ 'after disconnect')
else:
- LOGGER.info('Device did not respond to ping')
- time.sleep(5) # Wait 5 seconds before trying again
-
- if reserved_lease_accepted:
- result = True
- description = ('Device received expected IP address '
- 'after disconnect')
- else:
- result = False
- description = (
+ result = False
+ description = (
'Could not confirm DHCP lease active after disconnect'
- )
+ )
+ else:
+ result = 'Error'
+ description = 'Failed to set device interface to up state'
else:
result = 'Error'
- description = 'Failed to set device interface to up state'
+ description = (
+ 'Failed to set reserved address in DHCP server'
+ )
else:
result = 'Error'
- description = 'Failed to set reserved address in DHCP server'
- else:
- result = 'Error'
- description = 'Failed to set device interface to down state'
+ description = 'Failed to set device interface to down state'
+ else:
+ result = 'Error'
+ description = 'No active lease available for device'
else:
result = 'Error'
- description = 'No active lease available for device'
+ description = 'Device interface is down'
else:
result = 'Error'
- description = 'Device interface is down'
- else:
+ description = 'Device interface could not be resolved'
+ except Exception:
+ LOGGER.error('Unable to connect to gRPC server')
result = 'Error'
- description = 'Device interface could not be resolved'
+ description = (
+ 'Unable to connect to gRPC server'
+ )
else:
result = 'Error'
description = 'Failed to configure network for test'
+
if reserved_lease:
self._dhcp_util.delete_reserved_lease(self._device_mac)
@@ -563,10 +591,12 @@ def _connection_ipv6_slaac(self):
LOGGER.info('Running connection.ipv6_slaac')
result = None
- slac_test, sends_ipv6 = self._has_slaac_addres()
+ slac_test, sends_ipv6 = self._has_slaac_address()
if slac_test:
result = True, f'Device has formed SLAAC address {self._device_ipv6_addr}'
- if result is None:
+ elif slac_test is None:
+ result = 'Error', 'An error occurred whilst running this test'
+ else:
if sends_ipv6:
LOGGER.info('Device does not support IPv6 SLAAC')
result = False, 'Device does not support IPv6 SLAAC'
@@ -575,10 +605,15 @@ def _connection_ipv6_slaac(self):
result = False, 'Device does not support IPv6'
return result
- def _has_slaac_addres(self):
+ def _has_slaac_address(self):
packet_capture = (rdpcap(self.startup_capture_file) +
- rdpcap(self.monitor_capture_file) +
- rdpcap(DHCP_CAPTURE_FILE))
+ rdpcap(self.monitor_capture_file))
+
+ try:
+ packet_capture += rdpcap(DHCP_CAPTURE_FILE)
+ except (FileNotFoundError, Scapy_Exception):
+ LOGGER.error('dhcp-1.pcap not found or empty, ignoring')
+
sends_ipv6 = False
for packet_number, packet in enumerate(packet_capture, start=1):
if IPv6 in packet and packet.src == self._device_mac:
@@ -661,6 +696,67 @@ def setup_single_dhcp_server(self):
else:
return False, 'Secondary DHCP server stop command failed'
+ def _communication_network_type(self):
+ try:
+ result = 'Informational'
+ description = ''
+ details = ''
+ packets = self.get_network_packet_types()
+ details = packets
+ # Initialize a list for detected packet types
+ packet_types = []
+
+ # Check for the presence of each packet type and append to the list
+ if (packets['multicast']['from'] > 0) or (packets['multicast']['to'] > 0):
+ packet_types.append('Multicast')
+ if (packets['broadcast']['from'] > 0) or (packets['broadcast']['to'] > 0):
+ packet_types.append('Broadcast')
+ if (packets['unicast']['from'] > 0) or (packets['unicast']['to'] > 0):
+ packet_types.append('Unicast')
+
+ # Construct the description if any packet types were detected
+ if packet_types:
+ description = 'Packet types detected: ' + ', '.join(packet_types)
+ else:
+ description = 'No multicast, broadcast or unicast detected'
+
+ except Exception as e: # pylint: disable=W0718
+ LOGGER.error(e)
+ result = 'Error'
+ return result, description, details
+
+ def get_network_packet_types(self):
+ combined_results = {
+ 'mac_address': self._device_mac,
+ 'multicast': {
+ 'from': 0,
+ 'to': 0
+ },
+ 'broadcast': {
+ 'from': 0,
+ 'to': 0
+ },
+ 'unicast': {
+ 'from': 0,
+ 'to': 0
+ },
+ }
+ capture_files = [self.startup_capture_file, self.monitor_capture_file]
+ for capture_file in capture_files:
+ bin_file = self._bin_dir + '/get_packet_counts.sh'
+ args = f'"{capture_file}" "{self._device_mac}"'
+ command = f'{bin_file} {args}'
+ response = util.run_command(command)
+ packets = json.loads(response[0].strip())
+ # Combine results
+ combined_results['multicast']['from'] += packets['multicast']['from']
+ combined_results['multicast']['to'] += packets['multicast']['to']
+ combined_results['broadcast']['from'] += packets['broadcast']['from']
+ combined_results['broadcast']['to'] += packets['broadcast']['to']
+ combined_results['unicast']['from'] += packets['unicast']['from']
+ combined_results['unicast']['to'] += packets['unicast']['to']
+ return combined_results
+
def enable_failover(self):
# Move primary DHCP server to primary failover
LOGGER.info('Configuring primary failover DHCP server')
@@ -683,6 +779,7 @@ def is_ip_in_range(self, ip, start_ip, end_ip):
return start_int <= ip_int <= end_int
def _run_subnet_test(self, config):
+
# Resolve the configured dhcp subnet ranges
ranges = None
if 'ranges' in config:
@@ -697,6 +794,7 @@ def _run_subnet_test(self, config):
response = self.dhcp1_client.get_dhcp_range()
cur_range = {}
+
if response.code == 200:
cur_range['start'] = response.start
cur_range['end'] = response.end
@@ -709,16 +807,21 @@ def _run_subnet_test(self, config):
results = []
dhcp_setup = self.setup_single_dhcp_server()
+
if dhcp_setup[0]:
LOGGER.info(dhcp_setup[1])
lease = self._dhcp_util.get_cur_lease(mac_address=self._device_mac,
timeout=self._lease_wait_time_sec)
+
if lease is not None:
if self._dhcp_util.is_lease_active(lease):
results = self.test_subnets(ranges)
else:
- LOGGER.info('Failed to confirm a valid active lease for the device')
- return None, 'Failed to confirm a valid active lease for the device'
+ LOGGER.info('Device has no current DHCP lease ' +
+ 'so this test could not be run')
+ return (
+ None,
+ 'Device has no current DHCP lease so this test could not be run')
else:
LOGGER.error(dhcp_setup[1])
return None, 'Failed to setup DHCP server for test'
@@ -732,7 +835,7 @@ def _run_subnet_test(self, config):
else:
if result['result'] is not None:
final_result &= result['result']
- if result['result']:
+ if not result['result']:
final_result_details += result['details'] + '\n'
if final_result:
@@ -745,10 +848,17 @@ def _run_subnet_test(self, config):
# Wait for the current lease to expire
lease = self._dhcp_util.get_cur_lease(mac_address=self._device_mac,
timeout=self._lease_wait_time_sec)
- self._dhcp_util.wait_for_lease_expire(lease, self._lease_wait_time_sec)
+
+ # Check if lease is active
+ if lease is not None:
+ self._dhcp_util.wait_for_lease_expire(lease, self._lease_wait_time_sec)
+ else:
+ # If not, wait for 30 seconds as a fallback
+ time.sleep(30)
# Wait for a new lease to be provided before exiting test
# to prevent other test modules from failing
+
LOGGER.info('Checking for new lease')
# Subnet changes tend to take longer to pick up so we'll allow
# for twice the lease wait time
@@ -763,9 +873,8 @@ def _run_subnet_test(self, config):
else:
LOGGER.info('New lease not found. Waiting to check again')
- except Exception as e: # pylint: disable=W0718
- LOGGER.error('Failed to restore DHCP server configuration: ' + str(e))
- LOGGER.error(traceback.format_exc())
+ except Exception: # pylint: disable=W0718
+ LOGGER.error('Failed to restore DHCP server configuration')
return final_result, final_result_details
diff --git a/modules/test/conn/python/src/dhcp_util.py b/modules/test/conn/python/src/dhcp_util.py
index 3654d0401..22880dab0 100644
--- a/modules/test/conn/python/src/dhcp_util.py
+++ b/modules/test/conn/python/src/dhcp_util.py
@@ -256,21 +256,30 @@ def setup_single_dhcp_server(self):
return False
def wait_for_lease_expire(self, lease, max_wait_time=30):
- expiration_utc = datetime.strptime(lease['expires'], '%Y-%m-%d %H:%M:%S')
- # lease information stored in UTC so we need to convert to local time
- expiration = self.utc_to_local(expiration_utc)
- time_to_expire = expiration - datetime.now(tz=tz.tzlocal())
- # Wait until the expiration time and padd 5 seconds
- # If wait time is longer than max_wait_time, only wait
- # for the max wait time
- wait_time = min(max_wait_time,
- time_to_expire.total_seconds() +
- 5) if time_to_expire.total_seconds() > 0 else 0
- LOGGER.info('Time until lease expiration: ' + str(wait_time))
- LOGGER.info('Waiting for current lease to expire: ' + str(expiration))
- if wait_time > 0:
- time.sleep(wait_time)
- LOGGER.info('Current lease expired.')
+
+ try:
+ expiration_utc = datetime.strptime(lease['expires'], '%Y-%m-%d %H:%M:%S')
+
+ # Lease information stored in UTC so we need to convert to local time
+ expiration = self.utc_to_local(expiration_utc)
+ time_to_expire = expiration - datetime.now(tz=tz.tzlocal())
+
+ # Wait until the expiration time and padd 5 seconds
+ # If wait time is longer than max_wait_time, only wait
+ # for the max wait time
+ wait_time = min(max_wait_time,
+ time_to_expire.total_seconds() +
+ 5) if time_to_expire.total_seconds() > 0 else 0
+
+ LOGGER.info('Time until lease expiration: ' + str(wait_time))
+ LOGGER.info('Waiting for current lease to expire: ' + str(expiration))
+
+ if wait_time > 0:
+ time.sleep(wait_time)
+ LOGGER.info('Current lease expired')
+
+ except TypeError:
+ LOGGER.error('Device does not have an active lease')
# Convert from a UTC datetime to the local time zone
def utc_to_local(self, utc_datetime):
diff --git a/modules/test/conn/python/src/port_stats_util.py b/modules/test/conn/python/src/port_stats_util.py
index d923501eb..a1f68cb03 100644
--- a/modules/test/conn/python/src/port_stats_util.py
+++ b/modules/test/conn/python/src/port_stats_util.py
@@ -41,7 +41,7 @@ def __init__(self,
self.conn_stats = self._read_stats_file(self.ethtool_conn_stats_file)
def is_autonegotiate(self):
- auto_negotiation = False
+ auto_negotiation = None
auto_negotiation_status = self._get_stat_option(stats=self.conn_stats,
option='Auto-negotiation:')
if auto_negotiation_status is not None:
@@ -66,15 +66,22 @@ def connection_port_link_test(self):
option='rx_errors:')
rx_errors_post = self._get_stat_option(stats=stats_post,
option='rx_errors:')
- tx_errors = int(tx_errors_post) - int(tx_errors_pre)
- rx_errors = int(rx_errors_post) - int(rx_errors_pre)
- if tx_errors > 0 or rx_errors > 0:
- result = False
- description = 'Port errors detected'
- details = f'TX errors: {tx_errors}, RX errors: {rx_errors}'
+
+ # Check that the above have been resolved correctly
+ if (tx_errors_pre is None or tx_errors_post is None or
+ rx_errors_pre is None or rx_errors_post is None):
+ result = 'Error'
+ description = 'Port stats not available'
else:
- result = True
- description = 'No port errors detected'
+ tx_errors = int(tx_errors_post) - int(tx_errors_pre)
+ rx_errors = int(rx_errors_post) - int(rx_errors_pre)
+ if tx_errors > 0 or rx_errors > 0:
+ result = False
+ description = 'Port errors detected'
+ details = f'TX errors: {tx_errors}, RX errors: {rx_errors}'
+ else:
+ result = True
+ description = 'No port errors detected'
return result, description, details
def connection_port_duplex_test(self):
@@ -83,7 +90,10 @@ def connection_port_duplex_test(self):
result = None
description = ''
details = ''
- if not auto_negotiation:
+ if auto_negotiation is None:
+ result = 'Error'
+ description = 'Port stats not available'
+ elif not auto_negotiation:
result = False
description = 'Interface not configured for auto-negotiation'
else:
@@ -104,7 +114,10 @@ def connection_port_speed_test(self):
result = None
description = ''
details = ''
- if not auto_negotiation:
+ if auto_negotiation is None:
+ result = 'Error'
+ description = 'Port stats not available'
+ elif not auto_negotiation:
result = False
description = 'Interface not configured for auto-negotiation'
else:
diff --git a/modules/test/dns/README.md b/modules/test/dns/README.md
index 79bce57f7..c2a917b13 100644
--- a/modules/test/dns/README.md
+++ b/modules/test/dns/README.md
@@ -15,5 +15,5 @@ Within the ```python/src``` directory, the below tests are executed.
| ID | Description | Expected behavior | Required result
|---|---|---|---|
| dns.network.hostname_resolution | Verifies that the device resolves hostnames | The device sends DNS requests | Required |
-| dns.network.from_dhcp | Verifies that the device allows for a DNS server to be provided by the DHCP server | The device sends DNS requests to the DNS server provided by the DHCP server | Roadmap |
+| dns.network.from_dhcp | Verifies that the device allows for a DNS server to be provided by the DHCP server | The device sends DNS requests to the DNS server provided by the DHCP server | Informational |
| dns.mdns | Does the device has MDNS (or any kind of IP multicast) | Device may send MDNS requests | Informational |
\ No newline at end of file
diff --git a/modules/test/dns/bin/start_test_module b/modules/test/dns/bin/start_test_module
index a529c2fcf..c3209261a 100644
--- a/modules/test/dns/bin/start_test_module
+++ b/modules/test/dns/bin/start_test_module
@@ -41,11 +41,8 @@ else
fi
# Create and set permissions on the log files
-LOG_FILE=/runtime/output/$MODULE_NAME.log
RESULT_FILE=/runtime/output/$MODULE_NAME-result.json
-touch $LOG_FILE
touch $RESULT_FILE
-chown $HOST_USER $LOG_FILE
chown $HOST_USER $RESULT_FILE
# Run the python scrip that will execute the tests for this module
diff --git a/modules/test/dns/conf/module_config.json b/modules/test/dns/conf/module_config.json
index d3d0a1134..662273cd7 100644
--- a/modules/test/dns/conf/module_config.json
+++ b/modules/test/dns/conf/module_config.json
@@ -32,7 +32,7 @@
},
{
"name": "dns.mdns",
- "test_description": "Does the device has MDNS (or any kind of IP multicast)",
+ "test_description": "Does the device have MDNS (or any kind of IP multicast)",
"expected_behavior": "Device may send MDNS requests"
}
]
diff --git a/modules/test/dns/python/requirements.txt b/modules/test/dns/python/requirements.txt
index 93b351f44..f61132516 100644
--- a/modules/test/dns/python/requirements.txt
+++ b/modules/test/dns/python/requirements.txt
@@ -1 +1,6 @@
-scapy
\ No newline at end of file
+# Dependencies to user defined packages
+# Package dependencies should always be defined before the user defined
+# packages to prevent auto-upgrades of stable dependencies
+
+# User defined packages
+scapy==2.6.0
diff --git a/modules/test/dns/python/src/dns_module.py b/modules/test/dns/python/src/dns_module.py
index 2d98c36dc..fe244f0a7 100644
--- a/modules/test/dns/python/src/dns_module.py
+++ b/modules/test/dns/python/src/dns_module.py
@@ -31,7 +31,6 @@ class DNSModule(TestModule):
def __init__(self,
module,
- log_dir=None,
conf_file=None,
results_dir=None,
dns_server_capture_file=DNS_SERVER_CAPTURE_FILE,
@@ -39,7 +38,6 @@ def __init__(self,
monitor_capture_file=MONITOR_CAPTURE_FILE):
super().__init__(module_name=module,
log_name=LOG_NAME,
- log_dir=log_dir,
conf_file=conf_file,
results_dir=results_dir)
self.dns_server_capture_file = dns_server_capture_file
diff --git a/modules/test/ntp/bin/start_test_module b/modules/test/ntp/bin/start_test_module
index a09349cf9..33b2881f4 100644
--- a/modules/test/ntp/bin/start_test_module
+++ b/modules/test/ntp/bin/start_test_module
@@ -27,11 +27,8 @@ else
fi
# Create and set permissions on the log files
-LOG_FILE=/runtime/output/$MODULE_NAME.log
RESULT_FILE=/runtime/output/$MODULE_NAME-result.json
-touch $LOG_FILE
touch $RESULT_FILE
-chown $HOST_USER $LOG_FILE
chown $HOST_USER $RESULT_FILE
# Run the python scrip that will execute the tests for this module
diff --git a/modules/test/ntp/python/requirements.txt b/modules/test/ntp/python/requirements.txt
index 93b351f44..f61132516 100644
--- a/modules/test/ntp/python/requirements.txt
+++ b/modules/test/ntp/python/requirements.txt
@@ -1 +1,6 @@
-scapy
\ No newline at end of file
+# Dependencies to user defined packages
+# Package dependencies should always be defined before the user defined
+# packages to prevent auto-upgrades of stable dependencies
+
+# User defined packages
+scapy==2.6.0
diff --git a/modules/test/ntp/python/src/ntp_module.py b/modules/test/ntp/python/src/ntp_module.py
index f82240ff1..33729a8d1 100644
--- a/modules/test/ntp/python/src/ntp_module.py
+++ b/modules/test/ntp/python/src/ntp_module.py
@@ -14,6 +14,7 @@
"""NTP test module"""
from test_module import TestModule
from scapy.all import rdpcap, IP, IPv6, NTP, UDP, Ether
+from scapy.error import Scapy_Exception
import os
from collections import defaultdict
@@ -30,7 +31,6 @@ class NTPModule(TestModule):
def __init__(self,
module,
- log_dir=None,
conf_file=None,
results_dir=None,
ntp_server_capture_file=NTP_SERVER_CAPTURE_FILE,
@@ -38,7 +38,6 @@ def __init__(self,
monitor_capture_file=MONITOR_CAPTURE_FILE):
super().__init__(module_name=module,
log_name=LOG_NAME,
- log_dir=log_dir,
conf_file=conf_file,
results_dir=results_dir)
self.ntp_server_capture_file = ntp_server_capture_file
@@ -120,7 +119,7 @@ def generate_module_report(self):
if total_requests + total_responses > 0:
table_content = '''
-
+
| Source |
@@ -187,8 +186,12 @@ def extract_ntp_data(self):
# Read the pcap files
packets = (rdpcap(self.startup_capture_file) +
- rdpcap(self.monitor_capture_file) +
- rdpcap(self.ntp_server_capture_file))
+ rdpcap(self.monitor_capture_file))
+
+ try:
+ packets += rdpcap(self.ntp_server_capture_file)
+ except (FileNotFoundError, Scapy_Exception):
+ LOGGER.error('ntp.pcap not found or empty, ignoring')
# Iterate through NTP packets
for packet in packets:
@@ -240,9 +243,15 @@ def extract_ntp_data(self):
def _ntp_network_ntp_support(self):
LOGGER.info('Running ntp.network.ntp_support')
- packet_capture = (rdpcap(STARTUP_CAPTURE_FILE) +
- rdpcap(MONITOR_CAPTURE_FILE) +
- rdpcap(NTP_SERVER_CAPTURE_FILE))
+
+ # Read the pcap files
+ packet_capture = (rdpcap(self.startup_capture_file) +
+ rdpcap(self.monitor_capture_file))
+
+ try:
+ packet_capture += rdpcap(self.ntp_server_capture_file)
+ except (FileNotFoundError, Scapy_Exception):
+ LOGGER.error('ntp.pcap not found or empty, ignoring')
device_sends_ntp4 = False
device_sends_ntp3 = False
@@ -278,9 +287,15 @@ def _ntp_network_ntp_support(self):
def _ntp_network_ntp_dhcp(self):
LOGGER.info('Running ntp.network.ntp_dhcp')
- packet_capture = (rdpcap(STARTUP_CAPTURE_FILE) +
- rdpcap(MONITOR_CAPTURE_FILE) +
- rdpcap(NTP_SERVER_CAPTURE_FILE))
+
+ # Read the pcap files
+ packet_capture = (rdpcap(self.startup_capture_file) +
+ rdpcap(self.monitor_capture_file))
+
+ try:
+ packet_capture += rdpcap(self.ntp_server_capture_file)
+ except (FileNotFoundError, Scapy_Exception):
+ LOGGER.error('ntp.pcap not found or empty, ignoring')
device_sends_ntp = False
ntp_to_local = False
diff --git a/modules/test/protocol/README.md b/modules/test/protocol/README.md
index 765fbf758..08c8cd345 100644
--- a/modules/test/protocol/README.md
+++ b/modules/test/protocol/README.md
@@ -14,6 +14,6 @@ Within the ```python/src``` directory, the below tests are executed.
| ID | Description | Expected behavior | Required result
|---|---|---|---|
-| protocol.valid_bacnet | Can valid BACnet traffic be seen | BACnet traffic can be seen on the network and packets are valid | Required if Applicable |
+| protocol.valid_bacnet | Can valid BACnet traffic be seen | BACnet traffic can be seen on the network and packets are valid | Recommended |
| protocol.bacnet.version | Obtain the version of BACnet client used | The BACnet client implements an up to date version of BACnet | Recommended |
| protocol.valid_modbus | Can valid Modbus traffic be seen | Any Modbus functionality works as expected and valid Modbus traffic can be observed | Recommended |
\ No newline at end of file
diff --git a/modules/test/protocol/bin/start_test_module b/modules/test/protocol/bin/start_test_module
index d85ae7d6b..e51fdb7ed 100644
--- a/modules/test/protocol/bin/start_test_module
+++ b/modules/test/protocol/bin/start_test_module
@@ -38,11 +38,8 @@ else
fi
# Create and set permissions on the log files
-LOG_FILE=/runtime/output/$MODULE_NAME.log
RESULT_FILE=/runtime/output/$MODULE_NAME-result.json
-touch $LOG_FILE
touch $RESULT_FILE
-chown $HOST_USER $LOG_FILE
chown $HOST_USER $RESULT_FILE
# Run the python script that will execute the tests for this module
diff --git a/modules/test/protocol/python/requirements.txt b/modules/test/protocol/python/requirements.txt
index 5b54a724d..1fe889fe9 100644
--- a/modules/test/protocol/python/requirements.txt
+++ b/modules/test/protocol/python/requirements.txt
@@ -1,7 +1,14 @@
-# Required for BACnet protocol tests
-netifaces==0.11.0
-BAC0==23.7.3
-pytz==2024.1
-
-# Required for Modbus protocol tests
-pymodbus==3.7.0
\ No newline at end of file
+# Dependencies to user defined packages
+# Package dependencies should always be defined before the user defined
+# packages to prevent auto-upgrades of stable dependencies
+bacpypes==0.18.7
+colorama==0.4.6
+
+# User defined packages
+# Required for BACnet protocol tests
+netifaces==0.11.0
+BAC0==23.7.3
+pytz==2024.2
+
+# Required for Modbus protocol tests
+pymodbus==3.7.4
diff --git a/modules/test/protocol/python/src/protocol_bacnet.py b/modules/test/protocol/python/src/protocol_bacnet.py
index a17c9cdd3..9d4399b2b 100644
--- a/modules/test/protocol/python/src/protocol_bacnet.py
+++ b/modules/test/protocol/python/src/protocol_bacnet.py
@@ -82,8 +82,10 @@ def validate_device(self):
for device in self.devices:
object_id = str(device[3]) # BACnet Object ID
LOGGER.info('Checking device: ' + str(device))
- result &= self.validate_bacnet_source(
+ device_valid = self.validate_bacnet_source(
object_id=object_id, device_hw_addr=self.device_hw_addr)
+ if device_valid is not None:
+ result &= device_valid
description = ('BACnet device discovered' if result else
'BACnet device was found but was not device under test')
else:
diff --git a/modules/test/protocol/python/src/protocol_module.py b/modules/test/protocol/python/src/protocol_module.py
index 4f7c1a7e7..9d99c91bd 100644
--- a/modules/test/protocol/python/src/protocol_module.py
+++ b/modules/test/protocol/python/src/protocol_module.py
@@ -22,7 +22,7 @@
class ProtocolModule(TestModule):
- """Protocol Test module"""
+ """Protocol test module"""
def __init__(self, module):
self._supports_bacnet = False
diff --git a/modules/test/protocol/python/src/run.py b/modules/test/protocol/python/src/run.py
index d47c81cb6..a2788c833 100644
--- a/modules/test/protocol/python/src/run.py
+++ b/modules/test/protocol/python/src/run.py
@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-"""Run Baseline module"""
+"""Run protocol module"""
import argparse
import signal
import sys
diff --git a/modules/test/services/README.md b/modules/test/services/README.md
index eae8a0bd0..f6133cc6e 100644
--- a/modules/test/services/README.md
+++ b/modules/test/services/README.md
@@ -22,6 +22,6 @@ Within the ```python/src``` directory, the below tests are executed.
| security.services.pop | Check POP ports 109 and 110 are disabled and POP is not running on any port | There is no POP service running on any port | Required |
| security.services.imap | Check IMAP port 143 is disabled and IMAP is not running on any port | There is no IMAP service running on any port | Required |
| security.services.snmpv3 | Check SNMP port 161/162 is disabled. If SNMP is an essential service, it should be v3 | Device is unreachable on port 161/162 unless SNMP is essential in which case it is SNMPv3 that is used | Required |
-| security.services.vnc | Check VNS is disabled on any port | Device cannot be accessed via VNC on any port | Required |
+| security.services.vnc | Check VNC is disabled on any port | Device cannot be accessed via VNC on any port | Required |
| security.services.tftp | Check TFTP port 69 is disabled (UDP) | There is no TFTP service running on any port | Required |
| ntp.network.ntp_server | Check NTP port 123 is disabled and the device is not acting as an NTP server | The devices does not respond to NTP requests | Required |
\ No newline at end of file
diff --git a/modules/test/services/bin/start_test_module b/modules/test/services/bin/start_test_module
index d8cede486..a42ee4cf0 100644
--- a/modules/test/services/bin/start_test_module
+++ b/modules/test/services/bin/start_test_module
@@ -41,11 +41,8 @@ else
fi
# Create and set permissions on the log files
-LOG_FILE=/runtime/output/$MODULE_NAME.log
RESULT_FILE=/runtime/output/$MODULE_NAME-result.json
-touch $LOG_FILE
touch $RESULT_FILE
-chown $HOST_USER $LOG_FILE
chown $HOST_USER $RESULT_FILE
# Run the python scrip that will execute the tests for this module
diff --git a/modules/test/services/conf/module_config.json b/modules/test/services/conf/module_config.json
index fecb41862..b37435eda 100644
--- a/modules/test/services/conf/module_config.json
+++ b/modules/test/services/conf/module_config.json
@@ -9,7 +9,7 @@
"docker": {
"depends_on": "base",
"enable_container": true,
- "timeout": 600
+ "timeout": 900
},
"tests": [
{
@@ -310,6 +310,10 @@
{
"number": 5903,
"type": "tcp"
+ },
+ {
+ "number": 6001,
+ "type": "tcp"
}
]
},
@@ -368,6 +372,22 @@
"Disable the NTP server",
"Drop traffic entering port 123/udp"
]
+ },
+ {
+ "name": "protocol.services.bacnet",
+ "test_description": "Report whether the device is running a BACnet server",
+ "expected_behavior": "The device may or may not be running a BACnet server",
+ "config": {
+ "services": [
+ "bacnet"
+ ],
+ "ports": [
+ {
+ "number": 47808,
+ "type": "udp"
+ }
+ ]
+ }
}
]
}
diff --git a/modules/test/services/python/requirements.txt b/modules/test/services/python/requirements.txt
index a3fdd1857..02acc1e19 100644
--- a/modules/test/services/python/requirements.txt
+++ b/modules/test/services/python/requirements.txt
@@ -1 +1,6 @@
-xmltodict==0.13.0
\ No newline at end of file
+# Dependencies to user defined packages
+# Package dependencies should always be defined before the user defined
+# packages to prevent auto-upgrades of stable dependencies
+
+# User defined packages
+xmltodict==0.14.2
diff --git a/modules/test/services/python/src/services_module.py b/modules/test/services/python/src/services_module.py
index a96d47bc0..1a783e7dc 100644
--- a/modules/test/services/python/src/services_module.py
+++ b/modules/test/services/python/src/services_module.py
@@ -31,14 +31,12 @@ class ServicesModule(TestModule):
def __init__(self,
module,
- log_dir=None,
conf_file=None,
results_dir=None,
run=True,
nmap_scan_results_path=None):
super().__init__(module_name=module,
log_name=LOG_NAME,
- log_dir=log_dir,
conf_file=conf_file,
results_dir=results_dir)
self._scan_tcp_results = None
@@ -198,10 +196,9 @@ def _process_port_results(self):
self._scan_results.update(self._scan_udp_results)
def _scan_tcp_ports(self):
- max_port = 1000
LOGGER.info('Running nmap TCP port scan')
nmap_results = util.run_command( # pylint: disable=E1120
- f'''nmap --open -sT -sV -Pn -v -p 1-{max_port}
+ f'''nmap --open -sT -sV -Pn -v -p 1-65535
--version-intensity 7 -T4 -oX - {self._ipv4_addr}''')[0]
LOGGER.info('TCP port scan complete')
@@ -227,7 +224,7 @@ def _scan_udp_ports(self):
if len(ports) > 0:
port_list = ','.join(ports)
LOGGER.info('Running nmap UDP port scan')
- LOGGER.debug('UDP ports: ' + str(port_list))
+ LOGGER.info('UDP ports: ' + str(port_list))
nmap_results = util.run_command( # pylint: disable=E1120
f'nmap -sU -sV -p {port_list} -oX - {self._ipv4_addr}')[0]
LOGGER.info('UDP port scan complete')
@@ -423,3 +420,15 @@ def _security_ssh_version(self, config):
else:
return (False,
f"SSH server found running {open_port_info['version']}")
+
+ def _protocol_services_bacnet(self, config):
+ LOGGER.info('Running protocol.services.bacnet')
+
+ open_ports = self._check_results(config['ports'], config['services'])
+ if len(open_ports) == 0:
+ return False, 'No BACnet server found'
+ else:
+ return (
+ True,
+ f'''Found BACnet server running on port {', '.join(open_ports)}'''
+ )
diff --git a/modules/test/tls/README.md b/modules/test/tls/README.md
index ba1c6b1db..f0e0119af 100644
--- a/modules/test/tls/README.md
+++ b/modules/test/tls/README.md
@@ -14,5 +14,9 @@ Within the ```python/src``` directory, the below tests are executed.
| ID | Description | Expected behavior | Required result
|---|---|---|---|
-| security.tls.v1_2_server | Check the device web server is TLSv1.2 minimum and the certificate is valid | TLS 1.2 certificate is issues to the client when accessed | Required |
-| security.tls.v1_2_client | Device uses TLS with connections to external services on any port | The packet indicates a TLS connection with at least TLS v1.2 and support for ECDH and ECDSA ciphers | Required |
\ No newline at end of file
+| security.tls.v1_0_client | Device uses TLS with connection to an external service on port 443 (or any other port which could be running the webserver-HTTPS) | The packet indicates a TLS connection with at least TLS 1.0 and support | Informational |
+| security.tls.v1_2_server | Check the device web server TLS 1.2 and the certificate is valid | TLS 1.2 certificate is issues to the web browser client when accessed | Required if Applicable |
+| security.tls.v1_2_client | Device uses TLS with connection to an external service on port 443 (or any other port which could be running the webserver-HTTPS) | The packet indicates a TLS connection with at least TLS v1.2 and support for ECDH and ECDSA ciphers | Required if Applicable |
+| security.tls.v1_3_server | Check the device web server TLS 1.3 and the certificate is valid | TLS 1.3 certificate is issued to the web browser client when accessed | Informational |
+| security.tls.v1_3_client | Device uses TLS with connection to an external service on port 443 (or any other port which could be running the webserver-HTTPS) | The packet indicates a TLS connection with at least TLS 1.3 | Informational |
+
diff --git a/modules/test/tls/bin/get_client_hello_packets.sh b/modules/test/tls/bin/get_client_hello_packets.sh
index 317657187..fd06a3d28 100755
--- a/modules/test/tls/bin/get_client_hello_packets.sh
+++ b/modules/test/tls/bin/get_client_hello_packets.sh
@@ -15,11 +15,11 @@
# limitations under the License.
CAPTURE_FILE="$1"
-SRC_IP="$2"
+SRC_MAC="$2"
TLS_VERSION="$3"
TSHARK_OUTPUT="-T json -e ip.src -e tcp.dstport -e ip.dst"
-TSHARK_FILTER="ssl.handshake.type==1 and ip.src==$SRC_IP"
+TSHARK_FILTER="ssl.handshake.type==1 and eth.src==$SRC_MAC"
if [[ $TLS_VERSION == '1.0' ]]; then
TSHARK_FILTER="$TSHARK_FILTER and ssl.handshake.version==0x0301"
diff --git a/modules/test/tls/bin/get_non_tls_client_connections.sh b/modules/test/tls/bin/get_non_tls_client_connections.sh
index 2bfc3d635..03fe2a393 100755
--- a/modules/test/tls/bin/get_non_tls_client_connections.sh
+++ b/modules/test/tls/bin/get_non_tls_client_connections.sh
@@ -15,7 +15,7 @@
# limitations under the License.
CAPTURE_FILE="$1"
-SRC_IP="$2"
+SRC_MAC="$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
@@ -24,9 +24,8 @@ TSHARK_OUTPUT="-T json -e ip.src -e tcp.dstport -e ip.dst"
# - 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)"
+TSHARK_FILTER="eth.src == $SRC_MAC 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
index 7335cac80..8a0e1ddab 100755
--- a/modules/test/tls/bin/get_tls_client_connections.sh
+++ b/modules/test/tls/bin/get_tls_client_connections.sh
@@ -15,11 +15,11 @@
# limitations under the License.
CAPTURE_FILE="$1"
-SRC_IP="$2"
+SRC_MAC="$2"
PROTOCOL=$3
TSHARK_OUTPUT="-T json -e ip.src -e tcp.dstport -e ip.dst"
-TSHARK_FILTER="ip.src == $SRC_IP and tls"
+TSHARK_FILTER="eth.src == $SRC_MAC and tls"
# Add a protocol filter if defined
if [ -n "$PROTOCOL" ];then
@@ -29,4 +29,3 @@ fi
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
index e64d4e9fb..7498d8e62 100755
--- a/modules/test/tls/bin/get_tls_packets.sh
+++ b/modules/test/tls/bin/get_tls_packets.sh
@@ -16,13 +16,13 @@
CAPTURE_FILE="$1"
-SRC_IP="$2"
+SRC_MAC="$2"
TLS_VERSION="$3"
-TSHARK_OUTPUT="-T json -e ip.src -e tcp.dstport -e ip.dst"
+TSHARK_OUTPUT="-T json -e eth.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"
+TSHARK_FILTER="eth.src==$SRC_MAC and ssl.handshake.type!=1"
if [ $TLS_VERSION == '1.0' ];then
TSHARK_FILTER="$TSHARK_FILTER and ssl.record.version==0x0301"
@@ -37,4 +37,3 @@ fi
response=$(tshark -r "$CAPTURE_FILE" $TSHARK_OUTPUT $TSHARK_FILTER)
echo "$response"
-
\ No newline at end of file
diff --git a/modules/test/tls/bin/start_test_module b/modules/test/tls/bin/start_test_module
index d8cede486..a42ee4cf0 100644
--- a/modules/test/tls/bin/start_test_module
+++ b/modules/test/tls/bin/start_test_module
@@ -41,11 +41,8 @@ else
fi
# Create and set permissions on the log files
-LOG_FILE=/runtime/output/$MODULE_NAME.log
RESULT_FILE=/runtime/output/$MODULE_NAME-result.json
-touch $LOG_FILE
touch $RESULT_FILE
-chown $HOST_USER $LOG_FILE
chown $HOST_USER $RESULT_FILE
# Run the python scrip that will execute the tests for this module
diff --git a/modules/test/tls/conf/module_config.json b/modules/test/tls/conf/module_config.json
index 7058129f2..9c83c85df 100644
--- a/modules/test/tls/conf/module_config.json
+++ b/modules/test/tls/conf/module_config.json
@@ -9,7 +9,7 @@
"docker": {
"depends_on": "base",
"enable_container": true,
- "timeout": 300
+ "timeout": 420
},
"tests":[
{
@@ -23,7 +23,7 @@
},
{
"name": "security.tls.v1_2_server",
- "test_description": "Check the device web server TLS 1.2 & certificate is valid",
+ "test_description": "Check the device web server TLS 1.2 and the certificate is valid",
"expected_behavior": "TLS 1.2 certificate is issued to the web browser client when accessed",
"recommendations": [
"Enable TLS 1.2 support in the web server configuration",
@@ -42,26 +42,7 @@
},
{
"name": "security.tls.v1_3_server",
- "test_description": "Check the device web server TLS 1.3 & certificate is valid",
- "expected_behavior": "TLS 1.3 certificate is issued to the web browser client when accessed",
- "recommendations": [
- "Enable TLS 1.3 support in the web server configuration",
- "Disable TLS 1.0 and 1.1",
- "Sign the certificate used by the web server"
- ]
- },
- {
- "name": "security.tls.v1_3_client",
- "test_description": "Device uses TLS with connection to an external service on port 443 (or any other port which could be running the webserver-HTTPS)",
- "expected_behavior": "The packet indicates a TLS connection with at least TLS 1.3",
- "recommendations": [
- "Disable connections to unsecure services",
- "Ensure any URLs connected to are secure (https)"
- ]
- },
- {
- "name": "security.tls.v1_3_server",
- "test_description": "Check the device web server TLS 1.3 & certificate is valid",
+ "test_description": "Check the device web server TLS 1.3 and the certificate is valid",
"expected_behavior": "TLS 1.3 certificate is issued to the web browser client when accessed",
"recommendations": [
"Enable TLS 1.3 support in the web server configuration",
diff --git a/modules/test/tls/python/requirements-test.txt b/modules/test/tls/python/requirements-test.txt
index 93b351f44..10f9fa8d1 100644
--- a/modules/test/tls/python/requirements-test.txt
+++ b/modules/test/tls/python/requirements-test.txt
@@ -1 +1,6 @@
-scapy
\ No newline at end of file
+# Dependencies to user defined packages
+# Package dependencies should always be defined before the user defined
+# packages to prevent auto-upgrades of stable dependencies
+
+# User defined packages
+scapy==2.6.0
diff --git a/modules/test/tls/python/requirements.txt b/modules/test/tls/python/requirements.txt
index e03b1f45a..4f241ef86 100644
--- a/modules/test/tls/python/requirements.txt
+++ b/modules/test/tls/python/requirements.txt
@@ -1,5 +1,21 @@
-cryptography==43.0.1
-pyOpenSSL==24.2.1
-lxml==5.1.0 # Requirement of pyshark but if upgraded automatically above 5.1 will cause a python crash
-pyshark==0.6
-requests==2.32.3
+# Dependencies to user defined packages
+# Package dependencies should always be defined before the user defined
+# packages to prevent auto-upgrades of stable dependencies
+appdirs==1.4.4
+certifi==2024.8.30
+cffi==1.17.1
+charset-normalizer==3.3.2
+idna==3.8
+packaging==24.1
+pycparser==2.22
+pyshark==0.6
+termcolor==2.4.0
+urllib3==2.2.2
+
+# User defined packages
+cryptography==43.0.0
+pyOpenSSL==24.3.0
+lxml==5.1.0 # Requirement of pyshark but if upgraded automatically above 5.1 will cause a
+pyshark==0.6
+requests==2.32.3
+
diff --git a/modules/test/tls/python/src/run.py b/modules/test/tls/python/src/run.py
index 89de9f65e..2b7ea7e0f 100644
--- a/modules/test/tls/python/src/run.py
+++ b/modules/test/tls/python/src/run.py
@@ -37,7 +37,7 @@ def __init__(self, module):
self._test_module = TLSModule(module)
self._test_module.run_tests()
- #self._test_module.generate_module_report()
+ self._test_module.generate_module_report()
def _handler(self, signum):
LOGGER.debug('SigtermEnum: ' + str(signal.SIGTERM))
diff --git a/modules/test/tls/python/src/tls_module.py b/modules/test/tls/python/src/tls_module.py
index 186766b17..e9163843a 100644
--- a/modules/test/tls/python/src/tls_module.py
+++ b/modules/test/tls/python/src/tls_module.py
@@ -12,11 +12,19 @@
# See the License for the specific language governing permissions and
# limitations under the License.
"""TLS test module"""
+# pylint: disable=W0212
+
from test_module import TestModule
from tls_util import TLSUtil
+import os
import pyshark
+from binascii import hexlify
from cryptography import x509
from cryptography.hazmat.backends import default_backend
+from cryptography.hazmat.primitives import serialization
+from cryptography.hazmat.primitives.asymmetric import rsa, dsa, ec
+from cryptography.x509 import AuthorityKeyIdentifier, SubjectKeyIdentifier, BasicConstraints, KeyUsage
+from cryptography.x509 import GeneralNames, DNSName, ExtendedKeyUsage, ObjectIdentifier, SubjectAlternativeName
LOG_NAME = 'test_tls'
MODULE_REPORT_FILE_NAME = 'tls_report.html'
@@ -32,7 +40,6 @@ class TLSModule(TestModule):
def __init__(self,
module,
- log_dir=None,
conf_file=None,
results_dir=None,
startup_capture_file=STARTUP_CAPTURE_FILE,
@@ -40,7 +47,6 @@ def __init__(self,
tls_capture_file=TLS_CAPTURE_FILE):
super().__init__(module_name=module,
log_name=LOG_NAME,
- log_dir=log_dir,
conf_file=conf_file,
results_dir=results_dir)
self.startup_capture_file = startup_capture_file
@@ -50,146 +56,297 @@ def __init__(self,
LOGGER = self._get_logger()
self._tls_util = TLSUtil(LOGGER)
- # def generate_module_report(self):
- # html_content = 'TLS Module
'
-
- # # List of capture files to scan
- # pcap_files = [
- # self.startup_capture_file, self.monitor_capture_file,
- # self.tls_capture_file
- # ]
- # certificates = self.extract_certificates_from_pcap(pcap_files,
- # self._device_mac)
- # if len(certificates) > 0:
-
- # # Add summary table
- # summary_table = '''
- #
- #
- #
- # | Expiry |
- # Length |
- # Type |
- # Port number |
- # Signed by |
- #
- #
- #
- # '''
-
- # # table_content = '''
- # #
- # #
- # #
- # # | Expiry |
- # # Length |
- # # Type |
- # # Port number |
- # # Signed by |
- # #
- # #
- # # '''
-
- # cert_tables = []
- # for cert_num, ((ip_address, port), cert) in enumerate(
- # certificates.items()):
-
- # # Extract certificate data
- # not_valid_before = cert.not_valid_before
- # not_valid_after = cert.not_valid_after
- # version_value = f'{cert.version.value + 1} ({hex(cert.version.value)})'
- # signature_alg_value = cert.signature_algorithm_oid._name # pylint: disable=W0212
- # not_before = str(not_valid_before)
- # not_after = str(not_valid_after)
- # public_key = cert.public_key()
- # signed_by = 'None'
- # if isinstance(public_key, rsa.RSAPublicKey):
- # public_key_type = 'RSA'
- # elif isinstance(public_key, dsa.DSAPublicKey):
- # public_key_type = 'DSA'
- # elif isinstance(public_key, ec.EllipticCurvePublicKey):
- # public_key_type = 'EC'
- # else:
- # public_key_type = 'Unknown'
- # # Calculate certificate length
- # cert_length = len(cert.public_bytes(
- # encoding=serialization.Encoding.DER))
-
- # # Generate the Certificate table
- # # cert_table = (f'| Property | Value |\n'
- # # f'|---|---|\n'
- # # f"| {'Version':<17} | {version_value:^25} |\n"
- # # f"| {'Signature Alg.':<17} |
- # {signature_alg_value:^25} |\n"
- # # f"| {'Validity from':<17} | {not_before:^25} |\n"
- # # f"| {'Valid to':<17} | {not_after:^25} |")
-
- # # Generate the Subject table
- # subj_table = ('| Distinguished Name | Value |\n'
- # '|---|---|')
- # for val in cert.subject.rdns:
- # dn = val.rfc4514_string().split('=')
- # subj_table += f'\n| {dn[0]} | {dn[1]}'
-
- # # Generate the Issuer table
- # iss_table = ('| Distinguished Name | Value |\n'
- # '|---|---|')
- # for val in cert.issuer.rdns:
- # dn = val.rfc4514_string().split('=')
- # iss_table += f'\n| {dn[0]} | {dn[1]}'
- # if 'CN' in dn[0]:
- # signed_by = dn[1]
-
- # ext_table = None
- # # if cert.extensions:
- # # ext_table = ('| Extension | Value |\n'
- # # '|---|---|')
- # # for extension in cert.extensions:
- # # for extension_value in extension.value:
- # # ext_table += f'''\n| {extension.oid._name} |
- # # {extension_value.value}''' # pylint: disable=W0212
- # # cert_table = f'### Certificate\n{cert_table}'
- # # cert_table += f'\n\n### Subject\n{subj_table}'
- # # cert_table += f'\n\n### Issuer\n{iss_table}'
- # # if ext_table is not None:
- # # cert_table += f'\n\n### Extensions\n{ext_table}'
- # # cert_tables.append(cert_table)
-
- # summary_table += f'''
- #
- # | {not_after} |
- # {cert_length} |
- # {public_key_type} |
- # {port} |
- # {signed_by} |
- #
- # '''
-
- # summary_table += '''
- #
- #
- # '''
-
- # html_content += summary_table
-
- # else:
- # html_content += ('''
- #
- #
- # No TLS certificates found on the device
- #
''')
-
- # LOGGER.debug('Module report:\n' + html_content)
-
- # # Use os.path.join to create the complete file path
- # report_path = os.path.join(self._results_dir, MODULE_REPORT_FILE_NAME)
-
- # # Write the content to a file
- # with open(report_path, 'w', encoding='utf-8') as file:
- # file.write(html_content)
-
- # LOGGER.info('Module report generated at: ' + str(report_path))
- # return report_path
+ def generate_module_report(self):
+ html_content = 'TLS Module
'
+
+ # List of capture files to scan
+ pcap_files = [
+ self.startup_capture_file, self.monitor_capture_file,
+ self.tls_capture_file
+ ]
+ certificates = self.extract_certificates_from_pcap(pcap_files,
+ self._device_mac)
+
+ if len(certificates) > 0:
+
+ cert_tables = []
+ # pylint: disable=W0612
+ for cert_num, ((ip_address, port),
+ cert) in enumerate(certificates.items()):
+
+ # Add summary table
+ summary_table = '''
+
+
+
+ | Expiry |
+ Length |
+ Type |
+ Port number |
+ Signed by |
+
+
+
+ '''
+
+ # Generate the certificate table
+ cert_table = '''
+
+
+
+ | Property |
+ Value |
+
+
+ '''
+
+ # Extract certificate data
+ not_valid_before = cert.not_valid_before
+ not_valid_after = cert.not_valid_after
+ version_value = f'{cert.version.value + 1} ({hex(cert.version.value)})'
+ signature_alg_value = cert.signature_algorithm_oid._name
+ not_before = str(not_valid_before)
+ not_after = str(not_valid_after)
+ public_key = cert.public_key()
+ signed_by = 'None'
+
+ if isinstance(public_key, rsa.RSAPublicKey):
+ public_key_type = 'RSA'
+ elif isinstance(public_key, dsa.DSAPublicKey):
+ public_key_type = 'DSA'
+ elif isinstance(public_key, ec.EllipticCurvePublicKey):
+ public_key_type = 'EC'
+ else:
+ public_key_type = 'Unknown'
+
+ # Calculate certificate length
+ cert_length = len(
+ cert.public_bytes(encoding=serialization.Encoding.DER))
+
+ # Append certification information
+ cert_table += f'''
+
+ | Version |
+ {version_value} |
+
+
+ | Signature Alg. |
+ {signature_alg_value} |
+
+
+ | Validity from |
+ {not_before} |
+
+
+ | Valid to |
+ {not_after} |
+
+
+
+ '''
+
+ subject_table = '''
+
+
+
+ | Property |
+ Value |
+
+
+ '''
+
+ # Append the subject information
+ for val in cert.subject.rdns:
+ dn = val.rfc4514_string().split('=')
+ subject_table += f'''
+
+ | {dn[0]} |
+ {dn[1]} |
+
+ '''
+
+ subject_table += '''
+
+
'''
+
+ # Append issuer information
+ for val in cert.issuer.rdns:
+ dn = val.rfc4514_string().split('=')
+ if 'CN' in dn[0]:
+ signed_by = dn[1]
+
+ ext_table = ''
+
+ # Append extensions information
+ if cert.extensions:
+
+ ext_table = '''
+ Certificate Extensions
+
+
+
+ | Property |
+ Value |
+
+
+ '''
+
+ for extension in cert.extensions:
+ if isinstance(extension.value, list):
+ for extension_value in extension.value:
+ ext_table += f'''
+
+ | {extension.oid._name} |
+ {self.format_extension_value(extension_value.value)} |
+
+ '''
+ else:
+ ext_table += f'''
+
+ | {extension.oid._name} |
+ {self.format_extension_value(extension.value)} |
+
+ '''
+
+ ext_table += '''
+
+
'''
+
+ # Add summary table row
+ summary_table += f'''
+
+ | {not_after} |
+ {cert_length} |
+ {public_key_type} |
+ {port} |
+ {signed_by} |
+
+
+
+ '''
+
+ # Merge all table HTML
+ summary_table = f'\n{summary_table}'
+
+ summary_table += f'''
+
+
+
Certificate Information
+ {cert_table}
+
+
+
Subject Information
+ {subject_table}
+
+
'''
+
+ if ext_table is not None:
+ summary_table += f'\n\n{ext_table}'
+
+ cert_tables.append(summary_table)
+
+ outbound_conns = self._tls_util.get_all_outbound_connections(
+ device_mac=self._device_mac, capture_files=pcap_files)
+ conn_table = self.generate_outbound_connection_table(outbound_conns)
+
+ html_content += '\n'.join('\n' + tables for tables in cert_tables)
+ html_content += conn_table
+
+ else:
+ html_content += ('''
+
+
+ No TLS certificates found on the device
+
''')
+
+ LOGGER.debug('Module report:\n' + html_content)
+
+ # Use os.path.join to create the complete file path
+ report_path = os.path.join(self._results_dir, MODULE_REPORT_FILE_NAME)
+
+ # Write the content to a file
+ with open(report_path, 'w', encoding='utf-8') as file:
+ file.write(html_content)
+
+ LOGGER.info('Module report generated at: ' + str(report_path))
+ return report_path
+
+ def format_extension_value(self, value):
+ if isinstance(value, bytes):
+ # Convert byte sequences to hex strings
+ return hexlify(value).decode()
+ elif isinstance(value, (list, tuple)):
+ # Format lists/tuples for HTML output
+ return ', '.join([self.format_extension_value(v) for v in value])
+ elif isinstance(value, ExtendedKeyUsage):
+ # Handle ExtendedKeyUsage extension
+ return ', '.join(
+ [oid._name or f'Unknown OID ({oid.dotted_string})' for oid in value])
+ elif isinstance(value, GeneralNames):
+ # Handle GeneralNames (used in SubjectAlternativeName)
+ return ', '.join(
+ [name.value for name in value if isinstance(name, DNSName)])
+ elif isinstance(value, SubjectAlternativeName):
+ # Extract and format the GeneralNames (which contains DNSName,
+ #IPAddress, etc.)
+ return self.format_extension_value(value.get_values_for_type(DNSName))
+
+ elif isinstance(value, ObjectIdentifier):
+ # Handle ObjectIdentifier directly
+ return value._name or f'Unknown OID ({value.dotted_string})'
+ elif hasattr(value, '_name'):
+ # Extract the name for OIDs (Object Identifiers)
+ return value._name
+ elif isinstance(value, AuthorityKeyIdentifier):
+ # Handle AuthorityKeyIdentifier extension
+ key_id = self.format_extension_value(value.key_identifier)
+ cert_issuer = value.authority_cert_issuer
+ cert_serial = value.authority_cert_serial_number
+
+ return (f'key_identifier={key_id}, '
+ f'authority_cert_issuer={cert_issuer}, '
+ f'authority_cert_serial_number={cert_serial}')
+ elif isinstance(value, SubjectKeyIdentifier):
+ # Handle SubjectKeyIdentifier extension
+ return f'digest={self.format_extension_value(value.digest)}'
+ elif isinstance(value, BasicConstraints):
+ # Handle BasicConstraints extension
+ return f'ca={value.ca}, path_length={value.path_length}'
+ elif isinstance(value, KeyUsage):
+ # Handle KeyUsage extension
+ return (f'digital_signature={value.digital_signature}, '
+ f'key_cert_sign={value.key_cert_sign}, '
+ f'key_encipherment={value.key_encipherment}, '
+ f'crl_sign={value.crl_sign}')
+ return str(value) # Fallback to string conversion
+
+ def generate_outbound_connection_table(self, outbound_conns):
+ """Generate just an HTML table from a list of IPs"""
+ html_content = '''
+ Outbound Connections
+
+
+
+ | Destination IP |
+ Port |
+
+
+
+ '''
+
+ rows = [
+ f'\t| {ip} | {port} |
'
+ for ip, port in outbound_conns
+ ]
+ html_content += '\n'.join(rows)
+
+ # Close the table
+ html_content += """
+
+ \r
+ """
+
+ return html_content
def extract_certificates_from_pcap(self, pcap_files, mac_address):
# Initialize a list to store packets
@@ -224,7 +381,9 @@ def extract_certificates_from_pcap(self, pcap_files, mac_address):
port = packet.tcp.srcport if 'tcp' in packet else packet.udp.srcport
# Store certificate in dictionary with IP address and port as key
certificates[(ip_address, port)] = certificate
- return certificates
+ sorted_keys = sorted(certificates.keys(), key=lambda x: (x[0], x[1]))
+ sorted_certificates = {k: certificates[k] for k in sorted_keys}
+ return sorted_certificates
def _security_tls_v1_2_server(self):
LOGGER.info('Running security.tls.v1_2_server')
@@ -239,13 +398,16 @@ def _security_tls_v1_2_server(self):
tls_1_2_results, tls_1_3_results)
# Determine results and return proper messaging and details
description = ''
- if results[0] is None:
+ result = results[0]
+ details = results[1]
+ if result is None:
+ result = 'Feature Not Detected'
description = 'TLS 1.2 certificate could not be validated'
- elif results[0]:
+ elif result:
description = 'TLS 1.2 certificate is valid'
else:
description = 'TLS 1.2 certificate is invalid'
- return results[0], description, results[1]
+ return result, description, details
else:
LOGGER.error('Could not resolve device IP address. Skipping')
@@ -260,13 +422,17 @@ def _security_tls_v1_3_server(self):
tls_version='1.3')
# Determine results and return proper messaging and details
description = ''
- if results[0] is None:
+ result = results[0]
+ details = results[1]
+ description = ''
+ if result is None:
+ result = 'Feature Not Detected'
description = 'TLS 1.3 certificate could not be validated'
elif results[0]:
description = 'TLS 1.3 certificate is valid'
else:
description = 'TLS 1.3 certificate is invalid'
- return results[0], description, results[1]
+ return result, description, details
else:
LOGGER.error('Could not resolve device IP address')
@@ -274,71 +440,53 @@ def _security_tls_v1_3_server(self):
def _security_tls_v1_0_client(self):
LOGGER.info('Running security.tls.v1_0_client')
- self._resolve_device_ip()
- # If the ipv4 address wasn't resolved yet, try again
- if self._device_ipv4_addr is not None:
- tls_1_0_valid = self._validate_tls_client(self._device_ipv4_addr, '1.0')
- tls_1_1_valid = self._validate_tls_client(self._device_ipv4_addr, '1.1')
- tls_1_2_valid = self._validate_tls_client(self._device_ipv4_addr, '1.2')
- tls_1_3_valid = self._validate_tls_client(self._device_ipv4_addr, '1.3')
- states = [
- tls_1_0_valid[0], tls_1_1_valid[0], tls_1_2_valid[0], tls_1_3_valid[0]
- ]
- if any(state is True for state in states):
- # If any state is True, return True
- result_state = True
- result_message = 'TLS 1.0 or higher detected'
- elif all(state == 'Feature Not Detected' for state in states):
- # If all states are "Feature not Detected"
- result_state = 'Feature Not Detected'
- result_message = tls_1_0_valid[1]
- elif all(state == 'Error' for state in states):
- # If all states are "Error"
- result_state = 'Error'
- result_message = ''
- else:
- result_state = False
- result_message = 'TLS 1.0 or higher was not detected'
- result_details = tls_1_0_valid[2] + tls_1_1_valid[2] + tls_1_2_valid[
- 2] + tls_1_3_valid[2]
- result_tags = list(
- set(tls_1_0_valid[3] + tls_1_1_valid[3] + tls_1_2_valid[3] +
- tls_1_3_valid[3]))
- return result_state, result_message, result_details, result_tags
+ tls_1_0_valid = self._validate_tls_client(self._device_mac, '1.0')
+ tls_1_1_valid = self._validate_tls_client(self._device_mac, '1.1')
+ tls_1_2_valid = self._validate_tls_client(self._device_mac, '1.2')
+ tls_1_3_valid = self._validate_tls_client(self._device_mac, '1.3')
+ states = [
+ tls_1_0_valid[0], tls_1_1_valid[0], tls_1_2_valid[0], tls_1_3_valid[0]
+ ]
+ if any(state is True for state in states):
+ # If any state is True, return True
+ result_state = True
+ result_message = 'TLS 1.0 or higher detected'
+ elif all(state == 'Feature Not Detected' for state in states):
+ # If all states are "Feature not Detected"
+ result_state = 'Feature Not Detected'
+ result_message = tls_1_0_valid[1]
+ elif all(state == 'Error' for state in states):
+ # If all states are "Error"
+ result_state = 'Error'
+ result_message = ''
else:
- LOGGER.error('Could not resolve device IP address. Skipping')
- return 'Error', 'Could not resolve device IP address'
+ result_state = False
+ result_message = 'TLS 1.0 or higher was not detected'
+ result_details = tls_1_0_valid[2] + tls_1_1_valid[2] + tls_1_2_valid[
+ 2] + tls_1_3_valid[2]
+ result_tags = list(
+ set(tls_1_0_valid[3] + tls_1_1_valid[3] + tls_1_2_valid[3] +
+ tls_1_3_valid[3]))
+ return result_state, result_message, result_details, result_tags
def _security_tls_v1_2_client(self):
LOGGER.info('Running security.tls.v1_2_client')
- self._resolve_device_ip()
- # If the ipv4 address wasn't resolved yet, try again
- if self._device_ipv4_addr is not None:
- return self._validate_tls_client(self._device_ipv4_addr,
- '1.2',
- unsupported_versions=['1.0', '1.1'])
- else:
- LOGGER.error('Could not resolve device IP address. Skipping')
- return 'Error', 'Could not resolve device IP address'
+ return self._validate_tls_client(self._device_mac,
+ '1.2',
+ unsupported_versions=['1.0', '1.1'])
def _security_tls_v1_3_client(self):
LOGGER.info('Running security.tls.v1_3_client')
- self._resolve_device_ip()
- # If the ipv4 address wasn't resolved yet, try again
- if self._device_ipv4_addr is not None:
- return self._validate_tls_client(self._device_ipv4_addr,
- '1.3',
- unsupported_versions=['1.0', '1.1'])
- else:
- LOGGER.error('Could not resolve device IP address. Skipping')
- return 'Error', 'Could not resolve device IP address'
+ return self._validate_tls_client(self._device_mac,
+ '1.3',
+ unsupported_versions=['1.0', '1.1'])
def _validate_tls_client(self,
- client_ip,
+ client_mac,
tls_version,
unsupported_versions=None):
client_results = self._tls_util.validate_tls_client(
- client_ip=client_ip,
+ client_mac=client_mac,
tls_version=tls_version,
capture_files=[
MONITOR_CAPTURE_FILE, STARTUP_CAPTURE_FILE, TLS_CAPTURE_FILE
diff --git a/modules/test/tls/python/src/tls_util.py b/modules/test/tls/python/src/tls_util.py
index 9f00b96ef..37cd89133 100644
--- a/modules/test/tls/python/src/tls_util.py
+++ b/modules/test/tls/python/src/tls_util.py
@@ -25,6 +25,7 @@
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization
from ipaddress import IPv4Address
+from scapy.all import rdpcap, IP, Ether, TCP, UDP
LOG_NAME = 'tls_util'
LOGGER = None
@@ -37,6 +38,7 @@
ipaddress.ip_network('172.16.0.0/12'),
ipaddress.ip_network('192.168.0.0/16')
]
+TR_CONTAINER_MAC_PREFIX = '9a:02:57:1e:8f:'
#Define the allowed protocols as tshark filters
DEFAULT_ALLOWED_PROTOCOLS = ['quic']
@@ -59,6 +61,59 @@ def __init__(self,
if allowed_protocols is None:
self._allowed_protocols = DEFAULT_ALLOWED_PROTOCOLS
+ def get_all_outbound_connections(self, device_mac, capture_files):
+ """Process multiple pcap files and combine unique IP destinations in the
+ order of first appearance."""
+
+ all_outbound_conns = []
+ for capture in capture_files:
+ ips = self.get_outbound_connections(device_mac=device_mac,
+ capture_file=capture)
+ all_outbound_conns.extend(ips) # Collect all connections sequentially
+
+ # Remove duplicates while preserving the first-seen order
+ unique_ordered_conns = list(dict.fromkeys(all_outbound_conns))
+ return unique_ordered_conns
+
+ def get_outbound_connections(self, device_mac, capture_file):
+ """Extract unique IP and port destinations from a single pcap file
+ based on the known MAC address, preserving the order of appearance."""
+ packets = rdpcap(capture_file)
+ outbound_conns = []
+
+ for packet in packets:
+ if Ether in packet and IP in packet:
+ if packet[Ether].src == device_mac:
+ ip_dst = packet[IP].dst
+ port_dst = 'Unknown'
+
+ # Check if the packet has TCP or UDP layer to get the destination port
+ if TCP in packet:
+ port_dst = packet[TCP].dport
+ elif UDP in packet:
+ port_dst = packet[UDP].dport
+
+ if self.is_external_ip(ip_dst):
+ # Add to list as a tuple
+ outbound_conns.append((ip_dst, port_dst))
+
+ # Use dict.fromkeys to remove duplicates while preserving insertion order
+ unique_conns = list(dict.fromkeys(outbound_conns))
+ return unique_conns
+
+ def is_external_ip(self, ip):
+ """Check if the IP is an external (non-private) IP address."""
+ try:
+ # Convert the IP string into an IPv4Address object
+ ip_addr = ipaddress.ip_address(ip)
+
+ # Return True only if the IP is not in a private or reserved range
+ return not (ip_addr.is_private or ip_addr.is_loopback
+ or ip_addr.is_link_local)
+ except ValueError:
+ # Return False if the IP is invalid or not IPv4
+ return False
+
def get_public_certificate(self,
host,
port=443,
@@ -420,11 +475,11 @@ def get_ciphers(self, capture_file, dst_ip, dst_port):
ciphers = response[0].split('\n')
return ciphers
- def get_hello_packets(self, capture_files, src_ip, tls_version):
+ def get_hello_packets(self, capture_files, src_mac, 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}'
+ args = f'"{capture_file}" {src_mac} {tls_version}'
command = f'{bin_file} {args}'
response = util.run_command(command)
packets = response[0].strip()
@@ -446,11 +501,11 @@ def get_handshake_complete(self, capture_files, src_ip, dst_ip, tls_version):
return combined_results
# Resolve all connections from the device that don't use TLS
- def get_non_tls_packetes(self, client_ip, capture_files):
+ def get_non_tls_packetes(self, client_mac, 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}'
+ args = f'"{capture_file}" {client_mac}'
command = f'{bin_file} {args}'
response = util.run_command(command)
if len(response) > 0:
@@ -460,13 +515,13 @@ def get_non_tls_packetes(self, client_ip, capture_files):
# Resolve all connections from the device that use TLS
def get_tls_client_connection_packetes(self,
- client_ip,
+ client_mac,
capture_files,
protocol=None):
combined_packets = []
for capture_file in capture_files:
bin_file = self._bin_dir + '/get_tls_client_connections.sh'
- args = f'"{capture_file}" {client_ip}'
+ args = f'"{capture_file}" {client_mac}'
if protocol is not None:
args += f' {protocol}'
command = f'{bin_file} {args}'
@@ -479,16 +534,17 @@ def get_tls_client_connection_packetes(self,
# 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):
+ def get_tls_packets(self, capture_files, src_mac, 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}'
+ args = f'"{capture_file}" {src_mac} {tls_version}'
command = f'{bin_file} {args}'
response = util.run_command(command)
packets = response[0].strip()
+ parsed_json = json.loads(packets)
# Parse each packet and append key-value pairs to combined_results
- result = self.parse_packets(json.loads(packets), capture_file)
+ result = self.parse_packets(parsed_json, capture_file)
combined_results.extend(result)
return combined_results
@@ -560,16 +616,12 @@ def process_hello_packets(self,
# 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):
+ def get_non_tls_client_connection_ips(self, client_mac, capture_files):
LOGGER.info('Checking client for non-TLS client connections')
- packets = self.get_non_tls_packetes(client_ip=client_ip,
+ packets = self.get_non_tls_packetes(client_mac=client_mac,
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:
@@ -578,6 +630,13 @@ def get_non_tls_client_connection_ips(self, client_ip, capture_files):
tcp_flags = packet['_source']['layers']['tcp.flags']
if 'A' not in tcp_flags and 'S' not in tcp_flags:
# Packet is not ACK or SYN
+
+ src_ip = ipaddress.ip_address(
+ packet['_source']['layers']['ip.src'][0])
+ src_subnet = ipaddress.ip_network(src_ip, strict=False)
+ subnet_with_mask = ipaddress.ip_network(
+ src_subnet, strict=False).supernet(new_prefix=24)
+
dst_ip = ipaddress.ip_address(
packet['_source']['layers']['ip.dst'][0])
if not dst_ip in subnet_with_mask:
@@ -590,14 +649,14 @@ def get_non_tls_client_connection_ips(self, client_ip, capture_files):
# local network are approved and only connections to IP addresses outside
# our network will be flagged.
def get_unsupported_tls_ips(self,
- client_ip,
+ client_mac,
capture_files,
unsupported_versions=None):
LOGGER.info('Checking client for unsupported TLS client connections')
unsupported_tls_dst_ips = {}
if unsupported_versions is not None:
for unsupported_version in unsupported_versions:
- tls_packets = self.get_tls_packets(capture_files, client_ip, '1.0')
+ tls_packets = self.get_tls_packets(capture_files, client_mac, '1.0')
if len(tls_packets) > 0:
for packet in tls_packets:
dst_ip = packet['dst_ip']
@@ -618,10 +677,10 @@ def get_unsupported_tls_ips(self,
# 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):
+ def get_tls_client_connection_ips(self, client_mac, 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)
+ client_mac=client_mac, capture_files=capture_files)
tls_dst_ips = set() # Store unique destination IPs
for packet in packets:
@@ -631,13 +690,13 @@ def get_tls_client_connection_ips(self, client_ip, capture_files):
# Check if the device has made any outbound connections that use any
# allowed protocols that do not fit into a direct TLS packet inspection
- def get_allowed_protocol_client_connection_ips(self, client_ip,
+ def get_allowed_protocol_client_connection_ips(self, client_mac,
capture_files):
LOGGER.info('Checking client for TLS Protocol client connections')
tls_dst_ips = {} # Store unique destination IPs with the protocol name
for protocol in self._allowed_protocols:
packets = self.get_tls_client_connection_packetes(
- client_ip=client_ip, capture_files=capture_files, protocol=protocol)
+ client_mac=client_mac, capture_files=capture_files, protocol=protocol)
for packet in packets:
dst_ip = ipaddress.ip_address(packet['_source']['layers']['ip.dst'][0])
@@ -653,18 +712,18 @@ def is_private_ip(self, ip):
return False
def validate_tls_client(self,
- client_ip,
+ client_mac,
tls_version,
capture_files,
unsupported_versions=None):
LOGGER.info('Validating client for TLS: ' + tls_version)
- hello_packets = self.get_hello_packets(capture_files, client_ip,
+ hello_packets = self.get_hello_packets(capture_files, client_mac,
tls_version)
# Resolve allowed protocol connections that require
# additional consideration beyond packet inspection
protocol_client_ips = (self.get_allowed_protocol_client_connection_ips(
- client_ip, capture_files))
+ client_mac, capture_files))
if len(protocol_client_ips) > 0:
LOGGER.info(
@@ -735,10 +794,10 @@ def validate_tls_client(self,
# Resolve all non-TLS related client connections
non_tls_client_ips = self.get_non_tls_client_connection_ips(
- client_ip, capture_files)
+ client_mac, capture_files)
# Resolve all TLS related client connections
- tls_client_ips = self.get_tls_client_connection_ips(client_ip,
+ tls_client_ips = self.get_tls_client_connection_ips(client_mac,
capture_files)
# Filter out all outbound TLS connections regardless on whether
# or not they were validated. If they were not validated,
@@ -759,7 +818,8 @@ def validate_tls_client(self,
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,
+ unsupported_tls_ips = self.get_unsupported_tls_ips(client_mac,
+ capture_files,
unsupported_versions)
if len(unsupported_tls_ips) > 0:
tls_client_valid = False
diff --git a/modules/ui/angular.json b/modules/ui/angular.json
index 8109c8691..28e0e9a36 100644
--- a/modules/ui/angular.json
+++ b/modules/ui/angular.json
@@ -81,7 +81,8 @@
"inlineStyleLanguage": "scss",
"assets": ["src/favicon.ico", "src/assets"],
"styles": ["src/styles.scss"],
- "scripts": []
+ "scripts": [],
+ "karmaConfig": "karma.conf.js"
}
},
"lint": {
diff --git a/modules/ui/karma.conf.js b/modules/ui/karma.conf.js
new file mode 100644
index 000000000..b6d22bd62
--- /dev/null
+++ b/modules/ui/karma.conf.js
@@ -0,0 +1,43 @@
+// Karma configuration file, see link for more information
+// https://karma-runner.github.io/1.0/config/configuration-file.html
+
+module.exports = function (config) {
+ config.set({
+ basePath: '',
+ frameworks: ['jasmine', '@angular-devkit/build-angular'],
+ plugins: [
+ require('karma-jasmine'),
+ require('karma-chrome-launcher'),
+ require('karma-jasmine-html-reporter'),
+ require('karma-coverage'),
+ require('@angular-devkit/build-angular/plugins/karma'),
+ ],
+ client: {
+ jasmine: {
+ // you can add configuration options for Jasmine here
+ // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html
+ // for example, you can disable the random execution with `random: false`
+ // or set a specific seed with `seed: 4321`
+ },
+ },
+ jasmineHtmlReporter: {
+ suppressAll: true, // removes the duplicated traces
+ },
+ coverageReporter: {
+ dir: require('path').join(__dirname, './coverage/test-run-ui'),
+ subdir: '.',
+ reporters: [{ type: 'html' }, { type: 'text-summary' }],
+ check: {
+ global: {
+ statements: 75,
+ branches: 75,
+ functions: 75,
+ lines: 75,
+ },
+ },
+ },
+ reporters: ['progress', 'kjhtml'],
+ browsers: ['Chrome'],
+ restartOnFileChange: true,
+ });
+};
diff --git a/modules/ui/package-lock.json b/modules/ui/package-lock.json
index 7d3fb5c5c..b0ec71c07 100644
--- a/modules/ui/package-lock.json
+++ b/modules/ui/package-lock.json
@@ -6175,9 +6175,9 @@
"dev": true
},
"node_modules/cookie": {
- "version": "0.4.2",
- "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz",
- "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==",
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
+ "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
"dev": true,
"engines": {
"node": ">= 0.6"
@@ -6818,9 +6818,9 @@
}
},
"node_modules/engine.io": {
- "version": "6.6.1",
- "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.1.tgz",
- "integrity": "sha512-NEpDCw9hrvBW+hVEOK4T7v0jFJ++KgtPl4jKFwsZVfG1XhS0dCrSb3VMb9gPAd7VAdW52VT1EnaNiU2vM8C0og==",
+ "version": "6.6.2",
+ "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.2.tgz",
+ "integrity": "sha512-gmNvsYi9C8iErnZdVcJnvCpSKbWTt1E8+JZo8b+daLninywUWi5NQ5STSHZ9rFjFO7imNcvb8Pc5pe/wMR5xEw==",
"dev": true,
"dependencies": {
"@types/cookie": "^0.4.1",
@@ -6828,7 +6828,7 @@
"@types/node": ">=10.0.0",
"accepts": "~1.3.4",
"base64id": "2.0.0",
- "cookie": "~0.4.1",
+ "cookie": "~0.7.2",
"cors": "~2.8.5",
"debug": "~4.3.1",
"engine.io-parser": "~5.2.1",
@@ -7546,9 +7546,9 @@
"dev": true
},
"node_modules/express": {
- "version": "4.21.0",
- "resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz",
- "integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==",
+ "version": "4.21.1",
+ "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz",
+ "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==",
"dev": true,
"dependencies": {
"accepts": "~1.3.8",
@@ -7556,7 +7556,7 @@
"body-parser": "1.20.3",
"content-disposition": "0.5.4",
"content-type": "~1.0.4",
- "cookie": "0.6.0",
+ "cookie": "0.7.1",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
"depd": "2.0.0",
@@ -7588,9 +7588,9 @@
}
},
"node_modules/express/node_modules/cookie": {
- "version": "0.6.0",
- "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
- "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
+ "version": "0.7.1",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
+ "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==",
"dev": true,
"engines": {
"node": ">= 0.6"
diff --git a/modules/ui/package.json b/modules/ui/package.json
index b8f777d64..ae6d57f2d 100644
--- a/modules/ui/package.json
+++ b/modules/ui/package.json
@@ -9,6 +9,7 @@
"test": "ng test",
"test:coverage": "ng test --code-coverage",
"test-headless": "ng test --browsers=ChromeHeadless --watch=false",
+ "test-ci": "ng test --no-watch --no-progress --code-coverage --browsers=ChromeHeadless",
"docker": "docker rm -f test-run-ui && docker rmi test-run-ui && docker build -t test-run-ui . && docker run -d -p 80:80 --name test-run-ui test-run-ui",
"lint": "ng lint",
"lint:fix": "ng lint --fix",
diff --git a/modules/ui/src/app/app.component.html b/modules/ui/src/app/app.component.html
index 912e40c32..cb22dd809 100644
--- a/modules/ui/src/app/app.component.html
+++ b/modules/ui/src/app/app.component.html
@@ -79,7 +79,7 @@
class="app-toolbar-button app-toolbar-button-menu"
aria-label="Menu"
(click)="toggleMenu($event)"
- (keydown.tab)="skipToNavigation($event)">
+ (keydown.tab)="skipToNavigation($event, vm.focusNavigation)">
menu
{
let router: Router;
let mockService: SpyObj;
let store: MockStore;
- let focusNavigation = true;
let mockFocusManagerService: SpyObj;
let mockLiveAnnouncer: SpyObj;
let mockMqttService: SpyObj;
@@ -156,23 +149,14 @@ describe('AppComponent', () => {
{ provide: TestRunMqttService, useValue: mockMqttService },
{
provide: State,
- useValue: {
- getValue: () => ({
- [appFeatureKey]: {
- appComponent: {
- focusNavigation: focusNavigation,
- },
- },
- }),
- },
+ useValue: {},
},
provideMockStore({
selectors: [
{ selector: selectInterfaces, value: {} },
{ selector: selectHasConnectionSettings, value: true },
{ selector: selectInternetConnection, value: true },
- { selector: selectError, value: null },
- { selector: selectMenuOpened, value: false },
+ { selector: selectSystemConfig, value: { network: {} } },
{ selector: selectHasDevices, value: false },
{ selector: selectIsAllDevicesOutdated, value: false },
{ selector: selectHasExpiredDevices, value: false },
@@ -206,6 +190,7 @@ describe('AppComponent', () => {
router = TestBed.get(Router);
compiled = fixture.nativeElement as HTMLElement;
spyOn(store, 'dispatch').and.callFake(() => {});
+ component.appStore.updateSettingMissedError(null);
});
it('should create the app', () => {
@@ -397,17 +382,21 @@ describe('AppComponent', () => {
});
it('should dispatch toggleMenu action', () => {
+ spyOn(component.appStore, 'toggleMenu');
+
const menuBtn = compiled.querySelector(
'.app-toolbar-button-menu'
) as HTMLButtonElement;
menuBtn.click();
- expect(store.dispatch).toHaveBeenCalledWith(toggleMenu());
+ expect(component.appStore.toggleMenu).toHaveBeenCalled();
});
it('should focus navigation on tab press if menu button was clicked', () => {
- focusNavigation = true;
+ component.appStore.updateFocusNavigation(true);
+ fixture.detectChanges();
+ spyOn(component.appStore, 'updateFocusNavigation');
const menuBtn = compiled.querySelector(
'.app-toolbar-button-menu'
) as HTMLButtonElement;
@@ -415,8 +404,8 @@ describe('AppComponent', () => {
menuBtn.dispatchEvent(new KeyboardEvent('keydown', { key: 'Tab' }));
const navigation = compiled.querySelector('.app-sidebar');
- expect(store.dispatch).toHaveBeenCalledWith(
- updateFocusNavigation({ focusNavigation: false })
+ expect(component.appStore.updateFocusNavigation).toHaveBeenCalledWith(
+ false
);
expect(
mockFocusManagerService.focusFirstElementInContainer
@@ -424,7 +413,8 @@ describe('AppComponent', () => {
});
it('should not focus navigation button on tab press if menu button was not clicked', () => {
- focusNavigation = false;
+ component.appStore.updateFocusNavigation(false);
+ fixture.detectChanges();
const menuBtn = compiled.querySelector(
'.app-toolbar-button-menu'
) as HTMLButtonElement;
@@ -712,7 +702,7 @@ describe('AppComponent', () => {
});
});
- describe('with devices setted, without systemStatus data, but run the tests ', () => {
+ describe('with devices setted, without systemStatus data, but run the tests', () => {
beforeEach(() => {
store.overrideSelector(selectHasDevices, true);
fixture.detectChanges();
@@ -745,7 +735,7 @@ describe('AppComponent', () => {
describe('error', () => {
describe('with settingMissedError with one port is missed', () => {
beforeEach(() => {
- store.overrideSelector(selectError, {
+ component.appStore.updateSettingMissedError({
isSettingMissed: true,
devicePortMissed: true,
internetPortMissed: false,
@@ -764,7 +754,7 @@ describe('AppComponent', () => {
describe('with settingMissedError with two ports are missed', () => {
beforeEach(() => {
- store.overrideSelector(selectError, {
+ component.appStore.updateSettingMissedError({
isSettingMissed: true,
devicePortMissed: true,
internetPortMissed: true,
@@ -783,7 +773,7 @@ describe('AppComponent', () => {
describe('with no settingMissedError', () => {
beforeEach(() => {
- store.overrideSelector(selectError, null);
+ component.appStore.updateSettingMissedError(null);
store.overrideSelector(selectHasDevices, true);
fixture.detectChanges();
});
diff --git a/modules/ui/src/app/app.component.ts b/modules/ui/src/app/app.component.ts
index 7218459c4..ba8d33831 100644
--- a/modules/ui/src/app/app.component.ts
+++ b/modules/ui/src/app/app.component.ts
@@ -31,12 +31,7 @@ import { Routes } from './model/routes';
import { FocusManagerService } from './services/focus-manager.service';
import { State, Store } from '@ngrx/store';
import { AppState } from './store/state';
-import {
- setIsOpenAddDevice,
- toggleMenu,
- updateFocusNavigation,
-} from './store/actions';
-import { appFeatureKey } from './store/reducers';
+import { setIsOpenAddDevice } from './store/actions';
import { SettingsComponent } from './pages/settings/settings.component';
import { AppStore } from './app.store';
import { TestRunService } from './services/test-run.service';
@@ -205,19 +200,19 @@ export class AppComponent implements AfterViewInit {
public toggleMenu(event: MouseEvent) {
event.stopPropagation();
- this.store.dispatch(toggleMenu());
+ this.appStore.toggleMenu();
}
/**
* When side menu is opened
*/
- skipToNavigation(event: Event) {
- if (this.state.getValue()[appFeatureKey].appComponent.focusNavigation) {
+ skipToNavigation(event: Event, focusNavigation: boolean) {
+ if (focusNavigation) {
event.preventDefault(); // if not prevented, second element will be focused
this.focusManagerService.focusFirstElementInContainer(
this.navigation.nativeElement
);
- this.store.dispatch(updateFocusNavigation({ focusNavigation: false })); // user will be navigated according to normal flow on tab
+ this.appStore.updateFocusNavigation(false); // user will be navigated according to normal flow on tab
}
}
diff --git a/modules/ui/src/app/app.store.spec.ts b/modules/ui/src/app/app.store.spec.ts
index 4236b24fd..300a250fd 100644
--- a/modules/ui/src/app/app.store.spec.ts
+++ b/modules/ui/src/app/app.store.spec.ts
@@ -19,7 +19,6 @@ import { AppStore, CALLOUT_STATE_KEY, CONSENT_SHOWN_KEY } from './app.store';
import { MockStore, provideMockStore } from '@ngrx/store/testing';
import { AppState } from './store/state';
import {
- selectError,
selectHasConnectionSettings,
selectHasDevices,
selectHasExpiredDevices,
@@ -29,10 +28,10 @@ import {
selectIsAllDevicesOutdated,
selectIsOpenWaitSnackBar,
selectIsTestingComplete,
- selectMenuOpened,
selectReports,
selectRiskProfiles,
selectStatus,
+ selectSystemConfig,
selectSystemStatus,
selectTestModules,
} from './store/selectors';
@@ -53,6 +52,7 @@ import { NotificationService } from './services/notification.service';
import { FocusManagerService } from './services/focus-manager.service';
import { TestRunMqttService } from './services/test-run-mqtt.service';
import { MOCK_ADAPTERS } from './mocks/settings.mock';
+import { TestingType } from './model/device';
const mock = (() => {
let store: { [key: string]: string } = {};
@@ -87,6 +87,8 @@ describe('AppStore', () => {
let mockMqttService: SpyObj;
beforeEach(() => {
+ window.sessionStorage.clear();
+
mockService = jasmine.createSpyObj('mockService', [
'fetchDevices',
'getTestModules',
@@ -112,6 +114,8 @@ describe('AppStore', () => {
{ selector: selectSystemStatus, value: null },
{ selector: selectIsTestingComplete, value: false },
{ selector: selectRiskProfiles, value: [] },
+ { selector: selectSystemConfig, value: { network: {} } },
+ { selector: selectInterfaces, value: {} },
],
}),
{ provide: TestRunService, useValue: mockService },
@@ -130,9 +134,7 @@ describe('AppStore', () => {
store.overrideSelector(selectHasRiskProfiles, false);
store.overrideSelector(selectReports, []);
store.overrideSelector(selectHasConnectionSettings, true);
- store.overrideSelector(selectMenuOpened, true);
store.overrideSelector(selectInterfaces, {});
- store.overrideSelector(selectError, null);
store.overrideSelector(selectStatus, null);
spyOn(store, 'dispatch').and.callFake(() => {});
@@ -182,8 +184,9 @@ describe('AppStore', () => {
isTestingComplete: false,
riskProfiles: [],
hasConnectionSettings: true,
- isMenuOpen: true,
+ isMenuOpen: false,
interfaces: {},
+ focusNavigation: false,
settingMissedError: null,
calloutState: new Map(),
hasInternetConnection: false,
@@ -345,5 +348,177 @@ describe('AppStore', () => {
expect(mock.getObject(CALLOUT_STATE_KEY)).toBeTruthy();
});
});
+
+ describe('checkInterfacesInConfig', () => {
+ it('should update settingMissedError with all false if all ports are present', done => {
+ appStore.viewModel$.pipe(skip(1), take(1)).subscribe(store => {
+ expect(store.settingMissedError).toEqual({
+ isSettingMissed: false,
+ devicePortMissed: false,
+ internetPortMissed: false,
+ });
+ done();
+ });
+
+ store.overrideSelector(selectInterfaces, {
+ enx00e04c020fa8: '00:e0:4c:02:0f:a8',
+ enx207bd26205e9: '20:7b:d2:62:05:e9',
+ });
+ store.overrideSelector(selectSystemConfig, {
+ network: {
+ device_intf: 'enx00e04c020fa8',
+ internet_intf: 'enx207bd26205e9',
+ },
+ });
+ store.refreshState();
+ });
+
+ it('should update settingMissedError with all true if all ports are missing', done => {
+ appStore.viewModel$.pipe(skip(1), take(1)).subscribe(store => {
+ expect(store.settingMissedError).toEqual({
+ isSettingMissed: true,
+ devicePortMissed: true,
+ internetPortMissed: true,
+ });
+ done();
+ });
+
+ store.overrideSelector(selectInterfaces, {
+ enx00e04c020fa9: '00:e0:4c:02:0f:a8',
+ enx207bd26205e8: '20:7b:d2:62:05:e9',
+ });
+ store.overrideSelector(selectSystemConfig, {
+ network: {
+ device_intf: 'enx00e04c020fa8',
+ internet_intf: 'enx207bd26205e9',
+ },
+ });
+ store.refreshState();
+ });
+
+ it('should update settingMissedError with devicePortMissed true if device port is missing', done => {
+ appStore.viewModel$.pipe(skip(1), take(1)).subscribe(store => {
+ expect(store.settingMissedError).toEqual({
+ isSettingMissed: true,
+ devicePortMissed: true,
+ internetPortMissed: false,
+ });
+ done();
+ });
+
+ store.overrideSelector(selectInterfaces, {
+ enx00e04c020fa9: '00:e0:4c:02:0f:a8',
+ enx207bd26205e8: '20:7b:d2:62:05:e9',
+ });
+ store.overrideSelector(selectSystemConfig, {
+ network: {
+ device_intf: 'enx00e04c020fa8',
+ internet_intf: 'enx207bd26205e8',
+ },
+ });
+ store.refreshState();
+ });
+
+ it('should update settingMissedError with internetPortMissed true if device internet is missing', done => {
+ appStore.viewModel$.pipe(skip(1), take(1)).subscribe(store => {
+ expect(store.settingMissedError).toEqual({
+ isSettingMissed: true,
+ devicePortMissed: false,
+ internetPortMissed: true,
+ });
+ done();
+ });
+
+ store.overrideSelector(selectInterfaces, {
+ enx00e04c020fa9: '00:e0:4c:02:0f:a8',
+ enx207bd26205e8: '20:7b:d2:62:05:e9',
+ });
+ store.overrideSelector(selectSystemConfig, {
+ network: {
+ device_intf: 'enx00e04c020fa9',
+ internet_intf: 'enx207bd26205e9',
+ },
+ });
+ store.refreshState();
+ });
+
+ it('should update settingMissedError with all false if interface are not empty and config is not set', done => {
+ appStore.viewModel$.pipe(skip(1), take(1)).subscribe(store => {
+ expect(store.settingMissedError).toEqual({
+ isSettingMissed: false,
+ devicePortMissed: false,
+ internetPortMissed: false,
+ });
+ done();
+ });
+
+ store.overrideSelector(selectInterfaces, {
+ enx00e04c020fa8: '00:e0:4c:02:0f:a8',
+ enx207bd26205e9: '20:7b:d2:62:05:e9',
+ });
+ store.overrideSelector(selectSystemConfig, {
+ network: {
+ device_intf: '',
+ internet_intf: '',
+ },
+ });
+ store.refreshState();
+ });
+
+ it('should update settingMissedError with all false if interface are empty and config is not set', done => {
+ appStore.viewModel$.pipe(skip(1), take(1)).subscribe(store => {
+ expect(store.settingMissedError).toEqual({
+ isSettingMissed: false,
+ devicePortMissed: false,
+ internetPortMissed: false,
+ });
+ done();
+ });
+
+ store.overrideSelector(selectInterfaces, {});
+ store.overrideSelector(selectSystemConfig, {
+ network: {
+ device_intf: '',
+ internet_intf: '',
+ },
+ });
+ store.refreshState();
+ });
+
+ it('should send GA event', done => {
+ // @ts-expect-error data layer should be defined
+ window.dataLayer = window.dataLayer || [];
+
+ appStore.viewModel$.pipe(skip(1), take(1)).subscribe(() => {
+ expect(
+ // @ts-expect-error data layer should be defined
+ window.dataLayer.some(event => event.event === 'pilot_is_compliant')
+ ).toBeTrue();
+ done();
+ });
+
+ store.overrideSelector(selectIsTestingComplete, true);
+ store.overrideSelector(selectSystemStatus, {
+ status: 'Compliant',
+ mac_addr: '00:1e:42:35:73:c4',
+ device: {
+ manufacturer: 'Delta',
+ model: '03-DIN-CPU',
+ mac_addr: '00:1e:42:35:73:c4',
+ firmware: '1.2.2',
+ test_pack: TestingType.Pilot,
+ },
+ started: '2023-06-22T09:20:00.123Z',
+ finished: '2023-06-22T09:26:00.123Z',
+ report: 'https://api.testrun.io/report.pdf',
+ tags: [],
+ tests: {
+ total: 3,
+ results: [],
+ },
+ });
+ store.refreshState();
+ });
+ });
});
});
diff --git a/modules/ui/src/app/app.store.ts b/modules/ui/src/app/app.store.ts
index 91280724c..a12a536a3 100644
--- a/modules/ui/src/app/app.store.ts
+++ b/modules/ui/src/app/app.store.ts
@@ -16,9 +16,8 @@
import { Injectable } from '@angular/core';
import { ComponentStore } from '@ngrx/component-store';
-import { tap } from 'rxjs/operators';
+import { tap, withLatestFrom } from 'rxjs/operators';
import {
- selectError,
selectHasConnectionSettings,
selectHasDevices,
selectHasExpiredDevices,
@@ -27,17 +26,24 @@ import {
selectInternetConnection,
selectIsAllDevicesOutdated,
selectIsTestingComplete,
- selectMenuOpened,
selectReports,
selectRiskProfiles,
selectStatus,
+ selectSystemConfig,
selectSystemStatus,
} from './store/selectors';
import { Store } from '@ngrx/store';
import { AppState } from './store/state';
import { TestRunService } from './services/test-run.service';
-import { delay, exhaustMap, Observable, skip } from 'rxjs';
-import { Device, TestModule } from './model/device';
+import {
+ combineLatest,
+ delay,
+ exhaustMap,
+ filter,
+ Observable,
+ skip,
+} from 'rxjs';
+import { Device, TestingType, TestModule } from './model/device';
import {
setDevices,
setIsOpenStartTestrun,
@@ -47,10 +53,11 @@ import {
setTestModules,
updateAdapters,
} from './store/actions';
-import { TestrunStatus } from './model/testrun-status';
+import { StatusOfTestrun, TestrunStatus } from './model/testrun-status';
import {
Adapters,
SettingMissedError,
+ SystemConfig,
SystemInterfaces,
} from './model/setting';
import { FocusManagerService } from './services/focus-manager.service';
@@ -65,6 +72,12 @@ export interface AppComponentState {
isStatusLoaded: boolean;
systemStatus: TestrunStatus | null;
calloutState: Map;
+ isMenuOpen: boolean;
+ /**
+ * Indicates, if side menu should be focused on keyboard navigation after menu is opened
+ */
+ focusNavigation: boolean;
+ settingMissedError: SettingMissedError | null;
}
@Injectable()
export class AppStore extends ComponentStore {
@@ -80,11 +93,14 @@ export class AppStore extends ComponentStore {
private hasConnectionSetting$ = this.store.select(
selectHasConnectionSettings
);
- private isMenuOpen$ = this.store.select(selectMenuOpened);
+ private isMenuOpened$ = this.select(state => state.isMenuOpen);
+ private focusNavigation$ = this.select(state => state.focusNavigation);
private interfaces$: Observable =
this.store.select(selectInterfaces);
+ private systemConfig$: Observable =
+ this.store.select(selectSystemConfig);
private settingMissedError$: Observable =
- this.store.select(selectError);
+ this.select(state => state.settingMissedError);
systemStatus$: Observable = this.store.select(selectStatus);
testrunStatus$: Observable =
this.store.select(selectSystemStatus);
@@ -106,11 +122,12 @@ export class AppStore extends ComponentStore {
isTestingComplete: this.isTestingComplete$,
riskProfiles: this.riskProfiles$,
hasConnectionSettings: this.hasConnectionSetting$,
- isMenuOpen: this.isMenuOpen$,
+ isMenuOpen: this.isMenuOpened$,
interfaces: this.interfaces$,
settingMissedError: this.settingMissedError$,
calloutState: this.calloutState$,
hasInternetConnection: this.hasInternetConnection$,
+ focusNavigation: this.focusNavigation$,
});
updateConsent = this.updater((state, consentShown: boolean) => ({
@@ -134,6 +151,23 @@ export class AppStore extends ComponentStore {
isStatusLoaded,
}));
+ updateFocusNavigation = this.updater((state, focusNavigation: boolean) => ({
+ ...state,
+ focusNavigation,
+ }));
+
+ updateIsMenuOpened = this.updater((state, isMenuOpen: boolean) => ({
+ ...state,
+ isMenuOpen,
+ }));
+
+ updateSettingMissedError = this.updater(
+ (state, settingMissedError: SettingMissedError | null) => ({
+ ...state,
+ settingMissedError,
+ })
+ );
+
setContent = this.effect(trigger$ => {
return trigger$.pipe(
tap(() => {
@@ -259,6 +293,58 @@ export class AppStore extends ComponentStore {
);
});
+ toggleMenu = this.effect(trigger$ => {
+ return trigger$.pipe(
+ withLatestFrom(this.isMenuOpened$),
+ tap(([, opened]) => {
+ this.updateIsMenuOpened(!opened);
+ if (!opened) {
+ this.updateFocusNavigation(true);
+ }
+ })
+ );
+ });
+
+ checkInterfacesInConfig = this.effect(() => {
+ return combineLatest([
+ this.interfaces$.pipe(skip(1)),
+ this.systemConfig$.pipe(skip(1)),
+ ]).pipe(
+ filter(([, { network }]) => network !== null),
+ tap(([interfaces, { network, single_intf }]) => {
+ const deviceValid =
+ network?.device_intf == '' ||
+ (!!network?.device_intf && !!interfaces[network.device_intf]);
+ const internetValid = single_intf
+ ? true
+ : network?.internet_intf == '' ||
+ (!!network?.internet_intf && !!interfaces[network.internet_intf]);
+ this.updateSettingMissedError({
+ isSettingMissed: !deviceValid || !internetValid,
+ devicePortMissed: !deviceValid,
+ internetPortMissed: !internetValid,
+ });
+ })
+ );
+ });
+
+ sendGAEvent = this.effect(() => {
+ return combineLatest([this.isTestingComplete$, this.testrunStatus$]).pipe(
+ filter(([isTestingComplete]) => isTestingComplete === true),
+ filter(
+ ([, testrunStatus]) =>
+ testrunStatus?.status === StatusOfTestrun.Compliant &&
+ testrunStatus?.device.test_pack === TestingType.Pilot
+ ),
+ tap(() => {
+ // @ts-expect-error data layer is not null
+ window.dataLayer.push({
+ event: 'pilot_is_compliant',
+ });
+ })
+ );
+ });
+
constructor(
private store: Store,
private testRunService: TestRunService,
@@ -276,6 +362,9 @@ export class AppStore extends ComponentStore {
calloutState: calloutState
? new Map(Object.entries(calloutState))
: new Map(),
+ isMenuOpen: false,
+ focusNavigation: false,
+ settingMissedError: null,
});
}
}
diff --git a/modules/ui/src/app/components/download-report-zip/download-report-zip.component.spec.ts b/modules/ui/src/app/components/download-report-zip/download-report-zip.component.spec.ts
index 1f0919286..c0a2bd18b 100644
--- a/modules/ui/src/app/components/download-report-zip/download-report-zip.component.spec.ts
+++ b/modules/ui/src/app/components/download-report-zip/download-report-zip.component.spec.ts
@@ -13,36 +13,23 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import {
- ComponentFixture,
- discardPeriodicTasks,
- fakeAsync,
- TestBed,
- tick,
-} from '@angular/core/testing';
+import { ComponentFixture, fakeAsync, TestBed } from '@angular/core/testing';
import { DownloadReportZipComponent } from './download-report-zip.component';
import { of } from 'rxjs';
import { MatDialogRef } from '@angular/material/dialog';
-import { DownloadZipModalComponent } from '../download-zip-modal/download-zip-modal.component';
-import { Router } from '@angular/router';
-import { TestRunService } from '../../services/test-run.service';
-import { Routes } from '../../model/routes';
+import {
+ DialogCloseAction,
+ DownloadZipModalComponent,
+} from '../download-zip-modal/download-zip-modal.component';
import { RouterTestingModule } from '@angular/router/testing';
import { Component } from '@angular/core';
import { MOCK_PROGRESS_DATA_COMPLIANT } from '../../mocks/testrun.mock';
-import { FocusManagerService } from '../../services/focus-manager.service';
describe('DownloadReportZipComponent', () => {
let component: DownloadReportZipComponent;
let fixture: ComponentFixture;
let compiled: HTMLElement;
- let router: Router;
-
- const testrunServiceMock: jasmine.SpyObj =
- jasmine.createSpyObj('testrunServiceMock', ['downloadZip']);
- const focusServiceMock: jasmine.SpyObj =
- jasmine.createSpyObj('focusServiceMock', ['focusFirstElementInContainer']);
beforeEach(async () => {
await TestBed.configureTestingModule({
@@ -52,13 +39,8 @@ describe('DownloadReportZipComponent', () => {
]),
DownloadReportZipComponent,
],
- providers: [
- { provide: TestRunService, useValue: testrunServiceMock },
- { provide: FocusManagerService, useValue: focusServiceMock },
- ],
}).compileComponents();
fixture = TestBed.createComponent(DownloadReportZipComponent);
- router = TestBed.get(Router);
compiled = fixture.nativeElement as HTMLElement;
component = fixture.componentInstance;
component.url = 'localhost:8080';
@@ -71,87 +53,25 @@ describe('DownloadReportZipComponent', () => {
});
describe('#onClick', () => {
- beforeEach(() => {
- testrunServiceMock.downloadZip.calls.reset();
- });
-
- it('should call service if profile is a string', fakeAsync(() => {
+ it('should open zip modal dialog', fakeAsync(() => {
const openSpy = spyOn(component.dialog, 'open').and.returnValue({
- afterClosed: () => of(''),
+ afterClosed: () =>
+ of({ action: DialogCloseAction.Download, profile: '' }),
} as MatDialogRef);
-
component.onClick(new Event('click'));
expect(openSpy).toHaveBeenCalledWith(DownloadZipModalComponent, {
ariaLabel: 'Download zip',
data: {
profiles: [],
+ url: 'localhost:8080',
+ isPilot: false,
},
autoFocus: true,
hasBackdrop: true,
disableClose: true,
panelClass: 'initiate-test-run-dialog',
});
-
- tick();
-
- expect(testrunServiceMock.downloadZip).toHaveBeenCalled();
- expect(router.url).not.toBe(Routes.RiskAssessment);
- openSpy.calls.reset();
- }));
-
- it('should navigate to risk profiles page if profile is null', fakeAsync(() => {
- const openSpy = spyOn(component.dialog, 'open').and.returnValue({
- afterClosed: () => of(null),
- } as MatDialogRef);
-
- fixture.ngZone?.run(() => {
- component.onClick(new Event('click'));
-
- expect(openSpy).toHaveBeenCalledWith(DownloadZipModalComponent, {
- ariaLabel: 'Download zip',
- data: {
- profiles: [],
- },
- autoFocus: true,
- hasBackdrop: true,
- disableClose: true,
- panelClass: 'initiate-test-run-dialog',
- });
-
- tick(100);
-
- expect(router.url).toBe(Routes.RiskAssessment);
- expect(
- focusServiceMock.focusFirstElementInContainer
- ).toHaveBeenCalled();
-
- openSpy.calls.reset();
- discardPeriodicTasks();
- });
- }));
-
- it('should not call service to download zip if profile is undefined', fakeAsync(() => {
- const openSpy = spyOn(component.dialog, 'open').and.returnValue({
- afterClosed: () => of(undefined),
- } as MatDialogRef);
-
- component.onClick(new Event('click'));
-
- expect(openSpy).toHaveBeenCalledWith(DownloadZipModalComponent, {
- ariaLabel: 'Download zip',
- data: {
- profiles: [],
- },
- autoFocus: true,
- hasBackdrop: true,
- disableClose: true,
- panelClass: 'initiate-test-run-dialog',
- });
-
- tick();
-
- expect(testrunServiceMock.downloadZip).not.toHaveBeenCalled();
openSpy.calls.reset();
}));
});
diff --git a/modules/ui/src/app/components/download-report-zip/download-report-zip.component.ts b/modules/ui/src/app/components/download-report-zip/download-report-zip.component.ts
index c643cc687..742210937 100644
--- a/modules/ui/src/app/components/download-report-zip/download-report-zip.component.ts
+++ b/modules/ui/src/app/components/download-report-zip/download-report-zip.component.ts
@@ -19,20 +19,15 @@ import {
HostBinding,
HostListener,
Input,
- OnDestroy,
OnInit,
} from '@angular/core';
import { CommonModule, DatePipe } from '@angular/common';
import { Profile } from '../../model/profile';
import { MatDialog } from '@angular/material/dialog';
-import { Subject, takeUntil, timer } from 'rxjs';
-import { Routes } from '../../model/routes';
import { DownloadZipModalComponent } from '../download-zip-modal/download-zip-modal.component';
-import { TestRunService } from '../../services/test-run.service';
-import { Router } from '@angular/router';
import { ReportActionComponent } from '../report-action/report-action.component';
import { MatTooltip, MatTooltipModule } from '@angular/material/tooltip';
-import { FocusManagerService } from '../../services/focus-manager.service';
+import { TestingType } from '../../model/device';
@Component({
selector: 'app-download-report-zip',
@@ -45,9 +40,8 @@ import { FocusManagerService } from '../../services/focus-manager.service';
})
export class DownloadReportZipComponent
extends ReportActionComponent
- implements OnDestroy, OnInit
+ implements OnInit
{
- private destroy$: Subject = new Subject();
@Input() profiles: Profile[] = [];
@Input() url: string | null | undefined = null;
@@ -58,36 +52,18 @@ export class DownloadReportZipComponent
event.preventDefault();
event.stopPropagation();
- const dialogRef = this.dialog.open(DownloadZipModalComponent, {
+ this.dialog.open(DownloadZipModalComponent, {
ariaLabel: 'Download zip',
data: {
profiles: this.profiles,
+ url: this.url,
+ isPilot: this.data?.device.test_pack === TestingType.Pilot,
},
autoFocus: true,
hasBackdrop: true,
disableClose: true,
panelClass: 'initiate-test-run-dialog',
});
-
- dialogRef
- ?.afterClosed()
- .pipe(takeUntil(this.destroy$))
- .subscribe(profile => {
- if (profile === undefined) {
- return;
- }
- if (profile === null) {
- this.route.navigate([Routes.RiskAssessment]).then(() =>
- timer(100)
- .pipe(takeUntil(this.destroy$))
- .subscribe(() => {
- this.focusManagerService.focusFirstElementInContainer();
- })
- );
- } else if (this.url != null) {
- this.testrunService.downloadZip(this.getZipLink(this.url), profile);
- }
- });
}
@HostBinding('tabIndex')
@@ -105,11 +81,6 @@ export class DownloadReportZipComponent
this.tooltip.hide();
}
- ngOnDestroy() {
- this.destroy$.next(true);
- this.destroy$.unsubscribe();
- }
-
ngOnInit() {
if (this.data) {
this.tooltip.message = `Download zip for Testrun # ${this.getTestRunId(this.data)}`;
@@ -119,15 +90,8 @@ export class DownloadReportZipComponent
constructor(
datePipe: DatePipe,
public dialog: MatDialog,
- private testrunService: TestRunService,
- private route: Router,
- public tooltip: MatTooltip,
- private focusManagerService: FocusManagerService
+ public tooltip: MatTooltip
) {
super(datePipe);
}
-
- private getZipLink(reportURL: string): string {
- return reportURL.replace('report', 'export');
- }
}
diff --git a/modules/ui/src/app/components/download-report/download-report.component.spec.ts b/modules/ui/src/app/components/download-report/download-report.component.spec.ts
index f29430f54..af711a26b 100644
--- a/modules/ui/src/app/components/download-report/download-report.component.spec.ts
+++ b/modules/ui/src/app/components/download-report/download-report.component.spec.ts
@@ -21,6 +21,7 @@ import {
MOCK_PROGRESS_DATA_COMPLIANT,
MOCK_PROGRESS_DATA_NON_COMPLIANT,
} from '../../mocks/testrun.mock';
+import { TestrunStatus } from '../../model/testrun-status';
describe('DownloadReportComponent', () => {
let component: DownloadReportComponent;
@@ -39,13 +40,26 @@ describe('DownloadReportComponent', () => {
expect(component).toBeTruthy();
});
- it('#getReportTitle should return data for download property of link', () => {
- const expectedResult =
- 'delta_03-din-cpu_1.2.2_compliant_22_jun_2023_9:20';
+ describe('#getReportTitle', () => {
+ it('should return data for download property of link', () => {
+ const expectedResult =
+ 'delta_03-din-cpu_1.2.2_compliant_22_jun_2023_9:20';
- const result = component.getReportTitle(MOCK_PROGRESS_DATA_COMPLIANT);
+ const result = component.getReportTitle(MOCK_PROGRESS_DATA_COMPLIANT);
- expect(result).toEqual(expectedResult);
+ expect(result).toEqual(expectedResult);
+ });
+
+ it('should return empty string if no device data', () => {
+ const MOCK_DATA_WITHOUT_DEVICE = {
+ ...MOCK_PROGRESS_DATA_COMPLIANT,
+ device: undefined as unknown,
+ } as TestrunStatus;
+
+ const result = component.getReportTitle(MOCK_DATA_WITHOUT_DEVICE);
+
+ expect(result).toEqual('');
+ });
});
describe('#getClass', () => {
diff --git a/modules/ui/src/app/components/download-zip-modal/download-zip-modal.component.spec.ts b/modules/ui/src/app/components/download-zip-modal/download-zip-modal.component.spec.ts
index 728590ef8..74d0990e1 100644
--- a/modules/ui/src/app/components/download-zip-modal/download-zip-modal.component.spec.ts
+++ b/modules/ui/src/app/components/download-zip-modal/download-zip-modal.component.spec.ts
@@ -1,6 +1,15 @@
-import { ComponentFixture, TestBed } from '@angular/core/testing';
+import {
+ ComponentFixture,
+ discardPeriodicTasks,
+ fakeAsync,
+ TestBed,
+ tick,
+} from '@angular/core/testing';
-import { DownloadZipModalComponent } from './download-zip-modal.component';
+import {
+ DialogCloseAction,
+ DownloadZipModalComponent,
+} from './download-zip-modal.component';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import {
PROFILE_MOCK,
@@ -10,31 +19,56 @@ import {
import { of } from 'rxjs';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { TestRunService } from '../../services/test-run.service';
+import { Routes } from '../../model/routes';
+import { Router } from '@angular/router';
+import { FocusManagerService } from '../../services/focus-manager.service';
+import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject';
+import { RouterTestingModule } from '@angular/router/testing';
+import { Component } from '@angular/core';
describe('DownloadZipModalComponent', () => {
+ // @ts-expect-error data layer should be defined
+ window.dataLayer = window.dataLayer || [];
let component: DownloadZipModalComponent;
let fixture: ComponentFixture;
-
- const testRunServiceMock = jasmine.createSpyObj(['getRiskClass']);
+ let router: Router;
+ const testRunServiceMock = jasmine.createSpyObj('testRunServiceMock', [
+ 'getRiskClass',
+ 'downloadZip',
+ ]);
+ const focusServiceMock: jasmine.SpyObj =
+ jasmine.createSpyObj('focusServiceMock', ['focusFirstElementInContainer']);
+ const actionBehaviorSubject$ = new BehaviorSubject({
+ action: DialogCloseAction.Close,
+ });
beforeEach(async () => {
await TestBed.configureTestingModule({
- imports: [DownloadZipModalComponent, NoopAnimationsModule],
+ imports: [
+ RouterTestingModule.withRoutes([
+ { path: 'risk-assessment', component: FakeRiskAssessmentComponent },
+ ]),
+ DownloadZipModalComponent,
+ NoopAnimationsModule,
+ ],
providers: [
{
provide: MatDialogRef,
useValue: {
keydownEvents: () => of(new KeyboardEvent('keydown', { code: '' })),
close: () => ({}),
+ beforeClosed: () => actionBehaviorSubject$.asObservable(),
},
},
{
provide: MAT_DIALOG_DATA,
useValue: {
profiles: [PROFILE_MOCK_2, PROFILE_MOCK],
+ url: 'localhost:8080',
},
},
{ provide: TestRunService, useValue: testRunServiceMock },
+ { provide: FocusManagerService, useValue: focusServiceMock },
],
});
});
@@ -44,11 +78,14 @@ describe('DownloadZipModalComponent', () => {
TestBed.overrideProvider(MAT_DIALOG_DATA, {
useValue: {
profiles: [PROFILE_MOCK_2, PROFILE_MOCK, PROFILE_MOCK_3],
+ url: 'localhost:8080',
+ isPilot: true,
},
});
TestBed.compileComponents();
fixture = TestBed.createComponent(DownloadZipModalComponent);
+ router = TestBed.get(Router);
component = fixture.componentInstance;
fixture.detectChanges();
});
@@ -65,33 +102,60 @@ describe('DownloadZipModalComponent', () => {
);
});
- it('should close with null on redirect button click', async () => {
- const closeSpy = spyOn(component.dialogRef, 'close');
- const redirectLink = fixture.nativeElement.querySelector(
- '.redirect-link'
- ) as HTMLAnchorElement;
+ it('should close with Redirect action on redirect button click', fakeAsync(() => {
+ const result = {
+ action: DialogCloseAction.Redirect,
+ };
+ actionBehaviorSubject$.next(result);
+ fixture.detectChanges();
- redirectLink.click();
+ fixture.ngZone?.run(() => {
+ const closeSpy = spyOn(component.dialogRef, 'close');
+ const redirectLink = fixture.nativeElement.querySelector(
+ '.redirect-link'
+ ) as HTMLAnchorElement;
- expect(closeSpy).toHaveBeenCalledWith(null);
+ redirectLink.click();
- closeSpy.calls.reset();
- });
+ tick(2000);
- it('should close with undefined on cancel button click', async () => {
+ expect(router.url).toBe(Routes.RiskAssessment);
+ expect(
+ focusServiceMock.focusFirstElementInContainer
+ ).toHaveBeenCalled();
+ expect(closeSpy).toHaveBeenCalledWith(result);
+
+ closeSpy.calls.reset();
+ discardPeriodicTasks();
+ });
+ }));
+
+ it('should close with Close action on cancel button click', async () => {
+ const result = {
+ action: DialogCloseAction.Close,
+ };
const closeSpy = spyOn(component.dialogRef, 'close');
+ actionBehaviorSubject$.next(result);
+ fixture.detectChanges();
+
const cancelButton = fixture.nativeElement.querySelector(
'.cancel-button'
) as HTMLButtonElement;
cancelButton.click();
- expect(closeSpy).toHaveBeenCalledWith(undefined);
+ expect(closeSpy).toHaveBeenCalledWith(result);
closeSpy.calls.reset();
});
- it('should close with profile on download button click', async () => {
+ it('should close with Download action and profile on download button click', async () => {
+ const result = {
+ action: DialogCloseAction.Download,
+ profile: '',
+ };
+ actionBehaviorSubject$.next(result);
+ fixture.detectChanges();
const closeSpy = spyOn(component.dialogRef, 'close');
const downloadButton = fixture.nativeElement.querySelector(
'.download-button'
@@ -99,8 +163,32 @@ describe('DownloadZipModalComponent', () => {
downloadButton.click();
- expect(closeSpy).toHaveBeenCalledWith('');
+ expect(closeSpy).toHaveBeenCalledWith(result);
+ expect(testRunServiceMock.downloadZip).toHaveBeenCalled();
+ expect(router.url).not.toBe(Routes.RiskAssessment);
+ closeSpy.calls.reset();
+ });
+
+ it('should send GA event if report is for Pilot program', async () => {
+ const result = {
+ action: DialogCloseAction.Download,
+ profile: '',
+ };
+ actionBehaviorSubject$.next(result);
+ fixture.detectChanges();
+ const closeSpy = spyOn(component.dialogRef, 'close');
+ const downloadButton = fixture.nativeElement.querySelector(
+ '.download-button'
+ ) as HTMLButtonElement;
+
+ downloadButton.click();
+ expect(
+ // @ts-expect-error data layer should be defined
+ window.dataLayer.some(
+ (item: { event: string }) => item.event === 'pilot_download_zip'
+ )
+ ).toBeTruthy();
closeSpy.calls.reset();
});
@@ -132,11 +220,13 @@ describe('DownloadZipModalComponent', () => {
TestBed.overrideProvider(MAT_DIALOG_DATA, {
useValue: {
profiles: [],
+ url: 'localhost:8080',
},
});
TestBed.compileComponents();
fixture = TestBed.createComponent(DownloadZipModalComponent);
+ router = TestBed.get(Router);
component = fixture.componentInstance;
fixture.detectChanges();
});
@@ -147,20 +237,35 @@ describe('DownloadZipModalComponent', () => {
expect(select.classList.contains('mat-mdc-select-disabled')).toBeTruthy();
});
- it('should close with null on redirect button click', async () => {
- const closeSpy = spyOn(component.dialogRef, 'close');
- const redirectLink = fixture.nativeElement.querySelector(
- '.redirect-link'
- ) as HTMLAnchorElement;
+ it('should close with Redirect action on redirect button click', fakeAsync(() => {
+ const result = {
+ action: DialogCloseAction.Redirect,
+ };
+ actionBehaviorSubject$.next(result);
+ fixture.detectChanges();
- redirectLink.click();
+ fixture.ngZone?.run(() => {
+ const closeSpy = spyOn(component.dialogRef, 'close');
+ const redirectLink = fixture.nativeElement.querySelector(
+ '.redirect-link'
+ ) as HTMLAnchorElement;
- expect(closeSpy).toHaveBeenCalledWith(null);
+ redirectLink.click();
- closeSpy.calls.reset();
- });
+ tick(2000);
+
+ expect(router.url).toBe(Routes.RiskAssessment);
+ expect(
+ focusServiceMock.focusFirstElementInContainer
+ ).toHaveBeenCalled();
+ expect(closeSpy).toHaveBeenCalledWith(result);
- it('should close with undefined on cancel button click', async () => {
+ closeSpy.calls.reset();
+ discardPeriodicTasks();
+ });
+ }));
+
+ it('should close with Close action on cancel button click', async () => {
const closeSpy = spyOn(component.dialogRef, 'close');
const cancelButton = fixture.nativeElement.querySelector(
'.cancel-button'
@@ -168,7 +273,9 @@ describe('DownloadZipModalComponent', () => {
cancelButton.click();
- expect(closeSpy).toHaveBeenCalledWith(undefined);
+ expect(closeSpy).toHaveBeenCalledWith({
+ action: DialogCloseAction.Close,
+ });
closeSpy.calls.reset();
});
@@ -181,9 +288,18 @@ describe('DownloadZipModalComponent', () => {
downloadButton.click();
- expect(closeSpy).toHaveBeenCalledWith('');
+ expect(closeSpy).toHaveBeenCalledWith({
+ action: DialogCloseAction.Download,
+ profile: '',
+ });
closeSpy.calls.reset();
});
});
});
+
+@Component({
+ selector: 'app-fake-risk-assessment-component',
+ template: '',
+})
+class FakeRiskAssessmentComponent {}
diff --git a/modules/ui/src/app/components/download-zip-modal/download-zip-modal.component.ts b/modules/ui/src/app/components/download-zip-modal/download-zip-modal.component.ts
index a703edb6d..199d89bf5 100644
--- a/modules/ui/src/app/components/download-zip-modal/download-zip-modal.component.ts
+++ b/modules/ui/src/app/components/download-zip-modal/download-zip-modal.component.ts
@@ -1,4 +1,10 @@
-import { ChangeDetectionStrategy, Component, Inject } from '@angular/core';
+import {
+ ChangeDetectionStrategy,
+ Component,
+ Inject,
+ OnDestroy,
+ OnInit,
+} from '@angular/core';
import {
MAT_DIALOG_DATA,
MatDialogActions,
@@ -18,14 +24,29 @@ import { MatSelectModule } from '@angular/material/select';
import { MatOptionModule } from '@angular/material/core';
import { TestRunService } from '../../services/test-run.service';
import { Routes } from '../../model/routes';
-import { RouterLink } from '@angular/router';
+import { Router, RouterLink } from '@angular/router';
import { TestrunStatus, StatusOfTestrun } from '../../model/testrun-status';
import { DownloadReportComponent } from '../download-report/download-report.component';
+import { Subject, takeUntil, timer } from 'rxjs';
+import { FocusManagerService } from '../../services/focus-manager.service';
interface DialogData {
profiles: Profile[];
testrunStatus?: TestrunStatus;
isTestingComplete?: boolean;
+ url: string | null;
+ isPilot?: boolean;
+}
+
+export enum DialogCloseAction {
+ Close,
+ Redirect,
+ Download,
+}
+
+export interface DialogCloseResult {
+ action: DialogCloseAction;
+ profile: string | null | undefined;
}
@Component({
@@ -48,7 +69,11 @@ interface DialogData {
styleUrl: './download-zip-modal.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush,
})
-export class DownloadZipModalComponent extends EscapableDialogComponent {
+export class DownloadZipModalComponent
+ extends EscapableDialogComponent
+ implements OnDestroy, OnInit
+{
+ private destroy$: Subject = new Subject();
readonly NO_PROFILE = {
name: 'No Risk Profile selected',
questions: [],
@@ -60,7 +85,9 @@ export class DownloadZipModalComponent extends EscapableDialogComponent {
constructor(
private readonly testRunService: TestRunService,
public override dialogRef: MatDialogRef,
- @Inject(MAT_DIALOG_DATA) public data: DialogData
+ @Inject(MAT_DIALOG_DATA) public data: DialogData,
+ private route: Router,
+ private focusManagerService: FocusManagerService
) {
super(dialogRef);
this.profiles = data.profiles.filter(
@@ -75,19 +102,70 @@ export class DownloadZipModalComponent extends EscapableDialogComponent {
this.selectedProfile = this.profiles[0];
}
+ ngOnInit() {
+ this.dialogRef
+ ?.beforeClosed()
+ .pipe(takeUntil(this.destroy$))
+ .subscribe((result: DialogCloseResult) => {
+ if (result.action === DialogCloseAction.Close) {
+ return;
+ }
+ if (result.action === DialogCloseAction.Redirect) {
+ this.route.navigate([Routes.RiskAssessment]).then(() =>
+ timer(1000).subscribe(() => {
+ this.focusManagerService.focusFirstElementInContainer();
+ })
+ );
+ return;
+ }
+ if (this.data.url != null && typeof result.profile === 'string') {
+ this.testRunService.downloadZip(
+ this.getZipLink(this.data.url),
+ result.profile
+ );
+ if (this.data.isPilot) {
+ // @ts-expect-error data layer is not null
+ window.dataLayer.push({
+ event: 'pilot_download_zip',
+ });
+ }
+ }
+ });
+ }
+
+ ngOnDestroy() {
+ this.destroy$.next(true);
+ this.destroy$.unsubscribe();
+ }
+
cancel(profile?: Profile | null) {
if (profile === null) {
- this.dialogRef.close(null);
+ this.dialogRef.close({
+ action: DialogCloseAction.Redirect,
+ } as DialogCloseResult);
+ return;
+ }
+ if (!profile) {
+ this.dialogRef.close({
+ action: DialogCloseAction.Close,
+ } as DialogCloseResult);
return;
}
- let value = profile?.name;
- if (profile && profile?.name === this.NO_PROFILE.name) {
+ let value = profile.name;
+ if (value === this.NO_PROFILE.name) {
value = '';
}
- this.dialogRef.close(value);
+ this.dialogRef.close({
+ action: DialogCloseAction.Download,
+ profile: value,
+ } as DialogCloseResult);
}
public getRiskClass(riskResult: string): RiskResultClassName {
return this.testRunService.getRiskClass(riskResult);
}
+
+ private getZipLink(reportURL: string): string {
+ return reportURL.replace('report', 'export');
+ }
}
diff --git a/modules/ui/src/app/components/dynamic-form/dynamic-form.component.html b/modules/ui/src/app/components/dynamic-form/dynamic-form.component.html
index 0ea7d3df4..cdbf27695 100644
--- a/modules/ui/src/app/components/dynamic-form/dynamic-form.component.html
+++ b/modules/ui/src/app/components/dynamic-form/dynamic-form.component.html
@@ -226,7 +226,7 @@
- {{ getOptionValue(option) }}
+
{{
@@ -250,10 +250,13 @@
aria-label="{{ label }}"
id="{{ formControlName }}-group"
[formControlName]="formControlName">
+
+ {{ getControl(formControlName).value }}
+
- {{ getOptionValue(option) }}
+ [value]="getOptionValue(option)"
+ [innerHTML]="getSanitizedOptionValue(option)">
{{
diff --git a/modules/ui/src/app/components/dynamic-form/dynamic-form.component.scss b/modules/ui/src/app/components/dynamic-form/dynamic-form.component.scss
index 14f7086f0..7fda3a91a 100644
--- a/modules/ui/src/app/components/dynamic-form/dynamic-form.component.scss
+++ b/modules/ui/src/app/components/dynamic-form/dynamic-form.component.scss
@@ -17,7 +17,7 @@
@import 'src/theming/colors';
@import 'src/theming/variables';
-::ng-deep .field-label {
+.field-label {
margin: 0;
color: $grey-800;
font-size: 18px;
diff --git a/modules/ui/src/app/components/dynamic-form/dynamic-form.component.ts b/modules/ui/src/app/components/dynamic-form/dynamic-form.component.ts
index 3d6b13016..d3e1fa3da 100644
--- a/modules/ui/src/app/components/dynamic-form/dynamic-form.component.ts
+++ b/modules/ui/src/app/components/dynamic-form/dynamic-form.component.ts
@@ -13,7 +13,13 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import { Component, inject, Input, OnInit } from '@angular/core';
+import {
+ Component,
+ inject,
+ Input,
+ OnInit,
+ ViewEncapsulation,
+} from '@angular/core';
import {
FormControlType,
OptionType,
@@ -44,6 +50,7 @@ import { MatCheckboxModule } from '@angular/material/checkbox';
import { TextFieldModule } from '@angular/cdk/text-field';
import { DeviceValidators } from '../../pages/devices/components/device-form/device.validators';
import { ProfileValidators } from '../../pages/risk-assessment/profile-form/profile.validators';
+import { DomSanitizer } from '@angular/platform-browser';
@Component({
selector: 'app-dynamic-form',
standalone: true,
@@ -68,6 +75,7 @@ import { ProfileValidators } from '../../pages/risk-assessment/profile-form/prof
],
templateUrl: './dynamic-form.component.html',
styleUrl: './dynamic-form.component.scss',
+ encapsulation: ViewEncapsulation.None,
})
export class DynamicFormComponent implements OnInit {
public readonly FormControlType = FormControlType;
@@ -83,7 +91,8 @@ export class DynamicFormComponent implements OnInit {
constructor(
private fb: FormBuilder,
private deviceValidators: DeviceValidators,
- private profileValidators: ProfileValidators
+ private profileValidators: ProfileValidators,
+ private domSanitizer: DomSanitizer
) {}
getControl(name: string | number) {
return this.formGroup.get(name.toString()) as AbstractControl;
@@ -168,4 +177,10 @@ export class DynamicFormComponent implements OnInit {
}
return option;
}
+
+ getSanitizedOptionValue(option: OptionType) {
+ return this.domSanitizer.bypassSecurityTrustHtml(
+ this.getOptionValue(option)
+ );
+ }
}
diff --git a/modules/ui/src/app/components/program-type-icon/program-type-icon.component.ts b/modules/ui/src/app/components/program-type-icon/program-type-icon.component.ts
index ccf5b3335..45a34ddfc 100644
--- a/modules/ui/src/app/components/program-type-icon/program-type-icon.component.ts
+++ b/modules/ui/src/app/components/program-type-icon/program-type-icon.component.ts
@@ -28,6 +28,7 @@ import { MatIcon } from '@angular/material/icon';
padding-right: 4px;
}
.icon {
+ display: flex;
width: 16px;
height: 16px;
line-height: 16px;
diff --git a/modules/ui/src/app/components/report-action/report-action.component.spec.ts b/modules/ui/src/app/components/report-action/report-action.component.spec.ts
index b4d69b149..7408de89d 100644
--- a/modules/ui/src/app/components/report-action/report-action.component.spec.ts
+++ b/modules/ui/src/app/components/report-action/report-action.component.spec.ts
@@ -2,6 +2,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ReportActionComponent } from './report-action.component';
import { MOCK_PROGRESS_DATA_COMPLIANT } from '../../mocks/testrun.mock';
+import { TestrunStatus } from '../../model/testrun-status';
describe('ReportActionComponent', () => {
let component: ReportActionComponent;
@@ -21,12 +22,25 @@ describe('ReportActionComponent', () => {
expect(component).toBeTruthy();
});
- it('#getTestRunId should return data for title of link', () => {
- const expectedResult = 'Delta 03-DIN-CPU 1.2.2 22 Jun 2023 9:20';
+ describe('#getTestRunId', () => {
+ it('should return data for title of link', () => {
+ const expectedResult = 'Delta 03-DIN-CPU 1.2.2 22 Jun 2023 9:20';
- const result = component.getTestRunId(MOCK_PROGRESS_DATA_COMPLIANT);
+ const result = component.getTestRunId(MOCK_PROGRESS_DATA_COMPLIANT);
- expect(result).toEqual(expectedResult);
+ expect(result).toEqual(expectedResult);
+ });
+
+ it('should return title as empty string if no device data', () => {
+ const MOCK_DATA_WITHOUT_DEVICE = {
+ ...MOCK_PROGRESS_DATA_COMPLIANT,
+ device: undefined as unknown,
+ } as TestrunStatus;
+
+ const result = component.getTestRunId(MOCK_DATA_WITHOUT_DEVICE);
+
+ expect(result).toEqual('');
+ });
});
it('#getFormattedDateString should return date as string in the format "d MMM y H:mm"', () => {
diff --git a/modules/ui/src/app/components/testing-complete/testing-complete.component.spec.ts b/modules/ui/src/app/components/testing-complete/testing-complete.component.spec.ts
index 30c3108e6..494b58823 100644
--- a/modules/ui/src/app/components/testing-complete/testing-complete.component.spec.ts
+++ b/modules/ui/src/app/components/testing-complete/testing-complete.component.spec.ts
@@ -6,25 +6,25 @@ import {
} from '@angular/core/testing';
import { TestingCompleteComponent } from './testing-complete.component';
-import { TestRunService } from '../../services/test-run.service';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
-import { Router } from '@angular/router';
import { RouterTestingModule } from '@angular/router/testing';
import { Component } from '@angular/core';
import { MOCK_PROGRESS_DATA_COMPLIANT } from '../../mocks/testrun.mock';
import { of } from 'rxjs';
import { MatDialogRef } from '@angular/material/dialog';
-import { DownloadZipModalComponent } from '../download-zip-modal/download-zip-modal.component';
-import { Routes } from '../../model/routes';
+import {
+ DialogCloseAction,
+ DownloadZipModalComponent,
+} from '../download-zip-modal/download-zip-modal.component';
+import { FocusManagerService } from '../../services/focus-manager.service';
describe('TestingCompleteComponent', () => {
let component: TestingCompleteComponent;
let fixture: ComponentFixture;
- let router: Router;
-
- const testrunServiceMock: jasmine.SpyObj =
- jasmine.createSpyObj('testrunServiceMock', ['downloadZip']);
-
+ const mockFocusManagerService = jasmine.createSpyObj(
+ 'mockFocusManagerService',
+ ['focusFirstElementInContainer']
+ );
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [
@@ -34,12 +34,13 @@ describe('TestingCompleteComponent', () => {
TestingCompleteComponent,
BrowserAnimationsModule,
],
- providers: [{ provide: TestRunService, useValue: testrunServiceMock }],
+ providers: [
+ { provide: FocusManagerService, useValue: mockFocusManagerService },
+ ],
}).compileComponents();
fixture = TestBed.createComponent(TestingCompleteComponent);
component = fixture.componentInstance;
- router = TestBed.get(Router);
component.data = MOCK_PROGRESS_DATA_COMPLIANT;
fixture.detectChanges();
});
@@ -49,16 +50,13 @@ describe('TestingCompleteComponent', () => {
});
describe('#onInit', () => {
- beforeEach(() => {
- testrunServiceMock.downloadZip.calls.reset();
- });
-
- it('should call downloadZip on service if profile is a string', fakeAsync(() => {
+ it('should focus first element in container when dialog closes with Close action', fakeAsync(() => {
const openSpy = spyOn(component.dialog, 'open').and.returnValue({
- afterClosed: () => of(''),
+ afterClosed: () => of({ action: DialogCloseAction.Close }),
} as MatDialogRef);
component.ngOnInit();
+
tick(1000);
expect(openSpy).toHaveBeenCalledWith(DownloadZipModalComponent, {
@@ -67,6 +65,8 @@ describe('TestingCompleteComponent', () => {
profiles: [],
testrunStatus: MOCK_PROGRESS_DATA_COMPLIANT,
isTestingComplete: true,
+ url: 'https://api.testrun.io/report.pdf',
+ isPilot: false,
},
autoFocus: 'first-tabbable',
ariaDescribedBy: 'testing-result-main-info',
@@ -75,10 +75,11 @@ describe('TestingCompleteComponent', () => {
panelClass: 'initiate-test-run-dialog',
});
- tick();
+ tick(1000);
- expect(testrunServiceMock.downloadZip).toHaveBeenCalled();
- expect(router.url).not.toBe(Routes.RiskAssessment);
+ expect(
+ mockFocusManagerService.focusFirstElementInContainer
+ ).toHaveBeenCalled();
openSpy.calls.reset();
}));
});
diff --git a/modules/ui/src/app/components/testing-complete/testing-complete.component.ts b/modules/ui/src/app/components/testing-complete/testing-complete.component.ts
index 28da57bee..79fc46c79 100644
--- a/modules/ui/src/app/components/testing-complete/testing-complete.component.ts
+++ b/modules/ui/src/app/components/testing-complete/testing-complete.component.ts
@@ -7,13 +7,15 @@ import {
} from '@angular/core';
import { Subject, takeUntil, timer } from 'rxjs';
import { MatDialog } from '@angular/material/dialog';
-import { TestRunService } from '../../services/test-run.service';
-import { Router } from '@angular/router';
-import { DownloadZipModalComponent } from '../download-zip-modal/download-zip-modal.component';
-import { Routes } from '../../model/routes';
+import {
+ DialogCloseAction,
+ DialogCloseResult,
+ DownloadZipModalComponent,
+} from '../download-zip-modal/download-zip-modal.component';
import { Profile } from '../../model/profile';
import { TestrunStatus } from '../../model/testrun-status';
import { FocusManagerService } from '../../services/focus-manager.service';
+import { TestingType } from '../../model/device';
@Component({
selector: 'app-testing-complete',
@@ -29,15 +31,15 @@ export class TestingCompleteComponent implements OnDestroy, OnInit {
constructor(
public dialog: MatDialog,
- private testrunService: TestRunService,
- private route: Router,
private focusManagerService: FocusManagerService
) {}
ngOnInit() {
- timer(1000).subscribe(() => {
- this.openTestingCompleteModal();
- });
+ timer(1000)
+ .pipe(takeUntil(this.destroy$))
+ .subscribe(() => {
+ this.openTestingCompleteModal();
+ });
}
ngOnDestroy() {
this.destroy$.next(true);
@@ -51,6 +53,8 @@ export class TestingCompleteComponent implements OnDestroy, OnInit {
profiles: this.profiles,
testrunStatus: this.data,
isTestingComplete: true,
+ url: this.data?.report,
+ isPilot: this.data?.device.test_pack === TestingType.Pilot,
},
autoFocus: 'first-tabbable',
ariaDescribedBy: 'testing-result-main-info',
@@ -62,29 +66,14 @@ export class TestingCompleteComponent implements OnDestroy, OnInit {
dialogRef
?.afterClosed()
.pipe(takeUntil(this.destroy$))
- .subscribe(profile => {
- if (profile === undefined) {
- // close modal
+ .subscribe((result: DialogCloseResult) => {
+ if (result.action === DialogCloseAction.Close) {
this.focusFirstElement();
return;
}
- if (profile === null) {
- this.navigateToRiskAssessment();
- } else if (this.data?.report != null) {
- this.testrunService.downloadZip(
- this.getZipLink(this.data?.report),
- profile
- );
- }
});
}
- private navigateToRiskAssessment(): void {
- this.route.navigate([Routes.RiskAssessment]).then(() => {
- this.focusFirstElement();
- });
- }
-
private focusFirstElement() {
timer(1000)
.pipe(takeUntil(this.destroy$))
@@ -92,8 +81,4 @@ export class TestingCompleteComponent implements OnDestroy, OnInit {
this.focusManagerService.focusFirstElementInContainer();
});
}
-
- private getZipLink(reportURL: string): string {
- return reportURL.replace('report', 'export');
- }
}
diff --git a/modules/ui/src/app/mocks/settings.mock.ts b/modules/ui/src/app/mocks/settings.mock.ts
index 49a11a895..53f092a0b 100644
--- a/modules/ui/src/app/mocks/settings.mock.ts
+++ b/modules/ui/src/app/mocks/settings.mock.ts
@@ -33,16 +33,21 @@ export const MOCK_SYSTEM_CONFIG_WITH_DATA: SystemConfig = {
monitor_period: 600,
};
-export const MOCK_INTERFACES: SystemInterfaces = {
- mockDeviceKey: 'mockDeviceValue',
- mockInternetKey: 'mockInternetValue',
+export const MOCK_SYSTEM_CONFIG_WITH_SINGLE_PORT: SystemConfig = {
+ network: {
+ device_intf: 'mockDeviceKey',
+ internet_intf: '',
+ },
+ log_level: 'DEBUG',
+ monitor_period: 600,
+ single_intf: true,
};
-export const MOCK_INTERNET_OPTIONS: SystemInterfaces = {
- '': 'Not specified',
+export const MOCK_INTERFACES: SystemInterfaces = {
mockDeviceKey: 'mockDeviceValue',
mockInternetKey: 'mockInternetValue',
};
+
export const MOCK_DEVICE_VALUE: SystemInterfaces = {
key: 'mockDeviceKey',
value: 'mockDeviceValue',
diff --git a/modules/ui/src/app/mocks/testrun.mock.ts b/modules/ui/src/app/mocks/testrun.mock.ts
index c90927cd3..3decf9973 100644
--- a/modules/ui/src/app/mocks/testrun.mock.ts
+++ b/modules/ui/src/app/mocks/testrun.mock.ts
@@ -159,6 +159,12 @@ export const MOCK_PROGRESS_DATA_WAITING_FOR_DEVICE: TestrunStatus = {
started: null,
};
+export const MOCK_PROGRESS_DATA_VALIDATING: TestrunStatus = {
+ ...MOCK_PROGRESS_DATA_IN_PROGRESS,
+ status: StatusOfTestrun.Validating,
+ started: null,
+};
+
export const MOCK_PROGRESS_DATA_WITH_ERROR: TestrunStatus =
PROGRESS_DATA_RESPONSE(StatusOfTestrun.InProgress, null, {
...TEST_DATA,
diff --git a/modules/ui/src/app/model/testrun-status.ts b/modules/ui/src/app/model/testrun-status.ts
index faa707f92..f14dce652 100644
--- a/modules/ui/src/app/model/testrun-status.ts
+++ b/modules/ui/src/app/model/testrun-status.ts
@@ -66,6 +66,7 @@ export enum StatusOfTestrun {
Idle = 'Idle',
Monitoring = 'Monitoring',
Error = 'Error',
+ Validating = 'Validating Network',
}
export enum StatusOfTestResult {
diff --git a/modules/ui/src/app/pages/devices/components/device-qualification-from/device-qualification-from.component.scss b/modules/ui/src/app/pages/devices/components/device-qualification-from/device-qualification-from.component.scss
index 3ce6ee85b..c2088c67b 100644
--- a/modules/ui/src/app/pages/devices/components/device-qualification-from/device-qualification-from.component.scss
+++ b/modules/ui/src/app/pages/devices/components/device-qualification-from/device-qualification-from.component.scss
@@ -52,10 +52,6 @@ $form-min-width: 732px;
color: $primary;
}
-.device-form-mac-address-error {
- white-space: nowrap;
-}
-
.hidden {
display: none;
}
@@ -306,10 +302,9 @@ $form-min-width: 732px;
}
:host mat-form-field {
- &::ng-deep.mat-mdc-form-field-subscript-wrapper:has(
- mat-error.error-multiline
- ) {
- height: 46px;
+ &::ng-deep.mat-mdc-form-field-error-wrapper {
+ margin-top: -20px;
+ position: static;
}
}
diff --git a/modules/ui/src/app/pages/devices/components/device-qualification-from/device-qualification-from.component.spec.ts b/modules/ui/src/app/pages/devices/components/device-qualification-from/device-qualification-from.component.spec.ts
index e1b59025b..bcc34b35d 100644
--- a/modules/ui/src/app/pages/devices/components/device-qualification-from/device-qualification-from.component.spec.ts
+++ b/modules/ui/src/app/pages/devices/components/device-qualification-from/device-qualification-from.component.spec.ts
@@ -17,6 +17,7 @@ import {
ComponentFixture,
discardPeriodicTasks,
fakeAsync,
+ flush,
TestBed,
tick,
} from '@angular/core/testing';
@@ -50,17 +51,50 @@ import { DeviceStatus, TestingType } from '../../../../model/device';
import { Component, Input } from '@angular/core';
import { QuestionFormat } from '../../../../model/question';
import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject';
+import { selectDevices } from '../../../../store/selectors';
describe('DeviceQualificationFromComponent', () => {
let component: DeviceQualificationFromComponent;
let fixture: ComponentFixture;
let compiled: HTMLElement;
const testrunServiceMock: jasmine.SpyObj =
- jasmine.createSpyObj('testrunServiceMock', ['fetchQuestionnaireFormat']);
+ jasmine.createSpyObj('testrunServiceMock', [
+ 'fetchQuestionnaireFormat',
+ 'saveDevice',
+ ]);
const keyboardEvent = new BehaviorSubject(
new KeyboardEvent('keydown', { code: '' })
);
+ const MOCK_DEVICE = {
+ status: DeviceStatus.VALID,
+ manufacturer: '',
+ model: '',
+ mac_addr: '',
+ test_pack: TestingType.Qualification,
+ type: '',
+ technology: '',
+ test_modules: {
+ udmi: {
+ enabled: true,
+ },
+ connection: {
+ enabled: true,
+ },
+ },
+ additional_info: [
+ { question: 'What type of device is this?', answer: '' },
+ {
+ question: 'Does your device process any sensitive information? ',
+ answer: '',
+ },
+ {
+ question: 'Please select the technology this device falls into',
+ answer: '',
+ },
+ ],
+ };
+
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [FakeDynamicFormComponent],
@@ -90,7 +124,9 @@ describe('DeviceQualificationFromComponent', () => {
{ provide: MAT_DIALOG_DATA, useValue: {} },
{ provide: TestRunService, useValue: testrunServiceMock },
provideNgxMask(),
- provideMockStore({}),
+ provideMockStore({
+ selectors: [{ selector: selectDevices, value: [device, device] }],
+ }),
],
}).compileComponents();
@@ -107,6 +143,8 @@ describe('DeviceQualificationFromComponent', () => {
testrunServiceMock.fetchQuestionnaireFormat.and.returnValue(
of(DEVICES_FORM)
);
+
+ testrunServiceMock.saveDevice.and.returnValue(of(true));
});
it('should create', () => {
@@ -160,6 +198,92 @@ describe('DeviceQualificationFromComponent', () => {
closeSpy.calls.reset();
}));
+ it('should close dialog on submit with "Save" action', fakeAsync(() => {
+ component.device = MOCK_DEVICE;
+ const closeSpy = spyOn(component.dialogRef, 'close');
+ fixture.detectChanges();
+
+ component.submit();
+ tick();
+ flush();
+
+ expect(closeSpy).toHaveBeenCalledWith({
+ action: 'Save',
+ device: MOCK_DEVICE,
+ });
+
+ closeSpy.calls.reset();
+ }));
+
+ it('should close dialog on delete with "Delete" action', fakeAsync(() => {
+ const closeSpy = spyOn(component.dialogRef, 'close');
+ fixture.detectChanges();
+
+ component.delete();
+ tick();
+
+ expect(closeSpy).toHaveBeenCalledWith({
+ action: 'Delete',
+ device: MOCK_DEVICE,
+ index: 0,
+ });
+
+ closeSpy.calls.reset();
+ }));
+
+ describe('#deviceHasNoChanges', () => {
+ const deviceProps = [
+ { manufacturer: 'test' },
+ { model: 'test' },
+ { mac_addr: 'test' },
+ { test_pack: TestingType.Pilot },
+ { type: 'test' },
+ { technology: 'test' },
+ {
+ test_modules: {
+ udmi: {
+ enabled: false,
+ },
+ },
+ },
+ { additional_info: undefined },
+ {
+ additional_info: [
+ { question: 'What type of device is this?', answer: 'test' },
+ ],
+ },
+ ];
+ it('should return true if devices the same', () => {
+ const result = component.deviceHasNoChanges(MOCK_DEVICE, MOCK_DEVICE);
+
+ expect(result).toBeTrue();
+ });
+
+ deviceProps.forEach(item => {
+ it(`should return false if devices have different props`, () => {
+ const MOCK_DEVICE_1 = { ...MOCK_DEVICE, ...item };
+ const result = component.deviceHasNoChanges(MOCK_DEVICE_1, MOCK_DEVICE);
+
+ expect(result).toBeFalse();
+ });
+ });
+ });
+
+ it('should trigger onResize method when window is resized ', () => {
+ fixture.detectChanges();
+ const spyOnResize = spyOn(component, 'onResize');
+ window.dispatchEvent(new Event('resize'));
+ fixture.detectChanges();
+ expect(spyOnResize).toHaveBeenCalled();
+ });
+
+ it('#goToStep should set selected index', () => {
+ fixture.detectChanges();
+ component.goToStep(0);
+
+ expect(component.stepper.selectedIndex).toBe(0);
+ });
+
it('should close dialog on "cancel" click', () => {
fixture.detectChanges();
component.manufacturer.setValue('test');
diff --git a/modules/ui/src/app/pages/devices/components/device-qualification-from/device-qualification-from.component.ts b/modules/ui/src/app/pages/devices/components/device-qualification-from/device-qualification-from.component.ts
index 67eb39d4c..09edf8cdc 100644
--- a/modules/ui/src/app/pages/devices/components/device-qualification-from/device-qualification-from.component.ts
+++ b/modules/ui/src/app/pages/devices/components/device-qualification-from/device-qualification-from.component.ts
@@ -501,7 +501,7 @@ export class DeviceQualificationFromComponent
for (const key of keys1) {
const val1 = device1.test_modules![key];
const val2 = device2.test_modules![key];
- if (val1.enabled !== val2.enabled) {
+ if (val1?.enabled !== val2?.enabled) {
return false;
}
}
diff --git a/modules/ui/src/app/pages/reports/components/delete-report/delete-report.component.spec.ts b/modules/ui/src/app/pages/reports/components/delete-report/delete-report.component.spec.ts
index f85babe34..f7271c377 100644
--- a/modules/ui/src/app/pages/reports/components/delete-report/delete-report.component.spec.ts
+++ b/modules/ui/src/app/pages/reports/components/delete-report/delete-report.component.spec.ts
@@ -51,13 +51,27 @@ describe('DeleteReportComponent', () => {
it('#deleteReport should open delete dialog', () => {
const deviceRemovedSpy = spyOn(component.removeDevice, 'emit');
- spyOn(component.dialog, 'open').and.returnValue({
+ const openSpy = spyOn(component.dialog, 'open').and.returnValue({
afterClosed: () => of(true),
} as MatDialogRef);
component.deleteReport(new Event('click'));
expect(deviceRemovedSpy).toHaveBeenCalled();
+
+ expect(openSpy).toHaveBeenCalledWith(SimpleDialogComponent, {
+ ariaLabel: 'Delete report',
+ data: {
+ title: 'Delete report?',
+ content:
+ 'You are about to delete Delta 03-DIN-CPU 1.2.2 22 Jun 2023 9:20. Are you sure?',
+ },
+ autoFocus: true,
+ hasBackdrop: true,
+ disableClose: true,
+ panelClass: 'simple-dialog',
+ });
+ openSpy.calls.reset();
});
});
diff --git a/modules/ui/src/app/pages/reports/components/delete-report/delete-report.component.ts b/modules/ui/src/app/pages/reports/components/delete-report/delete-report.component.ts
index 74abb27d1..96e178443 100644
--- a/modules/ui/src/app/pages/reports/components/delete-report/delete-report.component.ts
+++ b/modules/ui/src/app/pages/reports/components/delete-report/delete-report.component.ts
@@ -62,7 +62,7 @@ export class DeleteReportComponent
ariaLabel: 'Delete report',
data: {
title: 'Delete report?',
- content: this.getTestRunId(this.data),
+ content: `You are about to delete ${this.getTestRunId(this.data)}. Are you sure?`,
},
autoFocus: true,
hasBackdrop: true,
diff --git a/modules/ui/src/app/pages/reports/components/filter-chips/filter-chips.component.spec.ts b/modules/ui/src/app/pages/reports/components/filter-chips/filter-chips.component.spec.ts
index 34cb9459f..508bb7c54 100644
--- a/modules/ui/src/app/pages/reports/components/filter-chips/filter-chips.component.spec.ts
+++ b/modules/ui/src/app/pages/reports/components/filter-chips/filter-chips.component.spec.ts
@@ -36,6 +36,40 @@ describe('FilterChipsComponent', () => {
expect(component).toBeTruthy();
});
+ describe('#clearFilter', () => {
+ const MOCK_FILTERS = {
+ deviceInfo: 'Delta',
+ deviceFirmware: '03',
+ results: ['Compliant'],
+ dateRange: { start: '10/2/2024', end: '11/2/2024' },
+ };
+
+ beforeEach(() => {
+ component.filters = MOCK_FILTERS;
+ });
+
+ it(`should clear deviceFirmware filter`, () => {
+ const result = { ...MOCK_FILTERS, deviceFirmware: '' };
+ component.clearFilter('deviceFirmware');
+
+ expect(component.filters).toEqual(result);
+ });
+
+ it(`should clear results filter`, () => {
+ const clearedFilters = { ...MOCK_FILTERS, results: [] };
+ component.clearFilter('results');
+
+ expect(component.filters).toEqual(clearedFilters);
+ });
+
+ it(`should clear dateRange filter`, () => {
+ const clearedFilters = { ...MOCK_FILTERS, dateRange: '' };
+ component.clearFilter('dateRange');
+
+ expect(component.filters).toEqual(clearedFilters);
+ });
+ });
+
describe('DOM tests', () => {
describe('"Clear all filters" button', () => {
it('should exist', () => {
diff --git a/modules/ui/src/app/pages/risk-assessment/profile-form/profile-form.component.html b/modules/ui/src/app/pages/risk-assessment/profile-form/profile-form.component.html
index dc1b0a330..8bde474ba 100644
--- a/modules/ui/src/app/pages/risk-assessment/profile-form/profile-form.component.html
+++ b/modules/ui/src/app/pages/risk-assessment/profile-form/profile-form.component.html
@@ -36,14 +36,12 @@
The Profile name is required
- This Profile name is already used for another Risk Assessment
- profile
+ This Profile name is already used for another profile
-
+
+
-
+
| Source |
diff --git a/testing/unit/report/report_compliant.json b/testing/unit/report/report_compliant.json
index 17e994d20..6b84a8d39 100644
--- a/testing/unit/report/report_compliant.json
+++ b/testing/unit/report/report_compliant.json
@@ -4,6 +4,9 @@
"manufacturer": "Testrun",
"model": "Faux",
"firmware": "1.0.0",
+ "type": "Controller - FCU",
+ "technology": "Hardware - Fitness",
+ "test_pack": "Device Qualification",
"test_modules": {
"connection": {
"enabled": true
@@ -23,7 +26,33 @@
"protocol": {
"enabled": true
}
- }
+ },
+ "additional_info": [
+ {
+ "question": "What type of device is this?",
+ "answer": "Controller - FCU"
+ },
+ {
+ "question": "Please select the technology this device falls into",
+ "answer": "Hardware - Fitness"
+ },
+ {
+ "question": "Does your device process any sensitive information? ",
+ "answer": "No"
+ },
+ {
+ "question": "Can all non-essential services be disabled on your device?",
+ "answer": "Yes"
+ },
+ {
+ "question": "Is there a second IP port on the device?",
+ "answer": "No"
+ },
+ {
+ "question": "Can the second IP port on your device be disabled?",
+ "answer": "N/A"
+ }
+ ]
},
"status": "Compliant",
"started": "2024-04-10 21:21:47",
@@ -68,7 +97,7 @@
},
{
"name": "connection.switch.arp_inspection",
- "description": "Device uses ARP",
+ "description": "Device uses ARP correctly",
"expected_behavior": "Device continues to operate correctly when ARP inspection is enabled on the switch. No functionality is lost with ARP inspection enabled.",
"required_result": "Required",
"result": "Compliant"
diff --git a/testing/unit/report/report_noncompliant.json b/testing/unit/report/report_noncompliant.json
index 98fbeb284..6619bba19 100644
--- a/testing/unit/report/report_noncompliant.json
+++ b/testing/unit/report/report_noncompliant.json
@@ -4,6 +4,9 @@
"manufacturer": "Testrun",
"model": "Faux",
"firmware": "1.0.0",
+ "type": "Controller - FCU",
+ "technology": "Hardware - Fitness",
+ "test_pack": "Device Qualification",
"test_modules": {
"connection": {
"enabled": true
@@ -23,7 +26,33 @@
"protocol": {
"enabled": true
}
- }
+ },
+ "additional_info": [
+ {
+ "question": "What type of device is this?",
+ "answer": "Controller - FCU"
+ },
+ {
+ "question": "Please select the technology this device falls into",
+ "answer": "Hardware - Fitness"
+ },
+ {
+ "question": "Does your device process any sensitive information? ",
+ "answer": "No"
+ },
+ {
+ "question": "Can all non-essential services be disabled on your device?",
+ "answer": "Yes"
+ },
+ {
+ "question": "Is there a second IP port on the device?",
+ "answer": "No"
+ },
+ {
+ "question": "Can the second IP port on your device be disabled?",
+ "answer": "N/A"
+ }
+ ]
},
"status": "Non-Compliant",
"started": "2024-04-10 21:21:47",
@@ -77,7 +106,7 @@
},
{
"name": "connection.switch.arp_inspection",
- "description": "Device uses ARP",
+ "description": "Device uses ARP correctly",
"expected_behavior": "Device continues to operate correctly when ARP inspection is enabled on the switch. No functionality is lost with ARP inspection enabled.",
"required_result": "Required",
"result": "Compliant"
diff --git a/testing/unit/report/report_test.py b/testing/unit/report/report_test.py
index e5c8b61a5..f706059b6 100644
--- a/testing/unit/report/report_test.py
+++ b/testing/unit/report/report_test.py
@@ -61,6 +61,7 @@ def create_report(self, results_file_path):
# Load each module html report
reports_md = []
+ reports_md.append(self.get_module_html_report('tls'))
reports_md.append(self.get_module_html_report('dns'))
reports_md.append(self.get_module_html_report('services'))
reports_md.append(self.get_module_html_report('ntp'))
@@ -70,12 +71,16 @@ def create_report(self, results_file_path):
# Create the HTML filename based on the JSON name
file_name = os.path.splitext(os.path.basename(results_file_path))[0]
- report_out_file = os.path.join(OUTPUT_DIR, file_name + '.html')
+ report_html_file = os.path.join(OUTPUT_DIR, file_name + '.html')
+ report_pdf_file = os.path.join(OUTPUT_DIR, file_name + '.pdf')
# Save report as HTML file
- with open(report_out_file, 'w', encoding='utf-8') as file:
+ with open(report_html_file, 'w', encoding='utf-8') as file:
file.write(report.to_html())
+ with open(report_pdf_file, 'wb') as file:
+ file.write(report.to_pdf().getvalue())
+
def report_compliant_test(self):
"""Generate a report for the compliant test"""
diff --git a/testing/unit/run_test_module.sh b/testing/unit/run_test_module.sh
old mode 100644
new mode 100755
index 37516ee72..8e31e6860
--- a/testing/unit/run_test_module.sh
+++ b/testing/unit/run_test_module.sh
@@ -15,6 +15,10 @@
# limitations under the License.
# Must be run from the root directory of Testrun
+
+# Read the JSON file into a variable
+DEVICE_TEST_PACK=$(TLS Module
+
+
+
+
+
+ | Expiry |
+ Length |
+ Type |
+ Port number |
+ Signed by |
+
+
+
+
+
+ | 2027-07-25 15:33:09 |
+ 888 |
+ EC |
+ 443 |
+ Sub CA |
+
+
+
+
+
+
+
Certificate Information
+
+
+
+
+ | Property |
+ Value |
+
+
+
+
+ | Version |
+ 3 (0x2) |
+
+
+ | Signature Alg. |
+ sha256WithRSAEncryption |
+
+
+ | Validity from |
+ 2022-07-26 15:33:09 |
+
+
+ | Valid to |
+ 2027-07-25 15:33:09 |
+
+
+
+
+
+
+
Subject Information
+
+
+
+
+ | Property |
+ Value |
+
+
+
+
+ | C |
+ US |
+
+
+
+ | CN |
+ apc27D605.nam.gad.schneider-electric.com |
+
+
+
+
+
+
+
+
+ Certificate Extensions
+
+
+
+ | Property |
+ Value |
+
+
+
+
+ | subjectAltName |
+ ap9643_qa1941270129.nam.gad.schneider-electric.com |
+
+
+
+
+ Outbound Connections
+
+
+
+ | Destination IP |
+ Port |
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/testing/unit/tls/reports/tls_report_ext_local.md b/testing/unit/tls/reports/tls_report_ext_local.md
deleted file mode 100644
index 878fa0743..000000000
--- a/testing/unit/tls/reports/tls_report_ext_local.md
+++ /dev/null
@@ -1,33 +0,0 @@
-# TLS Module
-
-### Certificate
-| Property | Value |
-|---|---|
-| Version | 3 (0x2) |
-| Signature Alg. | sha256WithRSAEncryption |
-| Validity from | 2022-07-26 15:33:09 |
-| Valid to | 2027-07-25 15:33:09 |
-
-### Subject
-| Distinguished Name | Value |
-|---|---|
-| C | US
-| CN | apc27D605.nam.gad.schneider-electric.com
-
-### Issuer
-| Distinguished Name | Value |
-|---|---|
-| C | US
-| O | IT Division
-| CN | Sub CA
-
-### Extensions
-| Extension | Value |
-|---|---|
-| subjectAltName | ap9643_qa1941270129.nam.gad.schneider-electric.com
-
-## Summary
-
-| # | Expiry | Length | Type | Port No. | Signed by |
-|-------|---------------------------|----------|--------|------------|-------------|
-| 1 | 2027-07-25 15:33:09 | 888 | EC | 443 | Sub CA |
\ No newline at end of file
diff --git a/testing/unit/tls/reports/tls_report_local.html b/testing/unit/tls/reports/tls_report_local.html
new file mode 100644
index 000000000..610381444
--- /dev/null
+++ b/testing/unit/tls/reports/tls_report_local.html
@@ -0,0 +1,410 @@
+TLS Module
+
+
+
+
+
+ | Expiry |
+ Length |
+ Type |
+ Port number |
+ Signed by |
+
+
+
+
+
+ | 2049-12-31 23:59:59 |
+ 779 |
+ EC |
+ 35288 |
+ None |
+
+
+
+
+
+
+
Certificate Information
+
+
+
+
+ | Property |
+ Value |
+
+
+
+
+ | Version |
+ 3 (0x2) |
+
+
+ | Signature Alg. |
+ sha256WithRSAEncryption |
+
+
+ | Validity from |
+ 2023-03-29 18:37:51 |
+
+
+ | Valid to |
+ 2049-12-31 23:59:59 |
+
+
+
+
+
+
+
Subject Information
+
+
+
+
+ | Property |
+ Value |
+
+
+
+
+ | C |
+ US |
+
+
+
+ | ST |
+ Pennsylvania |
+
+
+
+ | L |
+ Coopersburg |
+
+
+
+ | O |
+ Lutron Electronics Co.\, Inc. |
+
+
+
+ | CN |
+ athena04E580B9 |
+
+
+
+
+
+
+
+
+ Certificate Extensions
+
+
+
+ | Property |
+ Value |
+
+
+
+
+ | authorityKeyIdentifier |
+ key_identifier=accca4f9bd2a47dae81a8f4c87ed2c8edcfd07bf, authority_cert_issuer=None, authority_cert_serial_number=None |
+
+
+
+ | subjectKeyIdentifier |
+ digest=37d90a274635e963081520f98411bda240d30252 |
+
+
+
+ | basicConstraints |
+ ca=False, path_length=None |
+
+
+
+ | keyUsage |
+ digital_signature=True, key_cert_sign=False, key_encipherment=False, crl_sign=False |
+
+
+
+
+
+
+
+
+
+
+ | Expiry |
+ Length |
+ Type |
+ Port number |
+ Signed by |
+
+
+
+
+
+ | 2119-02-05 00:00:00 |
+ 619 |
+ EC |
+ 443 |
+ AthenaProcessor685E1CCB6ECB |
+
+
+
+
+
+
+
Certificate Information
+
+
+
+
+ | Property |
+ Value |
+
+
+
+
+ | Version |
+ 3 (0x2) |
+
+
+ | Signature Alg. |
+ ecdsa-with-SHA256 |
+
+
+ | Validity from |
+ 2019-03-01 00:00:00 |
+
+
+ | Valid to |
+ 2119-02-05 00:00:00 |
+
+
+
+
+
+
+
Subject Information
+
+
+
+
+ | Property |
+ Value |
+
+
+
+
+ | C |
+ US |
+
+
+
+ | ST |
+ Pennsylvania |
+
+
+
+ | L |
+ Coopersburg |
+
+
+
+ | O |
+ Lutron Electronics Co.\, Inc. |
+
+
+
+ | CN |
+ IPLServer4E580B9 |
+
+
+
+
+
+
+
+
+ Certificate Extensions
+
+
+
+ | Property |
+ Value |
+
+
+
+
+ | keyUsage |
+ digital_signature=True, key_cert_sign=False, key_encipherment=True, crl_sign=False |
+
+
+
+ | extendedKeyUsage |
+ serverAuth, Unknown OID |
+
+
+
+ | authorityKeyIdentifier |
+ key_identifier=dff100033b0ab36497bbcd2f3e0515ea7b2f7ea0, authority_cert_issuer=None, authority_cert_serial_number=None |
+
+
+
+ | subjectAltName |
+ IPLServer4E580B9 |
+
+
+
+
+
+
+
+
+
+
+ | Expiry |
+ Length |
+ Type |
+ Port number |
+ Signed by |
+
+
+
+
+
+ | 2049-12-31 23:59:59 |
+ 779 |
+ EC |
+ 47188 |
+ None |
+
+
+
+
+
+
+
Certificate Information
+
+
+
+
+ | Property |
+ Value |
+
+
+
+
+ | Version |
+ 3 (0x2) |
+
+
+ | Signature Alg. |
+ sha256WithRSAEncryption |
+
+
+ | Validity from |
+ 2023-03-29 18:37:51 |
+
+
+ | Valid to |
+ 2049-12-31 23:59:59 |
+
+
+
+
+
+
+
Subject Information
+
+
+
+
+ | Property |
+ Value |
+
+
+
+
+ | C |
+ US |
+
+
+
+ | ST |
+ Pennsylvania |
+
+
+
+ | L |
+ Coopersburg |
+
+
+
+ | O |
+ Lutron Electronics Co.\, Inc. |
+
+
+
+ | CN |
+ athena04E580B9 |
+
+
+
+
+
+
+
+
+ Certificate Extensions
+
+
+
+ | Property |
+ Value |
+
+
+
+
+ | authorityKeyIdentifier |
+ key_identifier=accca4f9bd2a47dae81a8f4c87ed2c8edcfd07bf, authority_cert_issuer=None, authority_cert_serial_number=None |
+
+
+
+ | subjectKeyIdentifier |
+ digest=37d90a274635e963081520f98411bda240d30252 |
+
+
+
+ | basicConstraints |
+ ca=False, path_length=None |
+
+
+
+ | keyUsage |
+ digital_signature=True, key_cert_sign=False, key_encipherment=False, crl_sign=False |
+
+
+
+
+ Outbound Connections
+
+
+
+ | Destination IP |
+ Port |
+
+
+
+ | 224.0.0.251 | 5353 |
+ | 209.244.0.3 | Unknown |
+ | 3.227.250.136 | 443 |
+ | 3.227.203.88 | 443 |
+ | 34.226.101.252 | 8883 |
+ | 3.227.250.208 | 443 |
+ | 52.94.225.110 | 443 |
+
+
+
+
\ No newline at end of file
diff --git a/testing/unit/tls/reports/tls_report_local.md b/testing/unit/tls/reports/tls_report_local.md
deleted file mode 100644
index dc3866dc6..000000000
--- a/testing/unit/tls/reports/tls_report_local.md
+++ /dev/null
@@ -1,35 +0,0 @@
-# TLS Module
-
-### Certificate
-| Property | Value |
-|---|---|
-| Version | 1 (0x0) |
-| Signature Alg. | sha256WithRSAEncryption |
-| Validity from | 2022-09-21 19:57:57 |
-| Valid to | 2027-09-21 19:57:57 |
-
-### Subject
-| Distinguished Name | Value |
-|---|---|
-| C | US
-| ST | California
-| L | Concord
-| O | BuildingsIoT
-| OU | Software
-| CN | EasyIO_FS-32
-
-### Issuer
-| Distinguished Name | Value |
-|---|---|
-| C | US
-| ST | California
-| L | Concord
-| O | BuildingsIoT
-| OU | Software
-| CN | BuildingsIoT RSA Signing CA
-
-## Summary
-
-| # | Expiry | Length | Type | Port No. | Signed by |
-|-------|---------------------------|----------|--------|------------|-------------|
-| 1 | 2027-09-21 19:57:57 | 901 | RSA | 443 | BuildingsIoT RSA Signing CA |
\ No newline at end of file
diff --git a/testing/unit/tls/reports/tls_report_no_cert_local.html b/testing/unit/tls/reports/tls_report_no_cert_local.html
new file mode 100644
index 000000000..c025ee9e8
--- /dev/null
+++ b/testing/unit/tls/reports/tls_report_no_cert_local.html
@@ -0,0 +1,5 @@
+TLS Module
+
+
+ No TLS certificates found on the device
+
\ No newline at end of file
diff --git a/testing/unit/tls/reports/tls_report_no_cert_local.md b/testing/unit/tls/reports/tls_report_no_cert_local.md
deleted file mode 100644
index 6de5bb88a..000000000
--- a/testing/unit/tls/reports/tls_report_no_cert_local.md
+++ /dev/null
@@ -1,9 +0,0 @@
-# TLS Module
-
-- No device certificates detected
-
-
-## Summary
-
-| # | Expiry | Length | Type | Port No. | Signed by |
-|-------|---------------------------|----------|--------|------------|-------------|
\ No newline at end of file
diff --git a/testing/unit/tls/reports/tls_report_single.html b/testing/unit/tls/reports/tls_report_single.html
new file mode 100644
index 000000000..6106068a6
--- /dev/null
+++ b/testing/unit/tls/reports/tls_report_single.html
@@ -0,0 +1,118 @@
+TLS Module
+
+
+
+
+
+ | Expiry |
+ Length |
+ Type |
+ Port number |
+ Signed by |
+
+
+
+
+
+ | 2027-09-21 19:57:57 |
+ 901 |
+ RSA |
+ 443 |
+ BuildingsIoT RSA Signing CA |
+
+
+
+
+
+
+
Certificate Information
+
+
+
+
+ | Property |
+ Value |
+
+
+
+
+ | Version |
+ 1 (0x0) |
+
+
+ | Signature Alg. |
+ sha256WithRSAEncryption |
+
+
+ | Validity from |
+ 2022-09-21 19:57:57 |
+
+
+ | Valid to |
+ 2027-09-21 19:57:57 |
+
+
+
+
+
+
+
Subject Information
+
+
+
+
+ | Property |
+ Value |
+
+
+
+
+ | C |
+ US |
+
+
+
+ | ST |
+ California |
+
+
+
+ | L |
+ Concord |
+
+
+
+ | O |
+ BuildingsIoT |
+
+
+
+ | OU |
+ Software |
+
+
+
+ | CN |
+ EasyIO_FS-32 |
+
+
+
+
+
+
+
+
+ Outbound Connections
+
+
+
+ | Destination IP |
+ Port |
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/testing/unit/tls/tls_module_test.py b/testing/unit/tls/tls_module_test.py
index fc37ade40..f42c7e9d4 100644
--- a/testing/unit/tls/tls_module_test.py
+++ b/testing/unit/tls/tls_module_test.py
@@ -12,6 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
"""Module run all the TLS related unit tests"""
+from tls_module import TLSModule
from tls_util import TLSUtil
import os
import unittest
@@ -38,9 +39,11 @@
CERT_DIR = os.path.join(TEST_FILES_DIR, 'certs/')
ROOT_CERTS_DIR = os.path.join(TEST_FILES_DIR, 'root_certs')
-LOCAL_REPORT = os.path.join(REPORTS_DIR, 'tls_report_local.md')
-LOCAL_REPORT_EXT = os.path.join(REPORTS_DIR, 'tls_report_ext_local.md')
-LOCAL_REPORT_NO_CERT = os.path.join(REPORTS_DIR, 'tls_report_no_cert_local.md')
+LOCAL_REPORT = os.path.join(REPORTS_DIR, 'tls_report_local.html')
+LOCAL_REPORT_SINGLE = os.path.join(REPORTS_DIR, 'tls_report_single.html')
+LOCAL_REPORT_EXT = os.path.join(REPORTS_DIR, 'tls_report_ext_local.html')
+LOCAL_REPORT_NO_CERT = os.path.join(REPORTS_DIR,
+ 'tls_report_no_cert_local.html')
CONF_FILE = 'modules/test/' + MODULE + '/conf/module_config.json'
INTERNET_IFACE = 'eth0'
@@ -197,6 +200,7 @@ def security_tls_v1_2_fail_server_test(self):
self.assertFalse(test_results[0])
# Test 1.2 server when 1.3 and 1.2 failed connection is established
+
def security_tls_v1_2_none_server_test(self):
tls_1_2_results = None, 'No cert'
tls_1_3_results = None, 'No cert'
@@ -225,7 +229,7 @@ def security_tls_client_skip_test(self):
capture_file = os.path.join(CAPTURES_DIR, 'no_tls.pcap')
# Run the client test
- test_results = TLS_UTIL.validate_tls_client(client_ip='172.27.253.167',
+ test_results = TLS_UTIL.validate_tls_client(client_mac='00:15:5d:0c:86:b9',
tls_version='1.2',
capture_files=[capture_file])
print(str(test_results))
@@ -269,8 +273,8 @@ def test_client_tls(self,
os.makedirs(OUTPUT_DIR, exist_ok=True)
capture_file = OUTPUT_DIR + '/client_tls.pcap'
- # Resolve the client ip used
- client_ip = self.get_interface_ip(INTERNET_IFACE)
+ # Resolve the client mac used
+ client_mac = self.get_interface_mac(INTERNET_IFACE)
# Genrate TLS outbound traffic
if tls_generate is None:
@@ -278,7 +282,7 @@ def test_client_tls(self,
self.generate_tls_traffic(capture_file, tls_generate, disable_valid_ciphers)
# Run the client test
- return TLS_UTIL.validate_tls_client(client_ip=client_ip,
+ return TLS_UTIL.validate_tls_client(client_mac=client_mac,
tls_version=tls_version,
capture_files=[capture_file])
@@ -287,7 +291,7 @@ def test_client_tls_with_non_tls_client(self):
capture_file = os.path.join(CAPTURES_DIR, 'monitor.pcap')
# Run the client test
- test_results = TLS_UTIL.validate_tls_client(client_ip='10.10.10.14',
+ test_results = TLS_UTIL.validate_tls_client(client_mac='70:b3:d5:96:c0:00',
tls_version='1.2',
capture_files=[capture_file])
print(str(test_results))
@@ -300,7 +304,7 @@ def security_tls_client_unsupported_tls_client(self):
capture_file = os.path.join(CAPTURES_DIR, 'unsupported_tls.pcap')
# Run the client test
- test_results = TLS_UTIL.validate_tls_client(client_ip='172.27.253.167',
+ test_results = TLS_UTIL.validate_tls_client(client_mac='00:15:5d:0c:86:b9',
tls_version='1.2',
capture_files=[capture_file])
print(str(test_results))
@@ -313,83 +317,135 @@ def security_tls_client_allowed_protocols_test(self):
capture_file = os.path.join(CAPTURES_DIR, 'monitor_with_quic.pcap')
# Run the client test
- test_results = TLS_UTIL.validate_tls_client(client_ip='10.10.10.15',
+ test_results = TLS_UTIL.validate_tls_client(client_mac='e4:5f:01:5f:92:9c',
tls_version='1.2',
capture_files=[capture_file])
print(str(test_results))
self.assertTrue(test_results[0])
- # Commented out whilst TLS report is recreated
- # def tls_module_report_test(self):
- # print('\ntls_module_report_test')
- # os.environ['DEVICE_MAC'] = '38:d1:35:01:17:fe'
- # pcap_file = os.path.join(CAPTURES_DIR, 'tls.pcap')
- # tls = TLSModule(module=MODULE,
- # log_dir=OUTPUT_DIR,
- # conf_file=CONF_FILE,
- # results_dir=OUTPUT_DIR,
- # startup_capture_file=pcap_file,
- # monitor_capture_file=pcap_file,
- # tls_capture_file=pcap_file)
- # report_out_path = tls.generate_module_report()
-
- # with open(report_out_path, 'r', encoding='utf-8') as file:
- # report_out = file.read()
-
- # # Read the local good report
- # with open(LOCAL_REPORT, 'r', encoding='utf-8') as file:
- # report_local = file.read()
-
- # self.assertEqual(report_out, report_local)
-
- # Commented out whilst TLS report is recreated
- # def tls_module_report_ext_test(self):
- # print('\ntls_module_report_ext_test')
- # os.environ['DEVICE_MAC'] = '28:29:86:27:d6:05'
- # pcap_file = os.path.join(CAPTURES_DIR, 'tls_ext.pcap')
- # tls = TLSModule(module=MODULE,
- # log_dir=OUTPUT_DIR,
- # conf_file=CONF_FILE,
- # results_dir=OUTPUT_DIR,
- # startup_capture_file=pcap_file,
- # monitor_capture_file=pcap_file,
- # tls_capture_file=pcap_file)
- # report_out_path = tls.generate_module_report()
-
- # # Read the generated report
- # with open(report_out_path, 'r', encoding='utf-8') as file:
- # report_out = file.read()
-
- # # Read the local good report
- # with open(LOCAL_REPORT_EXT, 'r', encoding='utf-8') as file:
- # report_local = file.read()
-
- # self.assertEqual(report_out, report_local)
-
- # Commented out whilst TLS report is recreated
- # def tls_module_report_no_cert_test(self):
- # print('\ntls_module_report_no_cert_test')
- # os.environ['DEVICE_MAC'] = ''
- # pcap_file = os.path.join(CAPTURES_DIR, 'tls_ext.pcap')
- # tls = TLSModule(module=MODULE,
- # log_dir=OUTPUT_DIR,
- # conf_file=CONF_FILE,
- # results_dir=OUTPUT_DIR,
- # startup_capture_file=pcap_file,
- # monitor_capture_file=pcap_file,
- # tls_capture_file=pcap_file)
-
- # report_out_path = tls.generate_module_report()
-
- # # Read the generated report
- # with open(report_out_path, 'r', encoding='utf-8') as file:
- # report_out = file.read()
-
- # # Read the local good report
- # with open(LOCAL_REPORT_NO_CERT, 'r', encoding='utf-8') as file:
- # report_local = file.read()
-
- # self.assertEqual(report_out, report_local)
+ def outbound_connections_test(self):
+ """ Test generation of the outbound connection ips"""
+ print('\noutbound_connections_test')
+ capture_file = os.path.join(CAPTURES_DIR, 'monitor.pcap')
+ ip_dst = TLS_UTIL.get_all_outbound_connections(
+ device_mac='70:b3:d5:96:c0:00', capture_files=[capture_file])
+ print(str(ip_dst))
+ # Expected set of IPs and ports in tuple format
+ expected_ips = {
+ ('216.239.35.0', 123),
+ ('8.8.8.8', 'Unknown'),
+ ('8.8.8.8', 53),
+ ('18.140.82.197', 443),
+ ('18.140.82.197', 22),
+ ('224.0.0.22', 'Unknown'),
+ ('18.140.82.197', 80)
+ }
+ # Compare as sets since returned order is not guaranteed
+ self.assertEqual(
+ set(ip_dst),
+ expected_ips)
+
+ def outbound_connections_report_test(self):
+ """ Test generation of the outbound connection ips"""
+ print('\noutbound_connections_report_test')
+ capture_file = os.path.join(CAPTURES_DIR, 'monitor.pcap')
+ ip_dst = TLS_UTIL.get_all_outbound_connections(
+ device_mac='70:b3:d5:96:c0:00', capture_files=[capture_file])
+ tls = TLSModule(module=MODULE)
+ gen_html = tls.generate_outbound_connection_table(ip_dst)
+ print(gen_html)
+
+ def tls_module_report_multi_page_test(self):
+ print('\ntls_module_report_test')
+ os.environ['DEVICE_MAC'] = '68:5e:1c:cb:6e:cb'
+ startup_pcap_file = os.path.join(CAPTURES_DIR, 'multi_page_startup.pcap')
+ monitor_pcap_file = os.path.join(CAPTURES_DIR, 'multi_page_monitor.pcap')
+ tls_pcap_file = os.path.join(CAPTURES_DIR, 'multi_page_tls.pcap')
+ tls = TLSModule(module=MODULE,
+ results_dir=OUTPUT_DIR,
+ startup_capture_file=startup_pcap_file,
+ monitor_capture_file=monitor_pcap_file,
+ tls_capture_file=tls_pcap_file)
+ report_out_path = tls.generate_module_report()
+ with open(report_out_path, 'r', encoding='utf-8') as file:
+ report_out = file.read()
+
+ # Read the local good report
+ with open(LOCAL_REPORT, 'r', encoding='utf-8') as file:
+ report_local = file.read()
+
+ self.assertEqual(report_out, report_local)
+
+ def tls_module_report_test(self):
+ print('\ntls_module_report_test')
+ os.environ['DEVICE_MAC'] = '38:d1:35:01:17:fe'
+ pcap_file = os.path.join(CAPTURES_DIR, 'tls.pcap')
+ tls = TLSModule(module=MODULE,
+ results_dir=OUTPUT_DIR,
+ startup_capture_file=pcap_file,
+ monitor_capture_file=pcap_file,
+ tls_capture_file=pcap_file)
+ report_out_path = tls.generate_module_report()
+ with open(report_out_path, 'r', encoding='utf-8') as file:
+ report_out = file.read()
+
+ # Read the local good report
+ with open(LOCAL_REPORT_SINGLE, 'r', encoding='utf-8') as file:
+ report_local = file.read()
+ self.assertEqual(report_out, report_local)
+
+ def tls_module_report_ext_test(self):
+ print('\ntls_module_report_ext_test')
+ os.environ['DEVICE_MAC'] = '28:29:86:27:d6:05'
+ pcap_file = os.path.join(CAPTURES_DIR, 'tls_ext.pcap')
+ tls = TLSModule(module=MODULE,
+ results_dir=OUTPUT_DIR,
+ startup_capture_file=pcap_file,
+ monitor_capture_file=pcap_file,
+ tls_capture_file=pcap_file)
+ report_out_path = tls.generate_module_report()
+
+ # Read the generated report
+ with open(report_out_path, 'r', encoding='utf-8') as file:
+ report_out = file.read()
+
+ # Read the local good report
+ with open(LOCAL_REPORT_EXT, 'r', encoding='utf-8') as file:
+ report_local = file.read()
+
+ # Copy the generated html report to a new file
+ new_report_name = 'tls_report_ext_local.html'
+ new_report_path = os.path.join(OUTPUT_DIR, new_report_name)
+ shutil.copy(report_out_path, new_report_path)
+
+ self.assertEqual(report_out, report_local)
+
+ def tls_module_report_no_cert_test(self):
+ print('\ntls_module_report_no_cert_test')
+ os.environ['DEVICE_MAC'] = ''
+ pcap_file = os.path.join(CAPTURES_DIR, 'tls_ext.pcap')
+ tls = TLSModule(module=MODULE,
+ results_dir=OUTPUT_DIR,
+ startup_capture_file=pcap_file,
+ monitor_capture_file=pcap_file,
+ tls_capture_file=pcap_file)
+
+ report_out_path = tls.generate_module_report()
+
+ # Read the generated report
+ with open(report_out_path, 'r', encoding='utf-8') as file:
+ report_out = file.read()
+
+ # Read the local good report
+ with open(LOCAL_REPORT_NO_CERT, 'r', encoding='utf-8') as file:
+ report_local = file.read()
+
+ # Copy the generated html report to a new file
+ new_report_name = 'tls_report_no_cert_local.html'
+ new_report_path = os.path.join(OUTPUT_DIR, new_report_name)
+ shutil.copy(report_out_path, new_report_path)
+
+ self.assertEqual(report_out, report_local)
def generate_tls_traffic(self,
capture_file,
@@ -470,11 +526,11 @@ def start_capture_thread(self, timeout):
return capture_thread
- def get_interface_ip(self, interface_name):
+ def get_interface_mac(self, interface_name):
try:
addresses = netifaces.ifaddresses(interface_name)
- ipv4 = addresses[netifaces.AF_INET][0]['addr']
- return ipv4
+ mac = addresses[netifaces.AF_LINK][0]['addr']
+ return mac
except (ValueError, KeyError) as e:
print(f'Error: {e}')
return None
@@ -540,6 +596,7 @@ def download_public_cert(self, hostname, port=443):
if __name__ == '__main__':
suite = unittest.TestSuite()
suite.addTest(TLSModuleTest('client_hello_packets_test'))
+
# TLS 1.2 server tests
suite.addTest(TLSModuleTest('security_tls_v1_2_server_test'))
suite.addTest(TLSModuleTest('security_tls_v1_2_for_1_3_server_test'))
@@ -553,6 +610,7 @@ def download_public_cert(self, hostname, port=443):
# # 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'))
@@ -564,10 +622,11 @@ def download_public_cert(self, hostname, port=443):
# Test the results options for tls server tests
suite.addTest(TLSModuleTest('security_tls_server_results_test'))
- # # Test various report module outputs
- # suite.addTest(TLSModuleTest('tls_module_report_test'))
- # suite.addTest(TLSModuleTest('tls_module_report_ext_test'))
- # suite.addTest(TLSModuleTest('tls_module_report_no_cert_test'))
+ # Test various report module outputs
+ suite.addTest(TLSModuleTest('tls_module_report_test'))
+ suite.addTest(TLSModuleTest('tls_module_report_ext_test'))
+ suite.addTest(TLSModuleTest('tls_module_report_no_cert_test'))
+ suite.addTest(TLSModuleTest('tls_module_report_multi_page_test'))
# Test signature validation methods
suite.addTest(TLSModuleTest('tls_module_trusted_ca_cert_chain_test'))
@@ -576,6 +635,9 @@ def download_public_cert(self, hostname, port=443):
suite.addTest(TLSModuleTest('security_tls_client_allowed_protocols_test'))
+ suite.addTest(TLSModuleTest('outbound_connections_test'))
+ suite.addTest(TLSModuleTest('outbound_connections_report_test'))
+
runner = unittest.TextTestRunner()
test_result = runner.run(suite)