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 = ''' - +
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 = ''' - #
Source
- # - # - # - # - # - # - # - # - # - # - # ''' - - # # table_content = ''' - # #
ExpiryLengthTypePort numberSigned 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''' - # - # - # - # - # - # - # - # ''' - - # summary_table += ''' - # - #
ExpiryLengthTypePort numberSigned by
{not_after}{cert_length}{public_key_type}{port}{signed_by}
- # ''' - - # 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 = ''' + + + + + + + + + + + + ''' + + # Generate the certificate table + cert_table = ''' +
ExpiryLengthTypePort numberSigned by
+ + + + + + + ''' + + # 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''' + + + + + + + + + + + + + + + + + +
PropertyValue
Version{version_value}
Signature Alg.{signature_alg_value}
Validity from{not_before}
Valid to{not_after}
+ ''' + + subject_table = ''' + + + + + + + + ''' + + # Append the subject information + for val in cert.subject.rdns: + dn = val.rfc4514_string().split('=') + subject_table += f''' + + + + + ''' + + subject_table += ''' + +
PropertyValue
{dn[0]}{dn[1]}
''' + + # 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
+ + + + + + + + ''' + + for extension in cert.extensions: + if isinstance(extension.value, list): + for extension_value in extension.value: + ext_table += f''' + + + + + ''' + else: + ext_table += f''' + + + + + ''' + + ext_table += ''' + +
PropertyValue
{extension.oid._name}{self.format_extension_value(extension_value.value)}
{extension.oid._name}{self.format_extension_value(extension.value)}
''' + + # 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

+ + + + + + + + + ''' + + rows = [ + f'\t' + for ip, port in outbound_conns + ] + html_content += '\n'.join(rows) + + # Close the table + html_content += """ + + \r
Destination IPPort
{ip}{port}
+ """ + + 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

- +
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

+ +
Source
+ + + + + + + + + + + + + + + + + + + +
ExpiryLengthTypePort numberSigned by
2027-07-25 15:33:09888EC443Sub CA
+ +
+
+
Certificate Information
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
PropertyValue
Version3 (0x2)
Signature Alg.sha256WithRSAEncryption
Validity from2022-07-26 15:33:09
Valid to2027-07-25 15:33:09
+ +
+
+
Subject Information
+ + + + + + + + + + + + + + + + + + + + +
PropertyValue
CUS
CNapc27D605.nam.gad.schneider-electric.com
+
+
+ + +
Certificate Extensions
+ + + + + + + + + + + + + + +
PropertyValue
subjectAltNameap9643_qa1941270129.nam.gad.schneider-electric.com
+ + + + + + + + + + + + + + + + + + + + + + +
ExpiryLengthTypePort numberSigned by
2027-07-25 15:33:09888EC443Sub CA
+ +
+
+
Certificate Information
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
PropertyValue
Version3 (0x2)
Signature Alg.sha256WithRSAEncryption
Validity from2022-07-26 15:33:09
Valid to2027-07-25 15:33:09
+ +
+
+
Subject Information
+ + + + + + + + + + + + + + + + + + + + +
PropertyValue
CUS
CNapc27D605.nam.gad.schneider-electric.com
+
+
+ + +
Certificate Extensions
+ + + + + + + + + + + + + + +
PropertyValue
subjectAltNameap9643_qa1941270129.nam.gad.schneider-electric.com
+

Outbound Connections

+ + + + + + + + + + + +
Destination IPPort
+ \ 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

+ + + + + + + + + + + + + + + + + + + + + +
ExpiryLengthTypePort numberSigned by
2049-12-31 23:59:59779EC47188None
+ +
+
+
Certificate Information
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
PropertyValue
Version3 (0x2)
Signature Alg.sha256WithRSAEncryption
Validity from2023-03-29 18:37:51
Valid to2049-12-31 23:59:59
+ +
+
+
Subject Information
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PropertyValue
CUS
STPennsylvania
LCoopersburg
OLutron Electronics Co.\, Inc.
CNathena04E580B9
+
+
+ + +
Certificate Extensions
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PropertyValue
authorityKeyIdentifierkey_identifier=accca4f9bd2a47dae81a8f4c87ed2c8edcfd07bf, authority_cert_issuer=None, authority_cert_serial_number=None
subjectKeyIdentifierdigest=37d90a274635e963081520f98411bda240d30252
basicConstraintsca=False, path_length=None
keyUsagedigital_signature=True, key_cert_sign=False, key_encipherment=False, crl_sign=False
+ + + + + + + + + + + + + + + + + + + + + + +
ExpiryLengthTypePort numberSigned by
2049-12-31 23:59:59779EC35288None
+ +
+
+
Certificate Information
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
PropertyValue
Version3 (0x2)
Signature Alg.sha256WithRSAEncryption
Validity from2023-03-29 18:37:51
Valid to2049-12-31 23:59:59
+ +
+
+
Subject Information
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PropertyValue
CUS
STPennsylvania
LCoopersburg
OLutron Electronics Co.\, Inc.
CNathena04E580B9
+
+
+ + +
Certificate Extensions
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PropertyValue
authorityKeyIdentifierkey_identifier=accca4f9bd2a47dae81a8f4c87ed2c8edcfd07bf, authority_cert_issuer=None, authority_cert_serial_number=None
subjectKeyIdentifierdigest=37d90a274635e963081520f98411bda240d30252
basicConstraintsca=False, path_length=None
keyUsagedigital_signature=True, key_cert_sign=False, key_encipherment=False, crl_sign=False
+ + + + + + + + + + + + + + + + + + + + + + + +
ExpiryLengthTypePort numberSigned by
2119-02-05 00:00:00619EC443AthenaProcessor685E1CCB6ECB
+ +
+
+
Certificate Information
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
PropertyValue
Version3 (0x2)
Signature Alg.ecdsa-with-SHA256
Validity from2019-03-01 00:00:00
Valid to2119-02-05 00:00:00
+ +
+
+
Subject Information
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PropertyValue
CUS
STPennsylvania
LCoopersburg
OLutron Electronics Co.\, Inc.
CNIPLServer4E580B9
+
+
+ + +
Certificate Extensions
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PropertyValue
keyUsagedigital_signature=True, key_cert_sign=False, key_encipherment=True, crl_sign=False
extendedKeyUsageserverAuth, Unknown OID
authorityKeyIdentifierkey_identifier=dff100033b0ab36497bbcd2f3e0515ea7b2f7ea0, authority_cert_issuer=None, authority_cert_serial_number=None
subjectAltNameIPLServer4E580B9
+ + + + + + + + + + + + + + + + + + + + + + + +
ExpiryLengthTypePort numberSigned by
2049-12-31 23:59:59779EC47188None
+ +
+
+
Certificate Information
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
PropertyValue
Version3 (0x2)
Signature Alg.sha256WithRSAEncryption
Validity from2023-03-29 18:37:51
Valid to2049-12-31 23:59:59
+ +
+
+
Subject Information
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PropertyValue
CUS
STPennsylvania
LCoopersburg
OLutron Electronics Co.\, Inc.
CNathena04E580B9
+
+
+ + +
Certificate Extensions
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PropertyValue
authorityKeyIdentifierkey_identifier=accca4f9bd2a47dae81a8f4c87ed2c8edcfd07bf, authority_cert_issuer=None, authority_cert_serial_number=None
subjectKeyIdentifierdigest=37d90a274635e963081520f98411bda240d30252
basicConstraintsca=False, path_length=None
keyUsagedigital_signature=True, key_cert_sign=False, key_encipherment=False, crl_sign=False
+

Outbound Connections

+ + + + + + + + + + + + + + + + + +
Destination IPPort
224.0.0.2515353
209.244.0.3Unknown
3.227.250.136443
3.227.203.88443
34.226.101.2528883
3.227.250.208443
52.94.225.110443
+ \ 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

+ + + + + + + + + + + + + + + + + + + + + +
ExpiryLengthTypePort numberSigned by
2027-09-21 19:57:57901RSA443BuildingsIoT RSA Signing CA
+ +
+
+
Certificate Information
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
PropertyValue
Version1 (0x0)
Signature Alg.sha256WithRSAEncryption
Validity from2022-09-21 19:57:57
Valid to2027-09-21 19:57:57
+ +
+
+
Subject Information
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PropertyValue
CUS
STCalifornia
LConcord
OBuildingsIoT
OUSoftware
CNEasyIO_FS-32
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + +
ExpiryLengthTypePort numberSigned by
2027-09-21 19:57:57901RSA443BuildingsIoT RSA Signing CA
+ +
+
+
Certificate Information
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
PropertyValue
Version1 (0x0)
Signature Alg.sha256WithRSAEncryption
Validity from2022-09-21 19:57:57
Valid to2027-09-21 19:57:57
+ +
+
+
Subject Information
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PropertyValue
CUS
STCalifornia
LConcord
OBuildingsIoT
OUSoftware
CNEasyIO_FS-32
+
+
+ + +

Outbound Connections

+ + + + + + + + + + + +
Destination IPPort
+ \ 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)