diff --git a/framework/python/src/test_orc/test_orchestrator.py b/framework/python/src/test_orc/test_orchestrator.py index 8fb0b1c85..6a0da9ce0 100644 --- a/framework/python/src/test_orc/test_orchestrator.py +++ b/framework/python/src/test_orc/test_orchestrator.py @@ -276,6 +276,7 @@ def _run_test_module(self, module): environment={ "HOST_USER": self._host_user, "DEVICE_MAC": device.mac_addr, + "IPV4_ADDR": device.ip_addr, "DEVICE_TEST_MODULES": json.dumps(device.test_modules), "IPV4_SUBNET": self._net_orc.network_config.ipv4_network, "IPV6_SUBNET": self._net_orc.network_config.ipv6_network @@ -397,7 +398,7 @@ def _load_test_module(self, module_dir): try: test_case = TestCase( name=test_case_json["name"], - description=test_case_json["description"], + description=test_case_json["test_description"], expected_behavior=test_case_json["expected_behavior"], required_result=test_case_json["required_result"] ) diff --git a/local/system.json.example b/local/system.json.example index 17e5b0891..c640669b4 100644 --- a/local/system.json.example +++ b/local/system.json.example @@ -6,6 +6,6 @@ "log_level": "INFO", "startup_timeout": 60, "monitor_period": 300, - "runtime": 1200, + "runtime": 120, "max_device_reports": 5 -} \ No newline at end of file +} diff --git a/modules/test/base/python/src/test_module.py b/modules/test/base/python/src/test_module.py index 26a6decdf..c36ba0adf 100644 --- a/modules/test/base/python/src/test_module.py +++ b/modules/test/base/python/src/test_module.py @@ -29,6 +29,7 @@ class TestModule: def __init__(self, module_name, log_name): self._module_name = module_name self._device_mac = os.environ['DEVICE_MAC'] + self._ipv4_addr = os.environ['IPV4_ADDR'] self._ipv4_subnet = os.environ['IPV4_SUBNET'] self._ipv6_subnet = os.environ['IPV6_SUBNET'] self._add_logger(log_name=log_name, module_name=module_name) @@ -74,14 +75,18 @@ def _get_device_test_module(self): return None def run_tests(self): + if self._config['config']['network']: self._device_ipv4_addr = self._get_device_ipv4() LOGGER.info('Device IP Resolved: ' + str(self._device_ipv4_addr)) + tests = self._get_tests() for test in tests: test_method_name = '_' + test['name'].replace('.', '_') result = None + test['start'] = datetime.now().isoformat() + if ('enabled' in test and test['enabled']) or 'enabled' not in test: LOGGER.debug('Attempting to run test: ' + test['name']) # Resolve the correct python method by test name and run test @@ -101,30 +106,19 @@ def run_tests(self): else: if result[0] is None: test['result'] = 'Skipped' - if len(result)>1: - test['result_details'] = result[1] + if len(result) > 1: + test['description'] = result[1] else: test['result'] = 'Compliant' if result[0] else 'Non-Compliant' - test['result_details'] = result[1] + test['description'] = result[1] else: test['result'] = 'Skipped' - # Generate the short result description based on result value - if test['result'] == 'Compliant': - test['result_description'] = test[ - 'short_description'] if 'short_description' in test else test[ - 'name'] + ' passed - see result details for more info' - elif test['result'] == 'Non-Compliant': - test['result_description'] = test[ - 'name'] + ' failed - see result details for more info' - else: - test['result_description'] = test[ - 'name'] + ' skipped - see result details for more info' - test['end'] = datetime.now().isoformat() duration = datetime.fromisoformat(test['end']) - datetime.fromisoformat( test['start']) test['duration'] = str(duration) + json_results = json.dumps({'results': tests}, indent=2) self._write_results(json_results) diff --git a/modules/test/baseline/conf/module_config.json b/modules/test/baseline/conf/module_config.json index f3a832ed0..1eb3b48b3 100644 --- a/modules/test/baseline/conf/module_config.json +++ b/modules/test/baseline/conf/module_config.json @@ -14,19 +14,19 @@ "tests":[ { "name": "baseline.compliant", - "description": "Simulate a compliant test", + "test_description": "Simulate a compliant test", "expected_behavior": "A compliant test result is generated", "required_result": "Required" }, { "name": "baseline.non-compliant", - "description": "Simulate a non-compliant test", + "test_description": "Simulate a non-compliant test", "expected_behavior": "A non-compliant test result is generated", "required_result": "Required" }, { "name": "baseline.informational", - "description": "Simulate an informational test", + "test_description": "Simulate an informational test", "expected_behavior": "An informational test result is generated", "required_result": "Informational" } diff --git a/modules/test/baseline/python/src/baseline_module.py b/modules/test/baseline/python/src/baseline_module.py index 220cb869d..111db708e 100644 --- a/modules/test/baseline/python/src/baseline_module.py +++ b/modules/test/baseline/python/src/baseline_module.py @@ -38,6 +38,6 @@ def _baseline_non_compliant(self): return False, 'Baseline non-compliant test ran successfully' def _baseline_informational(self): - LOGGER.info('Running baseline skip test') - LOGGER.info('Baseline skip test finished') - return None, 'Baseline skip test ran successfully' + LOGGER.info('Running baseline informational test') + LOGGER.info('Baseline informational test finished') + return None, 'Baseline informational test ran successfully' diff --git a/modules/test/conn/conf/module_config.json b/modules/test/conn/conf/module_config.json index c358ba1c2..1fb7fab0c 100644 --- a/modules/test/conn/conf/module_config.json +++ b/modules/test/conn/conf/module_config.json @@ -15,37 +15,37 @@ "tests": [ { "name": "connection.dhcp.disconnect", - "description": "The device under test has received an IP address from the DHCP server and responds to an ICMP echo (ping) request", + "test_description": "The device under test has received an IP address from the DHCP server and responds to an ICMP echo (ping) request", "expected_behavior": "The device is not setup with a static IP address. The device accepts an IP address from a DHCP server (RFC 2131) and responds succesfully to an ICMP echo (ping) request.", "required_result": "Required" }, { "name": "connection.dhcp.disconnect_ip_change", - "description": "Update device IP on the DHCP server and reconnect the device. Does the device receive the new IP address?", + "test_description": "Update device IP on the DHCP server and reconnect the device. Does the device receive the new IP address?", "expected_behavior": "Device recieves a new IP address within the range that is specified on the DHCP server. Device should respond to aping on this new address.", "required_result": "Required" }, { "name": "connection.dhcp_address", - "description": "The device under test has received an IP address from the DHCP server and responds to an ICMP echo (ping) request", + "test_description": "The device under test has received an IP address from the DHCP server and responds to an ICMP echo (ping) request", "expected_behavior": "The device is not setup with a static IP address. The device accepts an IP address from a DHCP server (RFC 2131) and responds succesfully to an ICMP echo (ping) request.", "required_result": "Required" }, { "name": "connection.mac_address", - "description": "Check and note device physical address.", + "test_description": "Check and note device physical address.", "expected_behavior": "N/A", "required_result": "Required" }, { "name": "connection.mac_oui", - "description": "The device under test hs a MAC address prefix that is registered against a known manufacturer.", + "test_description": "The device under test hs a MAC address prefix that is registered against a known manufacturer.", "expected_behavior": "The MAC address prefix is registered in the IEEE Organizationally Unique Identifier database.", "required_result": "Required" }, { "name": "connection.private_address", - "description": "The device under test accepts an IP address that is compliant with RFC 1918 Address Allocation for Private Internets.", + "test_description": "The device under test accepts an IP address that is compliant with RFC 1918 Address Allocation for Private Internets.", "expected_behavior": "The device under test accepts IP addresses within all ranges specified in RFC 1918 and communicates using these addresses. The Internet Assigned Numbers Authority (IANA) has reserved the following three blocks of the IP address space for private internets. 10.0.0.0 - 10.255.255.255.255 (10/8 prefix). 172.16.0.0 - 172.31.255.255 (172.16/12 prefix). 192.168.0.0 - 192.168.255.255 (192.168/16 prefix)", "required_result": "Required", "config": { @@ -67,7 +67,7 @@ }, { "name": "connection.shared_address", - "description": "Ensure the device supports RFC 6598 IANA-Reserved IPv4 Prefix for Shared Address Space", + "test_description": "Ensure the device supports RFC 6598 IANA-Reserved IPv4 Prefix for Shared Address Space", "expected_behavior": "The device under test accepts IP addresses within the ranges specified in RFC 6598 and communicates using these addresses", "required_result": "Required", "config": { @@ -81,7 +81,7 @@ }, { "name": "connection.private_address", - "description": "The device under test accepts an IP address that is compliant with RFC 1918 Address Allocation for Private Internets.", + "test_description": "The device under test accepts an IP address that is compliant with RFC 1918 Address Allocation for Private Internets.", "expected_behavior": "The device under test accepts IP addresses within all ranges specified in RFC 1918 and communicates using these addresses. The Internet Assigned Numbers Authority (IANA) has reserved the following three blocks of the IP address space for private internets. 10.0.0.0 - 10.255.255.255.255 (10/8 prefix). 172.16.0.0 - 172.31.255.255 (172.16/12 prefix). 192.168.0.0 - 192.168.255.255 (192.168/16 prefix)", "required_result": "Required", "config": [ @@ -101,37 +101,37 @@ }, { "name": "connection.single_ip", - "description": "The network switch port connected to the device reports only one IP address for the device under test.", + "test_description": "The network switch port connected to the device reports only one IP address for the device under test.", "expected_behavior": "The device under test does not behave as a network switch and only requets one IP address. This test is to avoid that devices implement network switches that allow connecting strings of daisy chained devices to one single network port, as this would not make 802.1x port based authentication possible.", "required_result": "Required" }, { "name": "connection.target_ping", - "description": "The device under test responds to an ICMP echo (ping) request.", + "test_description": "The device under test responds to an ICMP echo (ping) request.", "expected_behavior": "The device under test responds to an ICMP echo (ping) request.", "required_result": "Required" }, { "name": "connection.ipaddr.ip_change", - "description": "The device responds to a ping (ICMP echo request) to the new IP address it has received after the initial DHCP lease has expired.", + "test_description": "The device responds to a ping (ICMP echo request) to the new IP address it has received after the initial DHCP lease has expired.", "expected_behavior": "If the lease expires before the client receiveds a DHCPACK, the client moves to INIT state, MUST immediately stop any other network processing and requires network initialization parameters as if the client were uninitialized. If the client then receives a DHCPACK allocating the client its previous network addres, the client SHOULD continue network processing. If the client is given a new network address, it MUST NOT continue using the previous network address and SHOULD notify the local users of the problem.", "required_result": "Required" }, { "name": "connection.ipaddr.dhcp_failover", - "description": "The device has requested a DHCPREQUEST/REBIND to the DHCP failover server after the primary DHCP server has been brought down.", + "test_description": "The device has requested a DHCPREQUEST/REBIND to the DHCP failover server after the primary DHCP server has been brought down.", "expected_behavior": "", "required_result": "Required" }, { "name": "connection.ipv6_slaac", - "description": "The device forms a valid IPv6 address as a combination of the IPv6 router prefix and the device interface identifier", + "test_description": "The device forms a valid IPv6 address as a combination of the IPv6 router prefix and the device interface identifier", "expected_behavior": "The device under test complies with RFC4862 and forms a valid IPv6 SLAAC address", "required_result": "Required" }, { "name": "connection.ipv6_ping", - "description": "The device responds to an IPv6 ping (ICMPv6 Echo) request to the SLAAC address", + "test_description": "The device responds to an IPv6 ping (ICMPv6 Echo) request to the SLAAC address", "expected_behavior": "The device responds to the ping as per RFC4443", "required_result": "Required" } diff --git a/modules/test/dns/conf/module_config.json b/modules/test/dns/conf/module_config.json index e00061047..a30e28077 100644 --- a/modules/test/dns/conf/module_config.json +++ b/modules/test/dns/conf/module_config.json @@ -14,19 +14,19 @@ "tests":[ { "name": "dns.network.hostname_resolution", - "description": "Verify the device sends DNS requests", + "test_description": "Verify the device sends DNS requests", "expected_behavior": "The device sends DNS requests.", "required_result": "Required" }, { "name": "dns.network.from_dhcp", - "description": "Verify the device allows for a DNS server to be entered automatically", + "test_description": "Verify the device allows for a DNS server to be entered automatically", "expected_behavior": "The device sends DNS requests to the DNS server provided by the DHCP server", "required_result": "Roadmap" }, { "name": "dns.mdns", - "description": "If the device has MDNS (or any kind of IP multicast), can it be disabled", + "test_description": "If the device has MDNS (or any kind of IP multicast), can it be disabled", "expected_behavior": "Device may send MDNS requests", "required_result": "Recommended" } diff --git a/modules/test/nmap/conf/module_config.json b/modules/test/nmap/conf/module_config.json index 8a90febc1..c7aa644f8 100644 --- a/modules/test/nmap/conf/module_config.json +++ b/modules/test/nmap/conf/module_config.json @@ -13,166 +13,323 @@ }, "tests": [ { - "name": "security.nmap.ports", - "description": "Run an nmap scan of open ports", - "expected_behavior": "Report all open ports", + "name": "security.services.ftp", + "test_description": "Check FTP port 20/21 is disabled and FTP is not running on any port", + "expected_behavior": "There is no FTP service running on any port", + "required_result": "Required", "config": { - "security.services.ftp": { - "tcp_ports": { - "20": { - "allowed": false, - "description": "File Transfer Protocol (FTP) Server Data Transfer" - }, - "21": { - "allowed": false, - "description": "File Transfer Protocol (FTP) Server Data Transfer" - } - }, - "description": "Check FTP port 20/21 is disabled and FTP is not running on any port", - "expected_behavior": "There is no FTP service running on any port", - "required_result": "Required" - }, - "security.ssh.version": { - "tcp_ports": { - "22": { - "allowed": true, - "description": "Secure Shell (SSH) server", - "version": "2.0" - } - }, - "description": "If the device is running a SSH server ensure it is SSHv2", - "expected_behavior": "SSH server is not running or server is SSHv2", - "required_result": "Required" - }, - "security.services.telnet": { - "tcp_ports": { - "23": { - "allowed": false, - "description": "Telnet Server" - } - }, - "description": "Check TELNET port 23 is disabled and TELNET is not running on any port", - "expected_behavior": "There is no FTP service running on any port", - "required_result": "Required" - }, - "security.services.smtp": { - "tcp_ports": { - "25": { - "allowed": false, - "description": "Simple Mail Transfer Protocol (SMTP) Server" - }, - "465": { - "allowed": false, - "description": "Simple Mail Transfer Protocol over SSL (SMTPS) Server" - }, - "587": { - "allowed": false, - "description": "Simple Mail Transfer Protocol via TLS (SMTPS) Server" - } - }, - "description": "Check SMTP ports 25, 465 and 587 are not enabled and SMTP is not running on any port.", - "expected_behavior": "There is no smtp service running on any port", - "required_result": "Required" - }, - "security.services.http": { - "tcp_ports": { - "80": { - "service_scan": { - "script": "http-methods" - }, - "allowed": false, - "description": "Administrative Insecure Web-Server" - } - }, - "description": "Check that there is no HTTP server running on any port", - "expected_behavior": "Device is unreachable on port 80 (or any other port) and only responds to HTTPS requests on port 443 (or any other port if HTTP is used at all)", - "required_result": "Required" - }, - "security.services.pop": { - "tcp_ports": { - "110": { - "allowed": false, - "description": "Post Office Protocol v3 (POP3) Server" - } - }, - "description": "Check POP port 110 is disalbed and POP is not running on any port", - "expected_behavior": "There is no pop service running on any port", - "required_result": "Required" - }, - "security.services.imap": { - "tcp_ports": { - "143": { - "allowed": false, - "description": "Internet Message Access Protocol (IMAP) Server" - } - }, - "description": "Check IMAP port 143 is disabled and IMAP is not running on any port", - "expected_behavior": "There is no imap service running on any port", - "required_result": "Required" - }, - "security.services.snmpv3": { - "tcp_ports": { - "161": { - "allowed": false, - "description": "Simple Network Management Protocol (SNMP)" - }, - "162": { - "allowed": false, - "description": "Simple Network Management Protocol (SNMP) Trap" - } - }, - "udp_ports": { - "161": { - "allowed": false, - "description": "Simple Network Management Protocol (SNMP)" - }, - "162": { - "allowed": false, - "description": "Simple Network Management Protocol (SNMP) Trap" - } - }, - "description": "Check SNMP port 161/162 is disabled. If SNMP is an essential service, check it supports version 3", - "expected_behavior": "Device is unreachable on port 161 (or any other port) and device is unreachable on port 162 (or any other port) unless SNMP is essential in which case it is SNMPv3 is used.", - "required_result": "Required" - }, - "security.services.vnc": { - "tcp_ports": { - "5800": { - "allowed": false, - "description": "Virtual Network Computing (VNC) Remote Frame Buffer Protocol Over HTTP" - }, - "5500": { - "allowed": false, - "description": "Virtual Network Computing (VNC) Remote Frame Buffer Protocol" - } - }, - "description": "Check VNC is disabled on any port", - "expected_behavior": "Device cannot be accessed /connected to via VNC on any port", - "required_result": "Required" - }, - "security.services.tftp": { - "udp_ports": { - "69": { - "allowed": false, - "description": "Trivial File Transfer Protocol (TFTP) Server" - } - }, - "description": "Check TFTP port 69 is disabled (UDP)", - "expected_behavior": "There is no tftp service running on any port", - "required_result": "Required" - }, - "ntp.network.ntp_server": { - "udp_ports": { - "123": { - "allowed": false, - "description": "Network Time Protocol (NTP) Server" - } - }, - "description": "Check NTP port 123 is disabled and the device is not operating as an NTP server", - "expected_behavior": "The device dos not respond to NTP requests when it's IP is set as the NTP server on another device" - } - }, - "required_result": "Required" + "services": [ + "ftp", + "ftp-data" + ], + "ports": [ + { + "number": 20, + "type": "tcp" + }, + { + "number": 20, + "type": "udp" + }, + { + "number": 21, + "type": "tcp" + }, + { + "number": 21, + "type": "udp" + } + ] + } + }, + { + "name": "security.ssh.version", + "test_description": "If the device is running a SSH server ensure it is SSHv2", + "expected_behavior": "SSH server is not running or server is SSHv2", + "required_result": "Required", + "config": { + "services": ["ssh"], + "ports": [ + { + "number": 22, + "type": "tcp" + } + ], + "version": "protocol 2.0" + } + }, + { + "name": "security.services.telnet", + "test_description": "Check TELNET port 23 is disabled and TELNET is not running on any port", + "expected_behavior": "There is no FTP service running on any port", + "required_result": "Required", + "config": { + "services": [ + "telnet" + ], + "ports": [ + { + "number": 23, + "type": "tcp" + }, + { + "number": 23, + "type": "udp" + } + ] + } + }, + { + "name": "security.services.smtp", + "test_description": "Check SMTP ports 25, 465 and 587 are not enabled and SMTP is not running on any port.", + "expected_behavior": "There is no smtp service running on any port", + "required_result": "Required", + "config": { + "services": [ + "smtp" + ], + "ports": [ + { + "number": 25, + "type": "tcp" + }, + { + "number": 465, + "type": "tcp" + }, + { + "number": 587, + "type": "tcp" + } + ] + } + }, + { + "name": "security.services.http", + "test_description": "Check that there is no HTTP server running on any port", + "expected_behavior": "Device is unreachable on port 80 (or any other port) and only responds to HTTPS requests on port 443 (or any other port if HTTP is used at all)", + "required_result": "Required", + "config": { + "services": [ + "http" + ], + "ports": [ + { + "number": 80, + "type": "tcp" + }, + { + "number": 80, + "type": "udp" + } + ] + } + }, + { + "name": "security.services.pop", + "test_description": "Check POP ports 109 and 110 are disabled and POP is not running on any port", + "expected_behavior": "There is no pop service running on any port", + "required_result": "Required", + "config": { + "services": [ + "pop2", + "pop3", + "pop3s" + ], + "ports": [ + { + "number": 109, + "type": "tcp" + }, + { + "number": 109, + "type": "udp" + }, + { + "number": 110, + "type": "tcp" + }, + { + "number": 110, + "type": "udp" + }, + { + "number": 995, + "type": "tcp" + }, + { + "number": 995, + "type": "udp" + } + ] + } + }, + { + "name": "security.services.imap", + "test_description": "Check IMAP port 143 is disabled and IMAP is not running on any port", + "expected_behavior": "There is no imap service running on any port", + "required_result": "Required", + "config": { + "services": [ + "imap", + "imap3", + "imap4-ssl" + ], + "ports": [ + { + "number": 143, + "type": "tcp" + }, + { + "number": 143, + "type": "udp" + }, + { + "number": 220, + "type": "tcp" + }, + { + "number": 220, + "type": "udp" + }, + { + "number": 585, + "type": "tcp" + }, + { + "number": 585, + "type": "udp" + }, + { + "number": 993, + "type": "tcp" + }, + { + "number": 993, + "type": "udp" + } + ] + } + }, + { + "name": "security.services.snmpv3", + "test_description": "Check SNMP port 161/162 is disabled. If SNMP is an essential service, check it supports version 3", + "expected_behavior": "Device is unreachable on port 161 (or any other port) and device is unreachable on port 162 (or any other port) unless SNMP is essential in which case it is SNMPv3 is used.", + "required_result": "Required", + "config": { + "services": [ + "snmp" + ], + "ports": [ + { + "number": 161, + "type": "tcp" + }, + { + "number": 161, + "type": "udp" + } + ] + } + }, + { + "name": "security.services.vnc", + "test_description": "Check VNC is disabled on any port", + "expected_behavior": "Device cannot be accessed /connected to via VNC on any port", + "required_result": "Required", + "config": { + "services": [ + "vnc", + "vnc-1", + "vnc-2", + "vnc-3", + "vnc-http", + "vnc-http-1", + "vnc-http-2", + "vnc-http-3" + ], + "ports": [ + { + "number": 5800, + "type": "tcp" + }, + { + "number": 5801, + "type": "tcp" + }, + { + "number": 5802, + "type": "tcp" + }, + { + "number": 5803, + "type": "tcp" + }, + { + "number": 5900, + "type": "tcp" + }, + { + "number": 5901, + "type": "tcp" + }, + { + "number": 5902, + "type": "tcp" + }, + { + "number": 5903, + "type": "tcp" + } + ] + } + }, + { + "name": "security.services.tftp", + "test_description": "Check TFTP port 69 is disabled (UDP)", + "expected_behavior": "There is no tftp service running on any port", + "required_result": "Required", + "config": { + "services": [ + "tftp", + "tftps" + ], + "ports": [ + { + "number": 69, + "type": "tcp" + }, + { + "number": 69, + "type": "udp" + }, + { + "number": 3713, + "type": "tcp" + }, + { + "number": 3713, + "type": "udp" + } + ] + } + }, + { + "name": "ntp.network.ntp_server", + "test_description": "Check NTP port 123 is disabled and the device is not operating as an NTP server", + "expected_behavior": "The device dos not respond to NTP requests when it's IP is set as the NTP server on another device", + "required_result": "Required", + "config": { + "services": [ + "ntp" + ], + "ports": [ + { + "number": 123, + "type": "udp" + } + ] + } } ] } diff --git a/modules/test/nmap/python/src/nmap_module.py b/modules/test/nmap/python/src/nmap_module.py index 94597f03e..cb990fc58 100644 --- a/modules/test/nmap/python/src/nmap_module.py +++ b/modules/test/nmap/python/src/nmap_module.py @@ -18,7 +18,6 @@ import json import threading import xmltodict -import re from test_module import TestModule LOG_NAME = "test_nmap" @@ -30,329 +29,83 @@ class NmapModule(TestModule): def __init__(self, module): super().__init__(module_name=module, log_name=LOG_NAME) - self._unallowed_ports = [] self._scan_tcp_results = None self._udp_tcp_results = None - self._script_scan_results = None + self._scan_results = {} + global LOGGER LOGGER = self._get_logger() + self._run_nmap() - def _security_nmap_ports(self, config): - LOGGER.info("Running security.nmap.ports test") - result = None - - # Delete the enabled key from the config if it exists - # to prevent it being treated as a test key - if "enabled" in config: - del config["enabled"] - - if self._device_ipv4_addr is not None: - # Run the monitor method asynchronously to keep this method non-blocking - self._tcp_scan_thread = threading.Thread(target=self._scan_tcp_ports, - args=(config, )) - self._udp_scan_thread = threading.Thread(target=self._scan_udp_ports, - args=(config, )) - self._script_scan_thread = threading.Thread(target=self._scan_scripts, - args=(config, )) - - self._tcp_scan_thread.daemon = True - self._udp_scan_thread.daemon = True - self._script_scan_thread.daemon = True - - self._tcp_scan_thread.start() - self._udp_scan_thread.start() - self._script_scan_thread.start() - - while self._tcp_scan_thread.is_alive() or self._udp_scan_thread.is_alive( - ) or self._script_scan_thread.is_alive(): - time.sleep(1) - - LOGGER.debug("TCP scan results: " + str(self._scan_tcp_results)) - LOGGER.debug("UDP scan results: " + str(self._scan_udp_results)) - LOGGER.debug("Service scan results: " + str(self._script_scan_results)) - self._process_port_results(tests=config) - LOGGER.info("Unallowed Ports Detected: " + str(self._unallowed_ports)) - self._check_unallowed_port(self._unallowed_ports,config) - LOGGER.info("Unallowed Ports: " + str(self._unallowed_ports)) - if len(self._unallowed_ports) > 0: - result = False, 'Some allowed ports detected: ' + str(self._unallowed_ports) - else: - result = True, 'No unallowed ports detected' - else: - LOGGER.info("Device ip address not resolved, skipping") - result = None, "Device ip address not resolved" - return result + def _run_nmap(self): + LOGGER.info("Running nmap module") + + # Run the monitor method asynchronously to keep this method non-blocking + self._tcp_scan_thread = threading.Thread(target=self._scan_tcp_ports) + self._udp_scan_thread = threading.Thread(target=self._scan_udp_ports) + + self._tcp_scan_thread.daemon = True + self._udp_scan_thread.daemon = True + + self._tcp_scan_thread.start() + self._udp_scan_thread.start() + + while (self._tcp_scan_thread.is_alive() or + self._udp_scan_thread.is_alive()): + time.sleep(1) + + LOGGER.debug("TCP scan results: " + str(self._scan_tcp_results)) + LOGGER.debug("UDP scan results: " + str(self._scan_udp_results)) + + self._process_port_results() + + def _process_port_results(self): - def _process_port_results(self, tests): - scan_results = {} if self._scan_tcp_results is not None: - scan_results.update(self._scan_tcp_results) + self._scan_results.update(self._scan_tcp_results) if self._scan_udp_results is not None: - scan_results.update(self._scan_udp_results) - if self._script_scan_results is not None: - scan_results.update(self._script_scan_results) - - self._check_unknown_ports(tests=tests,scan_results=scan_results) - - for test in tests: - LOGGER.info("Checking scan results for test: " + str(test)) - self._check_scan_results(test_config=tests[test],scan_results=scan_results) - - def _check_unknown_ports(self,tests,scan_results): - """ Check if any of the open ports detected are not defined - in the test configurations. If an open port is detected - without a configuration associated with it, the default behavior - is to mark it as an unallowed port. - """ - known_ports = [] - for test in tests: - if "tcp_ports" in tests[test]: - for port in tests[test]['tcp_ports']: - known_ports.append(port) - if "udp_ports" in tests[test]: - for port in tests[test]['udp_ports']: - known_ports.append(port) - - for port_result in scan_results: - if not port_result in known_ports: - LOGGER.info("Unknown port detected: " + port_result) - unallowed_port = {'port':port_result, - 'service':scan_results[port_result]['service'], - 'tcp_udp':scan_results[port_result]['tcp_udp']} - #self._unallowed_ports.append(unallowed_port) - self._add_unknown_ports(tests,unallowed_port) - - def _add_unknown_ports(self,tests,unallowed_port): - known_service = False - result = {'description':"Undefined port",'allowed':False} - if unallowed_port['tcp_udp'] == 'tcp': - port_style = 'tcp_ports' - elif unallowed_port['tcp_udp'] == 'udp': - port_style = 'udp_ports' - - LOGGER.info("Unknown Port Service: " + unallowed_port['service']) - for test in tests: - LOGGER.debug("Checking for known service: " + test) - # Create a regular expression pattern to match the variable at the - # end of the string - port_service = r"\b" + re.escape(unallowed_port['service']) + r"\b$" - service_match = re.search(port_service, test) - if service_match: - LOGGER.info("Service Matched: " + test) - known_service=True - for test_port in tests[test][port_style]: - if "version" in tests[test][port_style][test_port]: - result['version'] = tests[test][port_style][test_port]['version'] - if "description" in tests[test][port_style][test_port]: - result['description'] = tests[test][port_style][test_port]['description'] - result['inherited_from'] = test_port - if tests[test][port_style][test_port]['allowed']: - result['allowed'] = True - break - tests[test][port_style][unallowed_port['port']]=result - break - - if not known_service: - service_name = "security.services.unknown." + str(unallowed_port['port']) - unknown_service = {port_style:{unallowed_port['port']:result}} - tests[service_name]=unknown_service - - - def _check_scan_results(self,test_config,scan_results): - if "tcp_ports" in test_config: - port_config = test_config["tcp_ports"] - self._check_scan_result(port_config=port_config,scan_results=scan_results) - if "udp_ports" in test_config: - port_config = test_config["udp_ports"] - self._check_scan_result(port_config=port_config,scan_results=scan_results) - - def _check_scan_result(self,port_config,scan_results): - if port_config is not None: - for port, config in port_config.items(): - result = None - LOGGER.info("Checking port: " + str(port)) - LOGGER.debug("Port config: " + str(config)) - if port in scan_results: - if scan_results[port]["state"] == "open": - if not config["allowed"]: - LOGGER.info("Unallowed port open") - self._unallowed_ports.append( - {"port":str(port), - "service":str(scan_results[port]["service"]), - 'tcp_udp':scan_results[port]['tcp_udp']} - ) - result = False - else: - LOGGER.info("Allowed port open") - if "version" in config and "version" in scan_results[port]: - version_check = self._check_version(scan_results[port]["service"], - scan_results[port]["version"],config["version"]) - if version_check is not None: - result = version_check - else: - result = True - else: - result = True - else: - LOGGER.info("Port is closed") - result = True - else: - LOGGER.info("Port not detected, closed") - result = True - - if result is not None: - config["result"] = "compliant" if result else "non-compliant" - else: - config["result"] = "skipped" - - def _check_unallowed_port(self,unallowed_ports,tests): - service_allowed=False - allowed = False - version = None - service = None - for port in unallowed_ports: - LOGGER.info("Checking unallowed port: " + port["port"]) - LOGGER.info("Looking for service: " + port["service"]) - LOGGER.debug("Unallowed Port Config: " + str(port)) - if port["tcp_udp"] == "tcp": - port_style = "tcp_ports" - elif port["tcp_udp"] == "udp": - port_style = "udp_ports" - for test in tests: - LOGGER.debug("Checking test: " + str(test)) - # Create a regular expression pattern to match the variable at the - # end of the string - port_service = r"\b" + re.escape(port['service']) + r"\b$" - service_match = re.search(port_service, test) - if service_match: - LOGGER.info("Service Matched: " + test) - service_config = tests[test] - service = port['service'] - for service_port in service_config[port_style]: - port_config = service_config[port_style][service_port] - service_allowed |= port_config['allowed'] - version = port_config['version'] if 'version' in port_config else None - if service_allowed: - LOGGER.info("Unallowed port detected for allowed service: " + service) - if version is not None: - allowed = self._check_version(service=service, - version_detected=self._scan_tcp_results[port['port']]['version'], - version_expected=version) - else: - allowed = True - if allowed: - LOGGER.info("Unallowed port exception for approved service: " + port['port']) - for u_port in self._unallowed_ports: - if port['port'] in u_port['port']: - self._unallowed_ports.remove(u_port) - break - break - - def _check_version(self,service,version_detected,version_expected): - """Check if the version specified for the service matches what was - detected by nmap. Since there is no consistency in how nmap service - results are returned, each service that needs a checked must be - implemented individually. If a service version is requested - that is not implemented, this test will provide a skip (None) - result. - """ - LOGGER.info("Checking version for service: " + service) - LOGGER.info("NMAP Version Detected: " + version_detected) - LOGGER.info("Version Expected: " + version_expected) - version_check = None - match service: - case "ssh": - version_check = f"protocol {version_expected}" in version_detected - case _: - LOGGER.info("No version check implemented for service: " + service + ". Skipping") - LOGGER.info("Version check result: " + str(version_check)) - return version_check - - def _scan_scripts(self, tests): - scan_results = {} - LOGGER.info("Checking for scan scripts") - for test in tests: - test_config = tests[test] - if "tcp_ports" in test_config: - for port in test_config["tcp_ports"]: - port_config = test_config["tcp_ports"][port] - if "service_scan" in port_config: - LOGGER.info("Service Scan Detected for: " + str(port)) - svc = port_config["service_scan"] - result = self._scan_tcp_with_script(svc["script"]) - scan_results.update(result) - if "udp_ports" in test_config: - for port in test_config["udp_ports"]: - if "service_scan" in port: - LOGGER.info("Service Scan Detected for: " + str(port)) - svc = port["service_scan"] - result = self._scan_udp_with_script(svc["script"], port) - scan_results.update(result) - self._script_scan_results = scan_results - - def _scan_tcp_with_script(self, script_name, ports=None): - LOGGER.info("Running TCP nmap scan with script " + script_name) - scan_options = " -v -n T3 --host-timeout=6m -A --script " + script_name - port_options = " --open " - if ports is None: - port_options += " -p- " - else: - port_options += " -p" + ports + " " - results_file = f"/runtime/output/{self._module_name}-script_name.log" - nmap_options = scan_options + port_options + " " + results_file + " -oX -" - nmap_results = util.run_command("nmap " + nmap_options + " " + - self._device_ipv4_addr)[0] - LOGGER.info("Nmap TCP script scan complete") - nmap_results_json = self._nmap_results_to_json(nmap_results) - return self._process_nmap_json_results(nmap_results_json=nmap_results_json) - - def _scan_udp_with_script(self, script_name, ports=None): - LOGGER.info("Running UDP nmap scan with script " + script_name) - scan_options = " --sU -Pn -n --script " + script_name - port_options = " --open " - if ports is None: - port_options += " -p- " - else: - port_options += " -p" + ports + " " - nmap_options = scan_options + port_options + " -oX - " - nmap_results = util.run_command("nmap " + nmap_options + - self._device_ipv4_addr)[0] - LOGGER.info("Nmap UDP script scan complete") - nmap_results_json = self._nmap_results_to_json(nmap_results) - return self._process_nmap_json_results(nmap_results_json=nmap_results_json) + self._scan_results.update(self._scan_udp_results) - def _scan_tcp_ports(self, tests): - max_port = 65535 + def _scan_tcp_ports(self): + max_port = 1000 LOGGER.info("Running nmap TCP port scan") nmap_results = util.run_command( f"""nmap --open -sT -sV -Pn -v -p 1-{max_port} - --version-intensity 7 -T4 -oX - {self._device_ipv4_addr}""")[0] + --version-intensity 7 -T4 -oX - {self._ipv4_addr}""")[0] LOGGER.info("TCP port scan complete") nmap_results_json = self._nmap_results_to_json(nmap_results) self._scan_tcp_results = self._process_nmap_json_results( nmap_results_json=nmap_results_json) - def _scan_udp_ports(self, tests): + def _scan_udp_ports(self): + ports = [] - for test in tests: - test_config = tests[test] - if "udp_ports" in test_config: - for port in test_config["udp_ports"]: - ports.append(port) + + for test in self._get_tests(): + if "config" not in test: + continue + test_config = test["config"] + if "ports" not in test_config: + continue + + for port in test_config["ports"]: + if port["type"] == "udp": + ports.append(str(port["number"])) + if len(ports) > 0: port_list = ",".join(ports) LOGGER.info("Running nmap UDP port scan") - LOGGER.info("UDP ports: " + str(port_list)) + LOGGER.debug("UDP ports: " + str(port_list)) nmap_results = util.run_command( - f"nmap -sU -sV -p {port_list} -oX - {self._device_ipv4_addr}")[0] + f"nmap -sU -sV -p {port_list} -oX - {self._ipv4_addr}")[0] LOGGER.info("UDP port scan complete") nmap_results_json = self._nmap_results_to_json(nmap_results) self._scan_udp_results = self._process_nmap_json_results( nmap_results_json=nmap_results_json) - def _nmap_results_to_json(self,nmap_results): + def _nmap_results_to_json(self, nmap_results): try: xml_data = xmltodict.parse(nmap_results) json_data = json.dumps(xml_data, indent=4) @@ -361,29 +114,166 @@ def _nmap_results_to_json(self,nmap_results): except Exception as e: LOGGER.error(f"Error parsing Nmap output: {e}") - def _process_nmap_json_results(self,nmap_results_json): - LOGGER.debug("nmap results\n" + json.dumps(nmap_results_json,indent=2)) + def _process_nmap_json_results(self, nmap_results_json): results = {} if "ports" in nmap_results_json["nmaprun"]["host"]: - ports = nmap_results_json["nmaprun"]["host"]["ports"] + ports = nmap_results_json["nmaprun"]["host"]["ports"] # Checking if an object is a JSON object if isinstance(ports["port"], dict): results.update(self._json_port_to_dict(ports["port"])) elif isinstance(ports["port"], list): for port in ports["port"]: results.update(self._json_port_to_dict(port)) + print(str(results)) return results - def _json_port_to_dict(self,port_json): + def _json_port_to_dict(self, port_json): port_result = {} port = {} + port["number"] = port_json["@portid"] port["tcp_udp"] = port_json["@protocol"] port["state"] = port_json["state"]["@state"] port["service"] = port_json["service"]["@name"] port["version"] = "" if "@version" in port_json["service"]: port["version"] += port_json["service"]["@version"] - if "@extrainfo" in port_json["service"]: - port["version"] += " " + port_json["service"]["@extrainfo"] - port_result = {port_json["@portid"]:port} + if "@extrainfo" in port_json["service"]: + port["version"] += " " + port_json["service"]["@extrainfo"] + port_result = {port_json["@portid"] + port["tcp_udp"]:port} return port_result + + def _check_results(self, ports, services): + + LOGGER.info("Checking results") + + match_ports = [] + + for open_port, open_port_info in self._scan_results.items(): + + for port in ports: + + if (int(open_port_info["number"]) == int(port["number"]) and + open_port_info["tcp_udp"] == port["type"] and + open_port_info["state"] == "open"): + LOGGER.debug("Found open port: " + str(port["number"]) + + "/" + open_port_info["tcp_udp"] + + " = " + open_port_info["state"]) + match_ports.append(open_port_info["number"] + "/" + + open_port_info["tcp_udp"]) + + if (open_port_info["service"] in services and + open_port not in match_ports and + open_port_info["state"] == "open"): + LOGGER.debug("Found service " + open_port_info["service"] + + " on port " + str(open_port) + "/" + + open_port_info["tcp_udp"]) + match_ports.append(open_port_info["number"] + "/" + + open_port_info["tcp_udp"]) + + return match_ports + + def _security_services_ftp(self, config): + LOGGER.info("Running security.services.ftp") + + open_ports = self._check_results(config["ports"], config["services"]) + if len(open_ports) == 0: + return True, "No FTP server found" + else: + return False, f"Found FTP server running on port {', '.join(open_ports)}" + + def _security_services_telnet(self, config): + LOGGER.info("Running security.services.telnet") + + open_ports = self._check_results(config["ports"], config["services"]) + if len(open_ports) == 0: + return True, "No telnet server found" + else: + return False, f"Found telnet server running on port {', '.join(open_ports)}" + + def _security_services_smtp(self, config): + LOGGER.info("Running security.services.smtp") + + open_ports = self._check_results(config["ports"], config["services"]) + if len(open_ports) == 0: + return True, "No SMTP server found" + else: + return False, f"Found SMTP server running on port {', '.join(open_ports)}" + + def _security_services_http(self, config): + LOGGER.info("Running security.services.http") + + open_ports = self._check_results(config["ports"], config["services"]) + if len(open_ports) == 0: + return True, "No HTTP server found" + else: + return False, f"Found HTTP server running on port {', '.join(open_ports)}" + + def _security_services_pop(self, config): + LOGGER.info("Running security.services.pop") + + open_ports = self._check_results(config["ports"], config["services"]) + if len(open_ports) == 0: + return True, "No POP server found" + else: + return False, f"Found POP server running on port {', '.join(open_ports)}" + + def _security_services_imap(self, config): + LOGGER.info("Running security.services.imap") + + open_ports = self._check_results(config["ports"], config["services"]) + if len(open_ports) == 0: + return True, "No IMAP server found" + else: + return False, f"Found IMAP server running on port {', '.join(open_ports)}" + + def _security_services_snmpv3(self, config): + LOGGER.info("Running security.services.snmpv3") + + open_ports = self._check_results(config["ports"], config["services"]) + if len(open_ports) == 0: + return True, "No SNMP server found" + else: + return False, f"Found SNMP server running on port {', '.join(open_ports)}" + + def _security_services_vnc(self, config): + LOGGER.info("Running ntp.services.vnc") + + open_ports = self._check_results(config["ports"], config["services"]) + if len(open_ports) == 0: + return True, "No VNC server found" + else: + return False, f"Found VNC server running on port {', '.join(open_ports)}" + + def _security_services_tftp(self, config): + LOGGER.info("Running security.services.tftp") + + open_ports = self._check_results(config["ports"], config["services"]) + if len(open_ports) == 0: + return True, "No TFTP server found" + else: + return False, f"Found TFTP server running on port {', '.join(open_ports)}" + + def _ntp_network_ntp_server(self, config): + LOGGER.info("Running ntp.network.ntp_server") + + open_ports = self._check_results(config["ports"], config["services"]) + if len(open_ports) == 0: + return True, "No NTP server found" + else: + return False, f"Found NTP server running on port {', '.join(open_ports)}" + + def _security_ssh_version(self, config): + LOGGER.info("Running security.ssh.version") + + open_ports = self._check_results(config["ports"], config["services"]) + if len(open_ports) == 0: + return True, "No SSH server found" + else: + # Perform version check + for open_port, open_port_info in self._scan_results.items(): + if ((open_port == 22 or open_port_info["service"] == "ssh") and + open_port_info["state"] == "open"): + if config["version"] in open_port_info["version"]: + return True, f"SSH server found running {open_port_info['version']}" + else: + return False, f"SSH server found running {open_port_info['version']}" diff --git a/modules/test/ntp/conf/module_config.json b/modules/test/ntp/conf/module_config.json index a1a297f06..fbfb04874 100644 --- a/modules/test/ntp/conf/module_config.json +++ b/modules/test/ntp/conf/module_config.json @@ -14,13 +14,13 @@ "tests":[ { "name": "ntp.network.ntp_support", - "description": "Does the device request network time sync as client as per RFC 5905 - Network Time Protocol Version 4: Protocol and Algorithms Specification", + "test_description": "Does the device request network time sync as client as per RFC 5905 - Network Time Protocol Version 4: Protocol and Algorithms Specification", "expected_behavior": "The device sends an NTPv4 request to the configured NTP server.", "required_result": "Required" }, { "name": "ntp.network.ntp_dhcp", - "description": "Accept NTP address over DHCP", + "test_description": "Accept NTP address over DHCP", "expected_behavior": "Device can accept NTP server address, provided by the DHCP server (DHCP OFFER PACKET)", "required_result": "Roadmap" } diff --git a/modules/test/tls/conf/module_config.json b/modules/test/tls/conf/module_config.json index 7f0305d19..194d1198c 100644 --- a/modules/test/tls/conf/module_config.json +++ b/modules/test/tls/conf/module_config.json @@ -14,25 +14,25 @@ "tests":[ { "name": "security.tls.v1_2_server", - "description": "Check the device web server TLS 1.2 & certificate is valid", + "test_description": "Check the device web server TLS 1.2 & certificate is valid", "expected_behavior": "TLS 1.2 certificate is issued to the web browser client when accessed", "required_result": "Required" }, { "name": "security.tls.v1_3_server", - "description": "Check the device web server TLS 1.3 & certificate is valid", + "test_description": "Check the device web server TLS 1.3 & certificate is valid", "expected_behavior": "TLS 1.3 certificate is issued to the web browser client when accessed", "required_result": "Recommended" }, { "name": "security.tls.v1_2_client", - "description": "Device uses TLS with connection to an external service on port 443 (or any other port which could be running the webserver-HTTPS)", + "test_description": "Device uses TLS with connection to an external service on port 443 (or any other port which could be running the webserver-HTTPS)", "expected_behavior": "The packet indicates a TLS connection with at least TLS 1.2 and support for ECDH and ECDSA ciphers", "required_result": "Required" }, { "name": "security.tls.v1_3_client", - "description": "Device uses TLS with connection to an external service on port 443 (or any other port which could be running the webserver-HTTPS)", + "test_description": "Device uses TLS with connection to an external service on port 443 (or any other port which could be running the webserver-HTTPS)", "expected_behavior": "The packet indicates a TLS connection with at least TLS 1.3", "required_result": "Recommended" } diff --git a/testing/tests/test_tests b/testing/tests/test_tests index 561fad80b..da24d0d64 100755 --- a/testing/tests/test_tests +++ b/testing/tests/test_tests @@ -25,7 +25,7 @@ mkdir -p $TEST_DIR # Setup requirements sudo apt-get update -sudo apt-get install openvswitch-common openvswitch-switch tcpdump jq moreutils coreutils isc-dhcp-client +sudo apt-get install -y openvswitch-common openvswitch-switch tcpdump jq moreutils coreutils isc-dhcp-client pip3 install pytest diff --git a/testing/tests/test_tests.json b/testing/tests/test_tests.json index 728f7764f..e74bb10d9 100644 --- a/testing/tests/test_tests.json +++ b/testing/tests/test_tests.json @@ -4,7 +4,14 @@ "args": "oddservices dns_static", "ethmac": "02:42:aa:00:00:01", "expected_results": { - "security.nmap.ports": "Non-Compliant" + "dns.network.hostname_resolution": "Non-Compliant", + "security.services.ftp": "Non-Compliant", + "security.services.tftp": "Non-Compliant", + "security.services.smtp": "Non-Compliant", + "security.services.pop": "Non-Compliant", + "security.services.imap": "Non-Compliant", + "ntp.network.ntp_support": "Non-Compliant", + "ntp.network.ntp_dhcp": "Non-Compliant" } }, "tester2": { @@ -13,7 +20,17 @@ "args": "ntpv4_dhcp dns_dhcp", "ethmac": "02:42:aa:00:00:02", "expected_results": { - "security.nmap.ports": "Compliant", + "security.services.ftp": "Compliant", + "security.ssh.version": "Compliant", + "security.services.telnet": "Compliant", + "security.services.smtp": "Compliant", + "security.services.http": "Compliant", + "security.services.pop": "Compliant", + "security.services.imap": "Compliant", + "security.services.snmpv3": "Compliant", + "security.services.vnc": "Compliant", + "security.services.tftp": "Compliant", + "ntp.network.ntp_server": "Compliant", "ntp.network.ntp_support": "Compliant", "ntp.network.ntp_dhcp": "Compliant", "connection.shared_address": "Compliant",