diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml
index b14f52c9f..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: {}
diff --git a/framework/python/src/common/testreport.py b/framework/python/src/common/testreport.py
index f9401fe80..6acc6eac0 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['style'] == 'display:flex;justify-content:space-between;'):
+ 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/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/requirements.txt b/framework/requirements.txt
index 6f54d3a99..82624b03b 100644
--- a/framework/requirements.txt
+++ b/framework/requirements.txt
@@ -42,3 +42,4 @@ APScheduler==3.10.4
# Requirements for reports generation
Jinja2==3.1.4
+beautifulsoup4==4.12.3
\ No newline at end of file
diff --git a/modules/test/ntp/python/src/ntp_module.py b/modules/test/ntp/python/src/ntp_module.py
index 67e2a3c92..22fbf40ed 100644
--- a/modules/test/ntp/python/src/ntp_module.py
+++ b/modules/test/ntp/python/src/ntp_module.py
@@ -118,7 +118,7 @@ def generate_module_report(self):
if total_requests + total_responses > 0:
table_content = '''
-
+
| Source |
diff --git a/modules/test/tls/conf/module_config.json b/modules/test/tls/conf/module_config.json
index 882bf91c7..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":[
{
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 9fc89d549..1405ad31c 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'
@@ -48,146 +56,298 @@ 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 += summary_table + '\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
@@ -222,7 +382,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')
diff --git a/modules/test/tls/python/src/tls_util.py b/modules/test/tls/python/src/tls_util.py
index 9f00b96ef..1557b61f2 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,
diff --git a/resources/report/test_report_styles.css b/resources/report/test_report_styles.css
index 8168da3f2..b1ed9d33c 100644
--- a/resources/report/test_report_styles.css
+++ b/resources/report/test_report_styles.css
@@ -174,10 +174,16 @@
margin-bottom: 25px;
}
+ .module-summary.not-first{
+ margin-top: 10px;
+ }
+
+
.module-summary thead tr th {
text-align: left;
padding-top: 15px;
- padding-left: 15px;
+ padding-left: 10px;
+ padding-right: 5px;
font-weight: 500;
color: #5F6368;
font-size: 14px;
@@ -185,8 +191,9 @@
.module-summary tbody tr td {
padding-bottom: 15px;
- padding-left: 15px;
- font-size: 24px;
+ padding-left: 10px;
+ padding-right: 5px;
+ font-size: 22px;
}
.module-data {
diff --git a/testing/device_configs/tester1/device_config.json b/testing/device_configs/tester1/device_config.json
index f9eeccb6a..0c94febd2 100644
--- a/testing/device_configs/tester1/device_config.json
+++ b/testing/device_configs/tester1/device_config.json
@@ -2,6 +2,9 @@
"manufacturer": "Google",
"model": "Tester 1",
"mac_addr": "02:42:aa:00:00:01",
+ "type": "Controller - FCU",
+ "technology": "Hardware - Fitness",
+ "test_pack": "Device Qualification",
"test_modules": {
"dns": {
"enabled": true
@@ -24,5 +27,31 @@
"tls": {
"enabled": false
}
- }
+ },
+ "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"
+ }
+ ]
}
diff --git a/testing/device_configs/tester2/device_config.json b/testing/device_configs/tester2/device_config.json
index ccbc14585..d816cb2ef 100644
--- a/testing/device_configs/tester2/device_config.json
+++ b/testing/device_configs/tester2/device_config.json
@@ -2,6 +2,9 @@
"manufacturer": "Google",
"model": "Tester 2",
"mac_addr": "02:42:aa:00:00:02",
+ "type": "Controller - FCU",
+ "technology": "Hardware - Fitness",
+ "test_pack": "Device Qualification",
"test_modules": {
"dns": {
"enabled": true
@@ -24,5 +27,31 @@
"tls": {
"enabled": false
}
- }
+ },
+ "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"
+ }
+ ]
}
diff --git a/testing/device_configs/tester3/device_config.json b/testing/device_configs/tester3/device_config.json
index b7792027e..7b0206ca3 100644
--- a/testing/device_configs/tester3/device_config.json
+++ b/testing/device_configs/tester3/device_config.json
@@ -2,6 +2,9 @@
"manufacturer": "Google",
"model": "Tester 3",
"mac_addr": "02:42:aa:00:00:03",
+ "type": "Controller - FCU",
+ "technology": "Hardware - Fitness",
+ "test_pack": "Device Qualification",
"test_modules": {
"dns": {
"enabled": false
@@ -18,5 +21,31 @@
"nmap": {
"enabled": false
}
- }
+ },
+ "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"
+ }
+ ]
}
diff --git a/testing/unit/ntp/ntp_module_test.py b/testing/unit/ntp/ntp_module_test.py
index ed5934048..52bd32aa9 100644
--- a/testing/unit/ntp/ntp_module_test.py
+++ b/testing/unit/ntp/ntp_module_test.py
@@ -16,6 +16,7 @@
import unittest
from scapy.all import rdpcap, NTP, wrpcap
import os
+import shutil
import sys
MODULE = 'ntp'
@@ -62,6 +63,11 @@ def ntp_module_report_test(self):
with open(LOCAL_REPORT, 'r', encoding='utf-8') as file:
report_local = file.read()
+ # Copy the generated html report to a new file
+ new_report_name = 'ntp_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)
# Test the module report generation if no DNS traffic
@@ -110,6 +116,11 @@ def ntp_module_report_no_ntp_test(self):
with open(LOCAL_REPORT_NO_NTP, 'r', encoding='utf-8') as file:
report_local = file.read()
+ # Copy the generated html report to a new file
+ new_report_name = 'ntp_no_ntp.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)
if __name__ == '__main__':
diff --git a/testing/unit/ntp/reports/ntp_report_local.html b/testing/unit/ntp/reports/ntp_report_local.html
index 025881db5..1fe5e3f3a 100644
--- a/testing/unit/ntp/reports/ntp_report_local.html
+++ b/testing/unit/ntp/reports/ntp_report_local.html
@@ -18,7 +18,7 @@ NTP Module
-
+
| Source |
diff --git a/testing/unit/report/report_compliant.json b/testing/unit/report/report_compliant.json
index 08ff585ad..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",
diff --git a/testing/unit/report/report_noncompliant.json b/testing/unit/report/report_noncompliant.json
index b3ba74c0d..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",
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/tls/captures/multi_page_monitor.pcap b/testing/unit/tls/captures/multi_page_monitor.pcap
new file mode 100644
index 000000000..d813e710b
Binary files /dev/null and b/testing/unit/tls/captures/multi_page_monitor.pcap differ
diff --git a/testing/unit/tls/captures/multi_page_startup.pcap b/testing/unit/tls/captures/multi_page_startup.pcap
new file mode 100644
index 000000000..e6089ee2b
Binary files /dev/null and b/testing/unit/tls/captures/multi_page_startup.pcap differ
diff --git a/testing/unit/tls/captures/multi_page_tls.pcap b/testing/unit/tls/captures/multi_page_tls.pcap
new file mode 100644
index 000000000..fc76ebe99
Binary files /dev/null and b/testing/unit/tls/captures/multi_page_tls.pcap differ
diff --git a/testing/unit/tls/reports/tls_report_ext_local.html b/testing/unit/tls/reports/tls_report_ext_local.html
new file mode 100644
index 000000000..7e691f66a
--- /dev/null
+++ b/testing/unit/tls/reports/tls_report_ext_local.html
@@ -0,0 +1,212 @@
+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 |
+
+
+
+
+
+
+
+
+
+ | 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..72b2e5a1a
--- /dev/null
+++ b/testing/unit/tls/reports/tls_report_local.html
@@ -0,0 +1,538 @@
+TLS Module
+
+
+
+
+ | 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 |
+
+
+
+
+
+
+
+
+
+ | 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..36adffb94
--- /dev/null
+++ b/testing/unit/tls/reports/tls_report_single.html
@@ -0,0 +1,220 @@
+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 |
+
+
+
+
+
+
+
+
+
+
+
+
+
+ | 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 fc7153aef..f0ad30014 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'
@@ -319,74 +322,130 @@ def security_tls_client_allowed_protocols_test(self):
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,
- # 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,
- # 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,
- # 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,
@@ -537,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'))
@@ -550,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'))
@@ -561,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'))
@@ -573,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)