diff --git a/daq/report.py b/daq/report.py index 40fde8aa3d..bd29844eec 100644 --- a/daq/report.py +++ b/daq/report.py @@ -270,7 +270,10 @@ def _analyse_and_write_results(self): # The device overall fails if any result is unexpected if result_dict["result"] != required_result: - passes = False + if required_result == 'notfail' and result_dict["result"] != 'fail': + pass + else: + passes = False if result_dict["result"] == 'gone': gone = True @@ -308,7 +311,7 @@ def _write_category_table(self): total = 0 results = [[0, 0, 0] for _ in range(len(self._expected_headers))] - result = self._NO_REQUIRED # Overall category result + result = self._NO_REQUIRED # Overall category result is n/a if no tests for test_name, result_dict in self._results.items(): test_info = self._get_test_info(test_name) @@ -335,6 +338,9 @@ def _write_category_table(self): # TODO remove when info tests are removed if result_dict["result"] == 'info': result_dict["result"] = 'pass' + elif (result_dict["result"] == 'skip' and + test_info['required'] == 'notfail'): + result = 'pass' else: result = "fail" else: diff --git a/docker/include/bin/start_faux b/docker/include/bin/start_faux index 87779b809d..6a558df1c9 100755 --- a/docker/include/bin/start_faux +++ b/docker/include/bin/start_faux @@ -166,6 +166,13 @@ if [ -n "${options[ntpv4]}" ]; then java -jar NTPClient/build/libs/NTPClient-1.0-SNAPSHOT.jar $ntp_server 123 4 > ntp.log sleep 8 done) & +elif [ -n "${options[ntpv4dns]}" ]; then + ntp_server=ntp.daqlocal + echo Transmitting NTP query to $ntp_server using NTPv4 + (while true; do + java -jar NTPClient/build/libs/NTPClient-1.0-SNAPSHOT.jar $ntp_server 123 4 > ntp.log + sleep 8 + done) & elif [ -n "${options[ntpv3]}" ]; then STATIC_NTP_SERVER=216.239.35.8 echo Transmitting NTP query to $STATIC_NTP_SERVER using NTPv3 diff --git a/docs/device_report.md b/docs/device_report.md index 1047645436..1c03236c3c 100644 --- a/docs/device_report.md +++ b/docs/device_report.md @@ -53,7 +53,7 @@ Overall device result FAIL |Base|2|FAIL|1/0/1|0/0/0|0/0/0|0/0/0|0/0/0|0/0/0| |Connection|12|FAIL|3/5/4|0/0/0|0/0/0|0/0/0|0/0/0|0/0/0| |Security|13|FAIL|2/4/4|0/0/0|0/0/1|0/0/0|0/2/0|0/0/0| -|NTP|2|PASS|2/0/0|0/0/0|0/0/0|0/0/0|0/0/0|0/0/0| +|NTP|3|PASS|2/0/1|0/0/0|0/0/0|0/0/0|0/0/0|0/0/0| |DNS|1|SKIP|0/0/1|0/0/0|0/0/0|0/0/0|0/0/0|0/0/0| |Communication|2|PASS|2/0/0|0/0/0|0/0/0|0/0/0|0/0/0|0/0/0| |Protocol|2|FAIL|0/0/0|0/0/0|0/1/1|0/0/0|0/0/0|0/0/0| @@ -64,7 +64,7 @@ Syntax: Pass / Fail / Skip |Expectation|pass|fail|skip|gone| |---|---|---|---|---| -|Required Pass|10|1|10|8| +|Required Pass|10|1|11|8| |Required Pass for PoE Devices|0|0|1|0| |Required Pass for BACnet Devices|0|1|2|0| |Required Pass for IoT Devices|0|0|1|0| @@ -97,7 +97,8 @@ Syntax: Pass / Fail / Skip |skip|dns.network.hostname_resolution|DNS|Required Pass|Device did not send any DNS requests| |pass|dot1x.dot1x|Other|Other|Authentication for 9a:02:57:1e:8f:01 succeeded.| |pass|ntp.network.ntp_support|NTP|Required Pass|Using NTPv4.| -|pass|ntp.network.ntp_update|NTP|Required Pass|Device clock synchronized.| +|pass|ntp.network.ntp_update_dhcp|NTP|Required Pass|Device clock synchronized.| +|skip|ntp.network.ntp_update_dns|NTP|Required Pass|Device not configured for NTP via DNS| |skip|poe.switch.power|PoE|Required Pass for PoE Devices|No local IP has been set, check system config| |fail|protocol.bacext.pic|Protocol|Required Pass for BACnet Devices|PICS file defined however a BACnet device was not found.| |skip|protocol.bacext.version|Protocol|Required Pass for BACnet Devices|Bacnet device not found.| @@ -557,11 +558,17 @@ Device supports NTP version 4. -------------------- RESULT pass ntp.network.ntp_support Using NTPv4. -------------------- -ntp.network.ntp_update +ntp.network.ntp_update_dhcp -------------------- -Device synchronizes its time to the NTP server. +Device synchronizes its time to the NTP server using DHCP -------------------- -RESULT pass ntp.network.ntp_update Device clock synchronized. +RESULT pass ntp.network.ntp_update_dhcp Device clock synchronized. +-------------------- +ntp.network.ntp_update_dns +-------------------- +Device synchronizes its time to the NTP server using DNS +-------------------- +RESULT skip ntp.network.ntp_update_dns Device not configured for NTP via DNS -------------------- connection.network.mac_oui -------------------- diff --git a/resources/setups/common/tests_config.json b/resources/setups/common/tests_config.json index 163163a88d..a32b6f586e 100644 --- a/resources/setups/common/tests_config.json +++ b/resources/setups/common/tests_config.json @@ -89,9 +89,14 @@ "required": "pass", "expected": "Required Pass" }, - "ntp.network.ntp_update": { + "ntp.network.ntp_update_dhcp": { "category": "NTP", - "required": "pass", + "required": "notfail", + "expected": "Required Pass" + }, + "ntp.network.ntp_update_dns": { + "category": "NTP", + "required": "notfail", "expected": "Required Pass" }, "communication.network.min_send": { diff --git a/subset/network/Dockerfile.test_network b/subset/network/Dockerfile.test_network index ef4b204207..bd50bc0a5e 100644 --- a/subset/network/Dockerfile.test_network +++ b/subset/network/Dockerfile.test_network @@ -4,11 +4,11 @@ RUN $AG update && $AG install openjdk-8-jre RUN $AG update && $AG install openjdk-8-jdk git -RUN $AG update && $AG install python python-setuptools python-pip netcat +RUN $AG update && $AG install python3.8 python3-setuptools python3-pip netcat RUN $AG update && $AG install curl -RUN pip install scapy +RUN python3.8 -m pip install scapy COPY subset/network/ . diff --git a/subset/network/__init__.py b/subset/network/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/subset/network/ntp_tests.py b/subset/network/ntp_tests.py index 094b45e75f..010049ab37 100644 --- a/subset/network/ntp_tests.py +++ b/subset/network/ntp_tests.py @@ -1,20 +1,23 @@ from __future__ import absolute_import, division -from scapy.all import NTP, rdpcap import sys -import os +import re +import json + +from scapy.config import scapy_delete_temp_files +import test_result +from scapy.all import NTP, rdpcap, DNS arguments = sys.argv test_request = str(arguments[1]) pcap_file = str(arguments[2]) +device_address = str(arguments[3]) report_filename = 'ntp_tests.txt' -ignore = '%%' -summary_text = '' -result = 'fail' -dash_break_line = '--------------------\n' + description_ntp_support = 'Device supports NTP version 4.' -description_ntp_update = 'Device synchronizes its time to the NTP server.' +description_ntp_update_dhcp = 'Device synchronizes its time to the NTP server using DHCP' +description_ntp_update_dns = 'Device synchronizes its time to the NTP server using DNS' NTP_VERSION_PASS = 4 LOCAL_PREFIX = '10.20.' @@ -26,22 +29,29 @@ OFFSET_ALLOWANCE = 0.128 LEAP_ALARM = 3 +IP_REGEX = r'(([0-9]{1,3}\.){3}[0-9]{1,3})' +NTP_SERVER_IP_SUFFIX = '.2' +NTP_SERVER_HOSTNAME = 'ntp.daqlocal' +MODULE_CONFIG_PATH = '/config/device/module_config.json' + +TEST_DHCP = 'dhpc' +TEST_DNS = 'dns' def write_report(string_to_append): with open(report_filename, 'a+') as file_open: file_open.write(string_to_append) -# Extracts the NTP version from the first client NTP packet def ntp_client_version(capture): + """ Extracts the NTP version from the first client NTP packet """ client_packets = ntp_packets(capture, MODE_CLIENT) if len(client_packets) == 0: return None return ntp_payload(client_packets[0]).version -# Filters the packets by type (NTP) def ntp_packets(capture, mode=None): + """ Filters the packets by type (NTP) """ packets = [] for packet in capture: if NTP in packet: @@ -53,53 +63,84 @@ def ntp_packets(capture, mode=None): return packets -# Extracts the NTP payload from a packet of type NTP +def ntp_configured_by_dns(): + """Checks module_config for parameter that NTP is configured using DNS + + Parameter must be (bool) True, else will be considered false + """ + module_config = open(MODULE_CONFIG_PATH) + module_config = json.load(module_config) + try: + ntp_by_dns = (module_config['modules']['network']['ntp_dns']) + except KeyError: + ntp_by_dns = False + + return ntp_by_dns is True + + def ntp_payload(packet): + """ Extracts the NTP payload from a packet of type NTP """ ip = packet.payload udp = ip.payload ntp = udp.payload return ntp -def test_ntp_support(): - capture = rdpcap(pcap_file) - packets = ntp_packets(capture) - if len(packets) > 0: - version = ntp_client_version(packets) - if version is None: - add_summary("No NTP packets received.") - return 'skip' - if version == NTP_VERSION_PASS: - add_summary("Using NTPv" + str(NTP_VERSION_PASS) + ".") - return 'pass' - else: - add_summary("Not using NTPv" + str(NTP_VERSION_PASS) + ".") - return 'fail' - else: - add_summary("No NTP packets received.") - return 'skip' +def dns_requests_for_hostname(hostname, packet_capture): + """Checks for DNS requests for a given hostname + Args: + packet_capture path to tcpdump packet capture file + hostname hostname to look for + + Returns: + true/false if any matching DNS requests detected to hostname + """ + capture = rdpcap(packet_capture) + fqdn = hostname + '.' + for packet in capture: + if DNS in packet: + if packet.qd.qname.decode("utf8") == fqdn: + return True + return False + + +def ntp_server_from_ip(ip_address): + """Returns the IP address of the NTP server provided by DAQ + + Args: + ip_address: IP address of the device under test + + Returns: + IP address of NTP server + """ + return re.sub(r'\.\d+$', NTP_SERVER_IP_SUFFIX, ip_address) + + +def check_ntp_synchronized(ntp_packets_array, ntp_server_ip): + """ Checks if NTP packets indicate a device is syncronized with the provided + IP address + + Args: + packet_capture Array of scapy object of packet capture with NTP + packets from ntp_packets() + ntp_server_ip IP address of server to checK + + Returns: + boolean true/false if synchronized with provided NTP server. + """ -def test_ntp_update(): - capture = rdpcap(pcap_file) - packets = ntp_packets(capture) - if len(packets) < 2: - add_summary("Not enough NTP packets received.") - return 'skip' - # Check that DAQ NTP server has been used - using_local_server = False local_ntp_packets = [] - for packet in packets: - # Packet is to or from local NTP server - if ((packet.payload.dst.startswith(LOCAL_PREFIX) and - packet.payload.dst.endswith(NTP_SERVER_SUFFIX)) or - (packet.payload.src.startswith(LOCAL_PREFIX) and - packet.payload.src.endswith(NTP_SERVER_SUFFIX))): - using_local_server = True + using_given_server = False + for packet in ntp_packets_array: + # Packet is to or from NTP server + if (packet.payload.dst == ntp_server_ip or packet.payload.src == ntp_server_ip): + using_given_server = True local_ntp_packets.append(packet) - if not using_local_server or len(local_ntp_packets) < 2: - add_summary("Device clock not synchronized with local NTP server.") - return 'fail' + + if not using_given_server or len(local_ntp_packets) < 2: + return False + # Obtain the latest NTP poll p1 = p2 = p3 = p4 = None for i in range(len(local_ntp_packets)): @@ -122,9 +163,10 @@ def test_ntp_update(): p3 = p4 = None else: p3 = local_ntp_packets[i] + if p1 is None or p2 is None: - add_summary("Device clock not synchronized with local NTP server.") - return 'fail' + return False + t1 = ntp_payload(p1).sent t2 = ntp_payload(p1).time t3 = ntp_payload(p2).sent @@ -142,26 +184,87 @@ def test_ntp_update(): offset = abs((t2 - t1) + (t3 - t4))/2 if offset < OFFSET_ALLOWANCE and not ntp_payload(p1).leap == LEAP_ALARM: - add_summary("Device clock synchronized.") - return 'pass' + return True else: - add_summary("Device clock not synchronized with local NTP server.") - return 'fail' + return False -def add_summary(text): - global summary_text - summary_text = summary_text + " " + text if summary_text else text +def test_ntp_support(): + """ Tests support for NTPv4 """ + capture = rdpcap(pcap_file) + packets = ntp_packets(capture) + test_ntp = test_result.test_result(name='ntp.network.ntp_support', + description=description_ntp_support) + if len(packets) > 0: + version = ntp_client_version(packets) + if version is None: + test_ntp.add_summary("No NTP packets received.") + test_ntp.result = test_result.SKIP + if version == NTP_VERSION_PASS: + test_ntp.add_summary("Using NTPv" + str(NTP_VERSION_PASS) + ".") + test_ntp.result = test_result.PASS + else: + test_ntp.add_summary("Not using NTPv" + str(NTP_VERSION_PASS) + ".") + test_ntp.result = test_result.FAIL + else: + test_ntp.add_summary("No NTP packets received.") + test_ntp.result = test_result.SKIP + test_ntp.write_results(report_filename) -write_report("{b}{t}\n{b}".format(b=dash_break_line, t=test_request)) +def test_ntp_update(): + """Runs NTP Update Test for both DHCP and DNS""" + # Used to always print test output in the same order + ntp_tests = {} + ntp_tests[TEST_DHCP] = test_result.test_result( + name='ntp.network.ntp_update_dhcp', + description=description_ntp_update_dhcp) + ntp_tests[TEST_DNS] = test_result.test_result( + name='ntp.network.ntp_update_dns', + description=description_ntp_update_dns) + + capture = rdpcap(pcap_file) + packets = ntp_packets(capture) + + if len(packets) < 2: + for test in ntp_tests: + ntp_tests[test].add_summary("Not enough NTP packets received.") + ntp_tests[test].result = test_result.SKIP + else: + test_dns = ntp_configured_by_dns() + local_ntp_ip = ntp_server_from_ip(device_address) + device_sync_local_server = check_ntp_synchronized(packets, local_ntp_ip) + + if test_dns: + active = TEST_DNS + ntp_tests[TEST_DHCP].add_summary("Device not configured for NTP via DHCP") + ntp_tests[TEST_DHCP].result = test_result.SKIP + else: + active = TEST_DHCP + ntp_tests[TEST_DNS].add_summary("Device not configured for NTP via DNS") + ntp_tests[TEST_DNS].result = test_result.SKIP + + if not device_sync_local_server: + ntp_tests[active].add_summary("Device clock not synchronized with local NTP server.") + ntp_tests[active].result = test_result.FAIL + + if device_sync_local_server: + # DNS and DHCP NTP currently resolve to the same IP address so + # ensure a device which says it's configured using DHCP does not + # perform DNS lookups for the NTP server + if (active == TEST_DHCP and dns_requests_for_hostname(NTP_SERVER_HOSTNAME, + capture)): + ntp_tests[active].add_summary("Device used DNS for NTP") + ntp_tests[active].result = test_result.FAiL + else: + ntp_tests[active].add_summary("Device clock synchronized.") + ntp_tests[active].result = test_result.PASS + + ntp_tests[TEST_DHCP].write_results(report_filename) + ntp_tests[TEST_DNS].write_results(report_filename) if test_request == 'ntp.network.ntp_support': - write_report("{d}\n{b}".format(b=dash_break_line, d=description_ntp_support)) - result = test_ntp_support() + test_ntp_support() elif test_request == 'ntp.network.ntp_update': - write_report("{d}\n{b}".format(b=dash_break_line, d=description_ntp_update)) - result = test_ntp_update() - -write_report("RESULT {r} {t} {s}\n".format(r=result, t=test_request, s=summary_text.strip())) + test_ntp_update() diff --git a/subset/network/test_network b/subset/network/test_network index 022c91bedb..b51f5f00b9 100755 --- a/subset/network/test_network +++ b/subset/network/test_network @@ -1,18 +1,18 @@ -#!/bin/bash -e +#!/bin/bash REPORT=/tmp/report.txt MONITOR=/scans/monitor.pcap # General Network Tests -python network_tests.py communication.network.min_send $MONITOR $TARGET_IP -python network_tests.py communication.network.type $MONITOR $TARGET_IP +python3.8 network_tests.py communication.network.min_send $MONITOR $TARGET_IP +python3.8 network_tests.py communication.network.type $MONITOR $TARGET_IP cat network_tests.txt >> $REPORT # NTP Tests -python ntp_tests.py ntp.network.ntp_support $MONITOR -python ntp_tests.py ntp.network.ntp_update $MONITOR +python3.8 ntp_tests.py ntp.network.ntp_support $MONITOR $TARGET_IP +python3.8 ntp_tests.py ntp.network.ntp_update $MONITOR $TARGET_IP cat ntp_tests.txt >> $REPORT @@ -20,6 +20,6 @@ cat ntp_tests.txt >> $REPORT ./run_macoui_test $TARGET_MAC $REPORT # DNS Tests -python dns_tests.py dns.network.hostname_resolution $MONITOR $TARGET_IP +python3.8 dns_tests.py dns.network.hostname_resolution $MONITOR $TARGET_IP cat dns_tests.txt >> $REPORT diff --git a/subset/network/test_result.py b/subset/network/test_result.py new file mode 100644 index 0000000000..c0e36433ff --- /dev/null +++ b/subset/network/test_result.py @@ -0,0 +1,41 @@ +""" Wrapper to build test result output +""" +from dataclasses import dataclass + +PASS = 'pass' +SKIP = 'skip' +FAIL = 'fail' + + +@dataclass +class test_result: + _dash_break_line = '--------------------\n' + + name: str + description: str + summary: str = '' + result: str = 'fail' + + def write_results(self, report_filename): + """Writes result to file + + Args: + report_filename: path to file to write results to + """ + with open(report_filename, 'a+') as file_open: + file_open.write("{b}{t}\n{b}".format(b=self._dash_break_line, t=self.name)) + file_open.write("{d}\n{b}".format( + b=self._dash_break_line, d=self.description)) + file_open.write("RESULT {r} {t} {s}\n".format( + r=self.result, t=self.name, s=self.summary.strip())) + + def add_summary(self, text): + """Adds summary text to result. + + e.g. RESULT pass test.name + Appends to existing text with a space if any. + + Args: + Text to add. + """ + self.summary = self.summary + " " + text if self.summary else text diff --git a/testing/test_aux.out b/testing/test_aux.out index de4b085911..72c2840a86 100644 --- a/testing/test_aux.out +++ b/testing/test_aux.out @@ -38,21 +38,24 @@ RESULT pass security.discover.firmware version found: ?\xFF\xFF\x19,>u\x08\x00no RESULT pass communication.network.min_send ARP packets received. Data packets were sent at a frequency of less than 5 minutes RESULT pass communication.network.type Broadcast packets received. Unicast packets received. RESULT pass ntp.network.ntp_support Using NTPv4. -RESULT pass ntp.network.ntp_update Device clock synchronized. +RESULT pass ntp.network.ntp_update_dhcp Device clock synchronized. +RESULT skip ntp.network.ntp_update_dns Device not configured for NTP via DNS RESULT fail connection.network.mac_oui Manufacturer prefix not found! RESULT pass connection.network.mac_address Device MAC address is 9a:02:57:1e:8f:01 RESULT skip dns.network.hostname_resolution Device did not send any DNS requests RESULT pass communication.network.min_send ARP packets received. Data packets were sent at a frequency of less than 5 minutes RESULT pass communication.network.type Broadcast packets received. Unicast packets received. RESULT fail ntp.network.ntp_support Not using NTPv4. -RESULT fail ntp.network.ntp_update Device clock not synchronized with local NTP server. +RESULT fail ntp.network.ntp_update_dhcp Device clock not synchronized with local NTP server. +RESULT skip ntp.network.ntp_update_dns Device not configured for NTP via DNS RESULT pass connection.network.mac_oui Manufacturer: Google found for address 3c:5a:b4:1e:8f:0b RESULT pass connection.network.mac_address Device MAC address is 3c:5a:b4:1e:8f:0b RESULT fail dns.network.hostname_resolution Device sent DNS requests to servers other than the DHCP provided server RESULT pass communication.network.min_send ARP packets received. Data packets were sent at a frequency of less than 5 minutes RESULT pass communication.network.type Broadcast packets received. Unicast packets received. RESULT skip ntp.network.ntp_support No NTP packets received. -RESULT skip ntp.network.ntp_update Not enough NTP packets received. +RESULT skip ntp.network.ntp_update_dhcp Not enough NTP packets received. +RESULT skip ntp.network.ntp_update_dns Not enough NTP packets received. RESULT pass connection.network.mac_oui Manufacturer: Google found for address 3c:5a:b4:1e:8f:0a RESULT pass connection.network.mac_address Device MAC address is 3c:5a:b4:1e:8f:0a RESULT pass dns.network.hostname_resolution Device sends DNS requests and resolves host names diff --git a/testing/test_modules.sh b/testing/test_modules.sh index 279db6d058..475ce51f26 100755 --- a/testing/test_modules.sh +++ b/testing/test_modules.sh @@ -27,6 +27,7 @@ cat > $TEST_LIST <