diff --git a/docs/test/compliant.md b/docs/test/compliant.md
index 2ad2b1f2c..e2819a2d7 100644
--- a/docs/test/compliant.md
+++ b/docs/test/compliant.md
@@ -111,4 +111,9 @@ Strict cryptographic standards apply to all inbound and outbound traffic via web
### Inbound Connections (Device as a Server / Web Interface)
* The device's embedded web server must support connections via **TLS 1.2** and/or **TLS 1.3**.
* **TLS 1.0 and TLS 1.1 must be explicitly disabled** within the web server configuration.
-* The TLS certificate presented by the web server must be valid and signed by a trusted (or test-environment designated) Certificate Authority (CA).
\ No newline at end of file
+* The TLS certificate presented by the web server must be valid and signed by a trusted (or test-environment designated) Certificate Authority (CA).
+
+### HTTP Server Compliance (RFC 9110)
+* Basic Method Support: Verification that the server successfully processes standard **GET** and **HEAD** methods, returning a **200 OK** status.
+* **HEAD** Response Semantics: Ensuring that the response body for a **HEAD** request is strictly empty, and critical headers **(Content-Length, Content-Type)** precisely match the **GET** response.
+* Unsupported Method Handling: Verification of correct server response to arbitrary or invalid methods, expecting a status code of **405 Method Not Allowed or 501 Not Implemented**.
diff --git a/modules/test/tls/README.md b/modules/test/tls/README.md
index cfca8100c..49e8d3464 100644
--- a/modules/test/tls/README.md
+++ b/modules/test/tls/README.md
@@ -19,4 +19,5 @@ Within the ```python/src``` directory, the below tests are executed.
| security.tls.v1_2_client | Device uses TLS with connection to an external service on port 443 (or any other port which could be running the webserver-HTTPS) | The packet indicates a TLS connection with at least TLS v1.2 and support for ECDH and ECDSA ciphers | Required if Applicable |
| security.tls.v1_3_server | Check the device web server TLS 1.3 and the certificate is valid | TLS 1.3 certificate is issued to the web browser client when accessed | Informational |
| security.tls.v1_3_client | Device uses TLS with connection to an external service on port 443 (or any other port which could be running the webserver-HTTPS) | The packet indicates a TLS connection with at least TLS 1.3 using AEAD-based ciphers | Informational |
+| security.http.server | Automated RFC 9110 compliance validation of the device's HTTP/HTTPS server | The server responds with status 200 OK for both GET and HEAD methods with an empty HEAD body and matching critical headers, returns 405 or 501 for unsupported methods | Informational |
diff --git a/modules/test/tls/conf/module_config.json b/modules/test/tls/conf/module_config.json
index 228fbb64d..76cef4143 100644
--- a/modules/test/tls/conf/module_config.json
+++ b/modules/test/tls/conf/module_config.json
@@ -58,6 +58,17 @@
"Disable connections to unsecure services",
"Ensure any URLs connected to are secure (https)"
]
+ },
+ {
+ "name": "security.http.server",
+ "test_description": "Automated RFC 9110 compliance validation of the device's HTTP/HTTPS server",
+ "expected_behavior": "The server responds with status 200 OK for both GET and HEAD methods with an empty HEAD body and matching critical headers, returns 405 or 501 for unsupported methods",
+ "recommendations": [
+ "Implement the GET and HEAD methods on the web server",
+ "Ensure that the HEAD response is empty",
+ "Ensure that the GET and HEAD response headers (Content-Length, Content-Type) match",
+ "Ensure that unsupported requests return a 405 or 501 status code"
+ ]
}
]
}
diff --git a/modules/test/tls/python/src/http_scan.py b/modules/test/tls/python/src/http_scan.py
index 8df21d26e..0b8ba2e4b 100644
--- a/modules/test/tls/python/src/http_scan.py
+++ b/modules/test/tls/python/src/http_scan.py
@@ -20,6 +20,11 @@
LOGGER = None
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
+_MSG_HTTP_NOT_DETECTED = 'No HTTP server detected on the device.'
+_MSG_HTTP_COMPLIANT = 'Device HTTP server is COMPLIANT with the RFC 9110'
+_MSG_HTTP_NON = 'Device HTTP server is NON-COMPLIANT with the RFC 9110'
+_MSG_HTTP_ERROR = 'Error checking HTTP server.'
+
class HTTPScan():
"""Helper class to scan for all HTTP/HTTPS services for a device"""
@@ -128,3 +133,131 @@ def scan_for_http_services(self, ip_address):
for port, service_type in results.items():
LOGGER.info(f'Port {port}: {service_type}')
return results
+
+ def _http_make_request(
+ self, method: str,
+ url: str,
+ timeout: int = 20
+ ) -> requests.Response | None:
+ """Sends HTTP request and handles exceptions uniformly.
+
+ Returns:
+ requests.Response | None
+ """
+
+ try:
+ resp = requests.request(
+ method=method,
+ url=url,
+ timeout=timeout,
+ verify=False # nosec B501
+ )
+ return resp
+ except requests.exceptions.Timeout as e:
+ msg = f'Timeout during {method.upper()} request to {url}'
+ LOGGER.error(f'{msg}: {e}')
+ return None
+ except requests.exceptions.ConnectionError as e:
+ msg = f'Connection refused during {method.upper()} request to {url}'
+ LOGGER.error(f'{msg}: {e}')
+ return None
+
+ def http_server_compliance(self, ip:str) -> tuple[str, str, list[str]]:
+ """Checks HTTP server comliance to RFC 9110."""
+ result_state = 'Feature Not Detected'
+ result_message = _MSG_HTTP_NOT_DETECTED
+ result_details = []
+ try:
+ ports = self.scan_all_ports(ip)
+ if not ports:
+ msg = f'No HTTP/HTTPS ports found on {ip}'
+ LOGGER.info(msg)
+ return result_state, result_message, result_details
+ if '443' in ports:
+ url = f'https://{ip}'
+ else:
+ url = f'http://{ip}'
+ LOGGER.info(f'Checking HTTP methods on {url}')
+ try:
+ get_resp = self._http_make_request('GET', url)
+ if get_resp is None:
+ get_ok = False
+ else:
+ get_ok = get_resp.status_code == 200
+ head_resp = self._http_make_request('HEAD', url)
+ if head_resp is None:
+ head_ok = False
+ else:
+ head_ok = head_resp.status_code == 200
+ method_resp = self._http_make_request('foobar', url)
+ except requests.exceptions.RequestException as e:
+ LOGGER.error(f'Error checking HTTP methods on {url}: {e}')
+ return 'Error', _MSG_HTTP_ERROR, []
+ if not get_ok or not head_ok:
+ LOGGER.info(f'HTTP GET/HEAD method is not supported on {url}')
+ result_state = 'Non-Compliant'
+ result_message = _MSG_HTTP_NON
+ if not get_ok:
+ result_details.append('Server does not support GET method')
+ else:
+ result_details.append('Server supports GET method')
+ if not head_ok:
+ result_details.append('Server does not support HEAD method')
+ else:
+ result_details.append('Server supports HEAD method')
+ elif get_ok and head_ok:
+ LOGGER.info(f'HTTP GET and HEAD methods are supported on {url}')
+ result_state = 'Compliant'
+ result_message = _MSG_HTTP_COMPLIANT
+ result_details.extend([
+ 'Server supports GET method',
+ 'Server supports HEAD method'
+ ])
+ if head_resp is not None and get_resp is not None:
+ head_body_len = len(head_resp.content)
+ head_body_empty = head_body_len == 0
+ headers_match = True
+ header_not_match = ''
+ for header in ('Content-Length', 'Content-Type'):
+ if header in get_resp.headers or header in head_resp.headers:
+ if get_resp.headers.get(header) != head_resp.headers.get(header):
+ headers_match = False
+ header_not_match = header
+ break
+ if not head_body_empty or not headers_match:
+ result_state = 'Non-Compliant'
+ result_message = _MSG_HTTP_NON
+ if not head_body_empty:
+ LOGGER.info('HEAD response body is not empty')
+ result_details.append('HEAD response body is not empty')
+ elif not headers_match:
+ msg = f'{header_not_match} does not match between GET and HEAD'
+ LOGGER.info(msg)
+ result_details.append(f'Header {header_not_match} does not match')
+ else:
+ LOGGER.info('HEAD response body is empty.')
+ LOGGER.info('Headers match between GET and HEAD methods.')
+ result_details.extend([
+ 'HEAD response body is empty',
+ 'Headers match between GET and HEAD methods'
+ ])
+ if method_resp is None:
+ result_state = 'Non-Compliant'
+ result_message = _MSG_HTTP_NON
+ msg = 'Device server does not return 501 or 405 for unsupported methods'
+ LOGGER.info(msg)
+ result_details.append(msg)
+ elif method_resp.status_code in (405, 501):
+ msg = 'Device server returns 501 or 405 for unsupported methods'
+ LOGGER.info(msg)
+ result_details.append(msg)
+ else:
+ result_state = 'Non-Compliant'
+ result_message = _MSG_HTTP_NON
+ msg = 'Device server does not return 501 or 405 for unsupported methods'
+ LOGGER.info(msg)
+ result_details.append(msg)
+ except Exception as e:
+ LOGGER.error(f'Unexpected error {e}')
+
+ return result_state, result_message, result_details
diff --git a/modules/test/tls/python/src/tls_module.py b/modules/test/tls/python/src/tls_module.py
index 3b0dc004c..020aedd26 100644
--- a/modules/test/tls/python/src/tls_module.py
+++ b/modules/test/tls/python/src/tls_module.py
@@ -556,6 +556,12 @@ def _security_tls_v1_3_client(self):
'1.3',
unsupported_versions=['1.0', '1.1'])
+ def _security_http_server(self):
+ LOGGER.info('Running HTTP server test')
+ return self._http_scan.http_server_compliance(
+ self._device_ipv4_addr
+ )
+
def _validate_tls_client(self,
client_mac,
tls_version,
diff --git a/modules/ui/karma.conf.js b/modules/ui/karma.conf.js
index 1eda40b4b..6e0a59fde 100644
--- a/modules/ui/karma.conf.js
+++ b/modules/ui/karma.conf.js
@@ -11,6 +11,12 @@ module.exports = function (config) {
require('karma-jasmine-html-reporter'),
require('karma-coverage'),
],
+ customLaunchers: {
+ ChromeHeadlessCI: {
+ base: 'ChromeHeadless',
+ flags: ['--no-sandbox', '--disable-setuid-sandbox'],
+ },
+ },
client: {
jasmine: {
// you can add configuration options for Jasmine here
diff --git a/modules/ui/package.json b/modules/ui/package.json
index 417cf2b66..c90d92387 100644
--- a/modules/ui/package.json
+++ b/modules/ui/package.json
@@ -9,7 +9,7 @@
"test": "ng test",
"test:coverage": "ng test --code-coverage",
"test-headless": "ng test --browsers=ChromeHeadless --watch=false",
- "test-ci": "ng test --no-watch --no-progress --code-coverage --browsers=ChromeHeadless",
+ "test-ci": "ng test --no-watch --no-progress --code-coverage --browsers=ChromeHeadlessCI",
"docker": "docker rm -f test-run-ui && docker rmi test-run-ui && docker build -t test-run-ui . && docker run -d -p 80:80 --name test-run-ui test-run-ui",
"lint": "ng lint",
"lint:fix": "ng lint --fix",
diff --git a/resources/test_packs/pilot/config.json b/resources/test_packs/pilot/config.json
index c7c1c47c6..4a417689c 100644
--- a/resources/test_packs/pilot/config.json
+++ b/resources/test_packs/pilot/config.json
@@ -176,6 +176,10 @@
{
"name": "security.tls.v1_3_client",
"required_result": "Informational"
+ },
+ {
+ "name": "security.http.server",
+ "required_result": "Informational"
}
]
}
\ No newline at end of file
diff --git a/resources/test_packs/pilot/report_templates/summary.jinja b/resources/test_packs/pilot/report_templates/summary.jinja
index 32eeaf320..cc0c22be0 100644
--- a/resources/test_packs/pilot/report_templates/summary.jinja
+++ b/resources/test_packs/pilot/report_templates/summary.jinja
@@ -13,6 +13,11 @@
MAC Address
{{ device['mac_addr'] }}
+ {% if device['kernel'] %}
+
+ Kernel
+ {{ device['kernel'] }}
+ {% endif %}
diff --git a/resources/test_packs/qualification/config.json b/resources/test_packs/qualification/config.json
index 8d3e26758..21c9c0dec 100644
--- a/resources/test_packs/qualification/config.json
+++ b/resources/test_packs/qualification/config.json
@@ -176,6 +176,10 @@
{
"name": "security.tls.v1_3_client",
"required_result": "Informational"
+ },
+ {
+ "name": "security.http.server",
+ "required_result": "Informational"
}
]
}
\ No newline at end of file
diff --git a/resources/test_packs/qualification/report_templates/summary.jinja b/resources/test_packs/qualification/report_templates/summary.jinja
index db4010bda..e9c6f072a 100644
--- a/resources/test_packs/qualification/report_templates/summary.jinja
+++ b/resources/test_packs/qualification/report_templates/summary.jinja
@@ -13,6 +13,11 @@
MAC Address
{{ device['mac_addr'] }}
+ {% if device['kernel'] %}
+
+
Kernel
+
{{ device['kernel'] }}
+ {% endif %}
diff --git a/test_vm/provision-non-compliant-tls.yml b/test_vm/provision-non-compliant-tls.yml
index 4dc419c59..8b5209d36 100644
--- a/test_vm/provision-non-compliant-tls.yml
+++ b/test_vm/provision-non-compliant-tls.yml
@@ -1,9 +1,10 @@
---
-- name: Non-compliant TLS handshakes from 10.10.10.14 via ens5 every 2s (with pause and logging)
+- name: Non-compliant TLS handshakes and HTTP server from 10.10.10.14 via ens5
hosts: all
become: yes
vars:
fail_tls_script: /usr/local/bin/fail_tls_handshake.sh
+ http_server_script: /usr/local/bin/http_server.py
bind_ip: "10.10.10.14"
iface: "ens5"
target_host: "www.google.com"
@@ -12,12 +13,13 @@
ssl_key: /etc/ssl/private/nginx-selfsigned.key
tasks:
- - name: Ensure openssl, dhclient, nginx and build dependencies are installed
+ - name: Ensure openssl, dhclient, nginx, python3 and build dependencies are installed
apt:
name:
- openssl
- isc-dhcp-client
- nginx
+ - python3
- perl
- pkg-config
- zlib1g-dev
@@ -78,8 +80,7 @@
chdir: /tmp/openssl-3.1.4
creates: /opt/openssl-3.1.4/bin/openssl
-
- # NGINX
+ # --- PYTHON BACKEND & NGINX PROXY SETUP ---
- name: Generate self-signed SSL certificate for nginx
command: >
@@ -89,26 +90,49 @@
args:
creates: "{{ ssl_cert }}"
- - name: Create nginx HTTPS config
+ - name: Deploy Non-Compliant Python Backend script
+ copy:
+ src: ./python/src/http_server.py
+ dest: "{{ http_server_script }}"
+ mode: '0755'
+ owner: vagrant
+ group: vagrant
+
+ - name: Create systemd service for Python Backend
+ copy:
+ dest: /etc/systemd/system/non-compliant-backend.service
+ content: |
+ [Unit]
+ Description=Non-Compliant Python Backend Server
+ After=network.target
+
+ [Service]
+ Type=simple
+ ExecStart=/usr/bin/python3 {{ http_server_script }} 8080
+ Restart=always
+
+ [Install]
+ WantedBy=multi-user.target
+
+ - name: Create nginx Reverse Proxy config
copy:
dest: /etc/nginx/sites-available/ssl_default
content: |
server {
- listen 443 ssl;
+ listen 443 ssl default_server;
+ listen 80 default_server;
server_name _;
+
ssl_certificate {{ ssl_cert }};
ssl_certificate_key {{ ssl_key }};
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
+
location / {
- return 200 'nginx HTTPS is up\n';
- }
- }
- server {
- listen 80;
- server_name _;
- location / {
- return 200 'nginx HTTP is up\n';
+ proxy_pass http://127.0.0.1:8080;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_buffering off;
}
}
@@ -124,12 +148,20 @@
path: /etc/nginx/sites-enabled/default
state: absent
+ - name: Start non-compliant Python backend service
+ systemd:
+ name: non-compliant-backend.service
+ state: restarted
+ enabled: yes
+
- name: Ensure nginx is started and enabled
systemd:
name: nginx
state: restarted
enabled: yes
+ # --- END PYTHON BACKEND & NGINX SETUP ---
+
- name: Create custom OpenSSL config for legacy protocol support
copy:
dest: /etc/ssl/openssl-legacy.cnf
@@ -178,7 +210,6 @@
echo "Running TLS 1.3 handshake..." >> $LOG
"$NEW_OPENSSL" s_client -connect tls-v1-2.badssl.com:1012 -tls1_3
-
- name: Create systemd service for failing TLS handshake
copy:
dest: /etc/systemd/system/fail-tls-handshake.service
@@ -205,7 +236,8 @@
WantedBy=timers.target
- name: Reload systemd
- shell: systemctl daemon-reload
+ systemd:
+ daemon_reload: yes
- name: Enable and start fail-tls-handshake.timer
systemd:
@@ -213,7 +245,7 @@
enabled: yes
state: started
- # BLOCK ICMP
+ # BLOCK ICMP / FIREWALL
- name: Ensure UFW is installed
apt:
@@ -221,19 +253,33 @@
state: present
- name: Set UFW default policy to deny incoming
- command: ufw default deny incoming
+ ufw:
+ default: deny
+ direction: incoming
- name: Set UFW default policy to allow outgoing
- command: ufw default allow outgoing
+ ufw:
+ default: allow
+ direction: outgoing
- name: Allow SSH
- command: ufw allow 22/tcp
+ ufw:
+ rule: allow
+ port: '22'
+ proto: tcp
- name: Allow HTTP
- command: ufw allow 80/tcp
+ ufw:
+ rule: allow
+ port: '80'
+ proto: tcp
- - name: Allow SNMP
- command: ufw allow 443/tcp
+ - name: Allow HTTPS
+ ufw:
+ rule: allow
+ port: '443'
+ proto: tcp
- name: Reload UFW
- command: ufw reload
+ ufw:
+ state: reloaded
\ No newline at end of file
diff --git a/test_vm/python/src/http_server.py b/test_vm/python/src/http_server.py
new file mode 100644
index 000000000..61894899c
--- /dev/null
+++ b/test_vm/python/src/http_server.py
@@ -0,0 +1,45 @@
+"""Minimal HTTP backend used by the non-compliant VM test."""
+import sys
+from http.server import HTTPServer, BaseHTTPRequestHandler
+
+
+class NonCompliantBackendHandler(BaseHTTPRequestHandler):
+ """Serve deliberately non-compliant HTTP responses for VM tests."""
+
+ def log_message(self, log_format, *args):
+ pass
+
+ # These names are required by BaseHTTPRequestHandler's dispatch protocol.
+ # pylint: disable=invalid-name
+ def do_GET(self):
+ self.send_response(200)
+ self.send_header('Content-Type', 'text/html; charset=utf-8')
+ self.send_header('Content-Length', '25')
+ self.end_headers()
+ self.wfile.write(b'GET response body content')
+
+ def do_HEAD(self):
+ self.send_response(405)
+ self.send_header('Content-Type', 'text/plain')
+ self.send_header('Content-Length', '18')
+ self.end_headers()
+ self.wfile.write(b'Method Not Allowed')
+ # pylint: enable=invalid-name
+
+ def dispatch_custom(self):
+ self.send_response(200)
+ self.send_header('Content-Type', 'text/plain')
+ self.send_header('Content-Length', '2')
+ self.end_headers()
+ self.wfile.write(b'OK')
+
+ def __getattr__(self, name):
+ if name.startswith('do_'):
+ return self.dispatch_custom
+ raise AttributeError(name)
+
+if __name__ == '__main__':
+ port = int(sys.argv[1]) if len(sys.argv) > 1 else 8080
+ server_address = ('127.0.0.1', port)
+ httpd = HTTPServer(server_address, NonCompliantBackendHandler)
+ httpd.serve_forever()
diff --git a/testing/unit/tls/tls_module_test.py b/testing/unit/tls/tls_module_test.py
index 77e14e1d4..1b26ffa4a 100644
--- a/testing/unit/tls/tls_module_test.py
+++ b/testing/unit/tls/tls_module_test.py
@@ -14,6 +14,7 @@
"""Module run all the TLS related unit tests"""
from tls_module import TLSModule
from tls_util import TLSUtil
+from http_scan import HTTPScan
from common import logger
import os
import unittest
@@ -25,6 +26,7 @@
import ssl
import shutil
import logging
+import requests
import socket
import sys
from cryptography import x509
@@ -76,6 +78,8 @@ def setUp(self):
self.tls_module = TLSModule(module=MODULE, conf_file=CONF_FILE)
self.tls_module._device_ipv4_addr = None # pylint: disable=W0212
self.tls_module._scan_results = None # pylint: disable=W0212
+ self.mock_logger = unittest.mock.MagicMock()
+ self.scanner = HTTPScan(self.mock_logger)
@patch('tls_module.TLSModule._get_device_ipv4')
def security_tls_v1_2_server_no_ip_test(self, mock_get_device_ipv4):
@@ -907,6 +911,120 @@ def download_public_cert(self, hostname, port=443):
with open(cert_path, 'w', encoding='utf-8') as cert_file:
cert_file.write(pem_cert.decode())
+ @patch.object(HTTPScan, 'scan_all_ports')
+ def test_http_server_not_detected(self, mock_scan_ports):
+ """No HTTP ports detected. Feature Not Detected."""
+ mock_scan_ports.return_value = []
+
+ state, message, details = self.scanner.http_server_compliance('10.10.10.14')
+
+ self.assertEqual(state, 'Feature Not Detected')
+ self.assertEqual(message, 'No HTTP server detected on the device.')
+ self.assertEqual(details, [])
+
+ @patch.object(HTTPScan, '_http_make_request')
+ @patch.object(HTTPScan, 'scan_all_ports')
+ def test_http_server_compliant(self, mock_scan_ports, mock_make_request):
+ """Test when the server fully complies with RFC 9110."""
+ mock_scan_ports.return_value = ['443']
+
+ get_resp = unittest.mock.MagicMock(status_code=200)
+ get_resp.headers = {'Content-Length': '50', 'Content-Type': 'text/html'}
+
+ head_resp = unittest.mock.MagicMock(status_code=200)
+ head_resp.headers = {'Content-Length': '50', 'Content-Type': 'text/html'}
+ head_resp.content = b''
+
+ method_resp = unittest.mock.MagicMock(status_code=405)
+
+ mock_make_request.side_effect = [get_resp, head_resp, method_resp]
+
+ state, message, details = self.scanner.http_server_compliance('10.10.10.14')
+
+ self.assertEqual(state, 'Compliant')
+ self.assertEqual(message,
+ 'Device HTTP server is COMPLIANT with the RFC 9110'
+ )
+ self.assertIn('Server supports GET method', details)
+ self.assertIn('Server supports HEAD method', details)
+ self.assertIn('Headers match between GET and HEAD methods',
+ details)
+ self.assertIn('HEAD response body is empty', details)
+ self.assertIn('Device server returns 501 or 405 for unsupported methods',
+ details
+ )
+
+
+ @patch.object(HTTPScan, '_http_make_request')
+ @patch.object(HTTPScan, 'scan_all_ports')
+ def test_http_server_non_compliant_status_codes(
+ self,
+ mock_scan_ports,
+ mock_make_request
+ ):
+ """Test compliance check when GET/HEAD return non-200 status codes."""
+ mock_scan_ports.return_value = ['80']
+
+ get_resp = unittest.mock.MagicMock(status_code=404)
+ head_resp = unittest.mock.MagicMock(status_code=404)
+ method_resp = unittest.mock.MagicMock(status_code=406)
+
+ mock_make_request.side_effect = [get_resp, head_resp, method_resp]
+
+ state, message, details = self.scanner.http_server_compliance('10.10.10.14')
+
+ self.assertEqual(state, 'Non-Compliant')
+ self.assertEqual(message,
+ 'Device HTTP server is NON-COMPLIANT with the RFC 9110'
+ )
+ self.assertIn('Server does not support GET method', details)
+ self.assertIn('Server does not support HEAD method', details)
+ self.assertIn(
+ 'Device server does not return 501 or 405 for unsupported methods',
+ details
+ )
+
+ @patch.object(HTTPScan, '_http_make_request')
+ @patch.object(HTTPScan, 'scan_all_ports')
+ def test_http_server_head_body_not_empty(
+ self,
+ mock_scan_ports,
+ mock_make_request
+ ):
+ """HEAD response contains a non-empty body."""
+ mock_scan_ports.return_value = ['80']
+
+ get_resp = unittest.mock.MagicMock(status_code=200)
+ get_resp.headers = {}
+
+ head_resp = unittest.mock.MagicMock(status_code=200)
+ head_resp.headers = {}
+ head_resp.content = b'unexpected body content'
+
+ method_resp = unittest.mock.MagicMock(status_code=501)
+
+ mock_make_request.side_effect = [get_resp, head_resp, method_resp]
+
+ state, _, details = self.scanner.http_server_compliance('10.10.10.14')
+
+ self.assertEqual(state, 'Non-Compliant')
+ self.assertIn('HEAD response body is not empty', details)
+
+ @patch.object(HTTPScan, '_http_make_request')
+ @patch.object(HTTPScan, 'scan_all_ports')
+ def test_https_server_exception(self, mock_scan_ports, mock_make_request):
+ """Test error handling;"""
+ mock_scan_ports.return_value = ['80']
+ mock_make_request.side_effect = requests.exceptions.RequestException(
+ 'Network failure'
+ )
+
+ state, message, details = self.scanner.http_server_compliance('10.10.10.14')
+
+ self.assertEqual(state, 'Error')
+ self.assertEqual(message, 'Error checking HTTP server.')
+ self.assertEqual(details, [])
+
if __name__ == '__main__':
suite = unittest.TestSuite()
@@ -972,6 +1090,13 @@ def download_public_cert(self, hostname, port=443):
suite.addTest(TLSModuleTest('outbound_connections_test'))
suite.addTest(TLSModuleTest('outbound_connections_report_test'))
+ # Test HTTP server
+ suite.addTest(TLSModuleTest('test_http_server_not_detected'))
+ suite.addTest(TLSModuleTest('test_http_server_compliant'))
+ suite.addTest(TLSModuleTest('test_http_server_non_compliant_status_codes'))
+ suite.addTest(TLSModuleTest('test_http_server_head_body_not_empty'))
+ suite.addTest(TLSModuleTest('test_https_server_exception'))
+
runner = unittest.TextTestRunner()
test_result = runner.run(suite)