diff --git a/framework/python/src/api/api.py b/framework/python/src/api/api.py index 719710694..5084555ae 100644 --- a/framework/python/src/api/api.py +++ b/framework/python/src/api/api.py @@ -168,7 +168,19 @@ async def post_sys_config(self, request: Request, response: Response): try: config = (await request.body()).decode("UTF-8") config_json = json.loads(config) + + # Validate req fields + if ("network" not in config_json or + "device_intf" not in config_json.get("network") or + "internet_intf" not in config_json.get("network") or + "log_level" not in config_json): + response.status_code = status.HTTP_400_BAD_REQUEST + return self._generate_msg( + False, + "Configuration is missing required fields") + self._session.set_config(config_json) + # Catch JSON Decode error etc except JSONDecodeError: response.status_code = status.HTTP_400_BAD_REQUEST @@ -690,6 +702,11 @@ async def update_profile(self, request: Request, response: Response): response.status_code = status.HTTP_400_BAD_REQUEST return self._generate_msg(False, "Invalid request received") + # Validate json profile + if not self.get_session().validate_profile_json(req_json): + response.status_code = status.HTTP_400_BAD_REQUEST + return self._generate_msg(False, "Invalid request received") + profile_name = req_json.get("name") # Check if profile exists diff --git a/framework/python/src/common/session.py b/framework/python/src/common/session.py index 20820c506..940fbe8f0 100644 --- a/framework/python/src/common/session.py +++ b/framework/python/src/common/session.py @@ -75,7 +75,7 @@ def apply_session_tracker(cls): @apply_session_tracker class TestrunSession(): - """Represents the current session of Test Run.""" + """Represents the current session of Testrun.""" def __init__(self, root_dir): self._root_dir = root_dir @@ -450,6 +450,11 @@ def _load_profiles(self): # Parse risk profile json json_data = json.load(f) + # Validate profile JSON + if not self.validate_profile_json(json_data): + LOGGER.error('Profile failed validation') + continue + # Instantiate a new risk profile risk_profile = RiskProfile() @@ -478,25 +483,6 @@ def get_profile(self, name): return profile return None - def validate_profile(self, profile_json): - - # Check name field is present - if 'name' not in profile_json: - return False - - # Check questions field is present - if 'questions' not in profile_json: - return False - - # Check all questions are present - for format_q in self.get_profiles_format(): - if self._get_profile_question(profile_json, - format_q.get('question')) is None: - LOGGER.error('Missing question: ' + format_q.get('question')) - return False - - return True - def _get_profile_question(self, profile_json, question): for q in profile_json.get('questions'): @@ -505,7 +491,14 @@ def _get_profile_question(self, profile_json, question): return None + def get_profile_format_question(self, question): + for q in self.get_profiles_format(): + if q.get('question') == question: + return q + def update_profile(self, profile_json): + """Update the risk profile with the provided JSON. + The content has already been validated in the API""" profile_name = profile_json['name'] @@ -513,39 +506,8 @@ def update_profile(self, profile_json): profile_json['version'] = self.get_version() profile_json['created'] = datetime.datetime.now().strftime('%Y-%m-%d') - if 'status' in profile_json and profile_json.get('status') == 'Valid': - # Attempting to submit a risk profile, we need to check it - - # Check all questions have been answered - all_questions_answered = True - - for question in self.get_profiles_format(): - - # Check question is present - profile_question = self._get_profile_question(profile_json, - question.get('question')) - - if profile_question is not None: - - # Check answer is present - if 'answer' not in profile_question: - LOGGER.error('Missing answer for question: ' + - question.get('question')) - all_questions_answered = False - - else: - LOGGER.error('Missing question: ' + question.get('question')) - all_questions_answered = False - - if not all_questions_answered: - LOGGER.error('Not all questions answered') - return None - - else: - profile_json['status'] = 'Draft' - + # Check if profile already exists risk_profile = self.get_profile(profile_name) - if risk_profile is None: # Create a new risk profile @@ -574,6 +536,106 @@ def update_profile(self, profile_json): return risk_profile + def validate_profile_json(self, profile_json): + """Validate properties in profile update requests""" + + # Get the status field + valid = False + if 'status' in profile_json and profile_json.get('status') == 'Valid': + valid = True + + # Check if 'name' exists in profile + if 'name' not in profile_json: + LOGGER.error('Missing "name" in profile') + return False + + # Check if 'name' field not empty + elif len(profile_json.get('name').strip()) == 0: + LOGGER.error('Name field left empty') + return False + + # Error handling if 'questions' not in request + if 'questions' not in profile_json and valid: + LOGGER.error('Missing "questions" field in profile') + return False + + # Validating the questions section + for question in profile_json.get('questions'): + + # Check if the question field is present + if 'question' not in question: + LOGGER.error('The "question" field is missing') + return False + + # Check if 'question' field not empty + elif len(question.get('question').strip()) == 0: + LOGGER.error('A question is missing from "question" field') + return False + + # Check if question is a recognized question + format_q = self.get_profile_format_question( + question.get('question')) + + if format_q is None: + LOGGER.error(f'Unrecognized question: {question.get("question")}') + return False + + # Error handling if 'answer' is missing + if 'answer' not in question and valid: + LOGGER.error('The answer field is missing') + return False + + # If answer is present, check the validation rules + else: + + # Extract the answer out of the profile + answer = question.get('answer') + + # Get the validation rules + field_type = format_q.get('type') + + # Check if type is string or single select, answer should be a string + if ((field_type in ['string', 'select']) + and not isinstance(answer, str)): + LOGGER.error(f'''Answer for question \ +{question.get('question')} is incorrect data type''') + return False + + # Check if type is select, answer must be from list + if field_type == 'select' and valid: + possible_answers = format_q.get('options') + if answer not in possible_answers: + LOGGER.error(f'''Answer for question \ +{question.get('question')} is not valid''') + return False + + # Validate select multiple field types + if field_type == 'select-multiple': + + if not isinstance(answer, list): + LOGGER.error(f'''Answer for question \ +{question.get('question')} is incorrect data type''') + return False + + question_options_len = len(format_q.get('options')) + + # We know it is a list, now check the indexes + for index in answer: + + # Check if the index is an integer + if not isinstance(index, int): + LOGGER.error(f'''Answer for question \ +{question.get('question')} is incorrect data type''') + return False + + # Check if index is 0 or above and less than the num of options + if index < 0 or index >= question_options_len: + LOGGER.error(f'''Invalid index provided as answer for \ +question {question.get('question')}''') + return False + + return True + def delete_profile(self, profile): try: diff --git a/framework/requirements.txt b/framework/requirements.txt index 402009ef9..0484905ee 100644 --- a/framework/requirements.txt +++ b/framework/requirements.txt @@ -21,6 +21,8 @@ pydantic==2.7.1 # Requirements for testing pytest==7.4.4 pytest-timeout==2.2.0 +responses==0.25.3 + # Requirements for the report markdown==3.5.2 diff --git a/modules/test/ntp/python/src/ntp_module.py b/modules/test/ntp/python/src/ntp_module.py index 033e98974..be27abbad 100644 --- a/modules/test/ntp/python/src/ntp_module.py +++ b/modules/test/ntp/python/src/ntp_module.py @@ -14,8 +14,8 @@ """NTP test module""" from test_module import TestModule from scapy.all import rdpcap, IP, IPv6, NTP, UDP, Ether -from datetime import datetime import os +from collections import defaultdict LOG_NAME = 'test_ntp' MODULE_REPORT_FILE_NAME = 'ntp_report.html' @@ -69,6 +69,33 @@ def generate_module_report(self): total_responses = sum(1 for row in ntp_table_data if row['Type'] == 'Server') + # Initialize a dictionary to store timestamps for each unique combination + timestamps = defaultdict(list) + + # Collect timestamps for each unique combination + for row in ntp_table_data: + # Add the timestamp to the corresponding combination + key = (row['Source'], row['Destination'], row['Type'], row['Version']) + timestamps[key].append(row['Timestamp']) + + # Calculate the average time between requests for each unique combination + average_time_between_requests = {} + + for key, times in timestamps.items(): + # Sort the timestamps + times.sort() + + # Calculate the time differences between consecutive timestamps + time_diffs = [t2 - t1 for t1, t2 in zip(times[:-1], times[1:])] + + # Calculate the average of the time differences + if time_diffs: + avg_diff = sum(time_diffs) / len(time_diffs) + else: + avg_diff = 0 # one timestamp, the average difference is 0 + + average_time_between_requests[key] = avg_diff + # Add summary table html_content += (f''' @@ -92,7 +119,6 @@ def generate_module_report(self): ''') if total_requests + total_responses > 0: - table_content = '''
@@ -101,37 +127,39 @@ def generate_module_report(self): - + + ''' - for row in ntp_table_data: - - # Timestamp of the NTP packet - dt_object = datetime.utcfromtimestamp(row['Timestamp']) + # Generate the HTML table with the count column + for (src, dst, typ, + version), avg_diff in average_time_between_requests.items(): + cnt = len(timestamps[(src, dst, typ, version)]) - # Extract milliseconds from the fractional part of the timestamp - milliseconds = int((row['Timestamp'] % 1) * 1000) - - # Format the datetime object with milliseconds - formatted_time = dt_object.strftime( - '%b %d, %Y %H:%M:%S.') + f'{milliseconds:03d}' + # Sync Average only applies to client requests + if 'Client' in typ: + # Convert avg_diff to seconds and format it + avg_diff_seconds = avg_diff + avg_formatted_time = f'{avg_diff_seconds:.3f} seconds' + else: + avg_formatted_time = 'N/A' - table_content += (f''' + table_content += f''' - - - - - - ''') + + + + + + + ''' table_content += '''
Destination Type VersionTimestampCountSync Request Average
{row['Source']}{row['Destination']}{row['Type']}{row['Version']}{formatted_time}
{src}{dst}{typ}{version}{cnt}{avg_formatted_time}
''' - html_content += table_content else: @@ -159,8 +187,8 @@ def extract_ntp_data(self): # Read the pcap files packets = (rdpcap(self.startup_capture_file) + - rdpcap(self.monitor_capture_file) + - rdpcap(self.ntp_server_capture_file)) + rdpcap(self.monitor_capture_file) + + rdpcap(self.ntp_server_capture_file)) # Iterate through NTP packets for packet in packets: @@ -283,7 +311,7 @@ def _ntp_network_ntp_dhcp(self): 'server and non-DHCP provided server') elif ntp_to_remote: result = ('Feature Not Detected', - 'Device sent NTP request to non-DHCP provided server') + 'Device sent NTP request to non-DHCP provided server') elif ntp_to_local: result = True, 'Device sent NTP request to DHCP provided server' diff --git a/testing/api/profiles/new_profile.json b/testing/api/profiles/new_profile.json index 7043f6cfa..d63ecd17c 100644 --- a/testing/api/profiles/new_profile.json +++ b/testing/api/profiles/new_profile.json @@ -1,145 +1,54 @@ { "name": "New Profile", - "status": "Draft", - "created": "2024-05-23 12:38:26", - "version": "v1.3", + "status": "Valid", "questions": [ - { - "question": "What type of device is this?", - "type": "select", - "options": [ - "IoT Sensor", - "IoT Controller", - "Smart Device", - "Something else" - ], - "answer": "IoT Sensor", - "validation": { - "required": true - } - }, - { - "question": "How will this device be used at Google?", - "type": "text-long", - "answer": "Installed in a building", - "validation": { - "max": "128", - "required": true - } - }, - { - "question": "What is the email of the device owner(s)?", - "type": "email-multiple", - "answer": "boddey@google.com, cmeredith@google.com", - "validation": { - "required": true, - "max": "128" - } - }, - { - "question": "Is this device going to be managed by Google or a third party?", - "type": "select", - "options": [ - "Google", - "Third Party" - ], - "answer": "Google", - "validation": { - "required": true - } - }, - { - "question": "Will the third-party device administrator be able to grant access to authorized Google personnel upon request?", - "type": "select", - "options": [ - "Yes", - "No", - "N/A" - ], - "default": "N/A", - "answer": "Yes", - "validation": { - "required": true - } - }, - { - "question": "Are any of the following statements true about your device?", - "description": "This tells us about the data your device will collect", - "type": "select-multiple", - "answer": [ - 0, - 2 - ], - "options": [ - "The device collects any Personal Identifiable Information (PII) or Personal Health Information (PHI)", - "The device collects intellectual property and trade secrets, sensitive business data, critical infrastructure data, identity assets", - "The device stream confidential business data in real-time (seconds)?" - ] - }, - { - "question": "Which of the following statements are true about this device?", - "description": "This tells us about the types of data that are transmitted from this device and how the transmission is performed from a technical standpoint.", - "type": "select-multiple", - "answer": [ - 0, - 1, - 5 - ], - "options": [ - "PII/PHI, confidential business data, or crown jewel data is transmitted to a destination outside Alphabet's ownership", - "Data transmission occurs across less-trusted networks (e.g. the internet).", - "A failure in data transmission would likely have a substantial negative impact (https://www.rra.rocks/docs/standard_levels#levels-definitions)", - "A confidentiality breach during transmission would have a substantial negative impact", - "The device encrypts data during transmission", - "The device network protocol is well-established and currently used by Google" - ] - }, - { - "question": "Does the network protocol assure server-to-client identity verification?", - "type": "select", - "answer": "Yes", - "options": [ - "Yes", - "No", - "I don't know" - ], - "validation": { - "required": true - } - }, - { - "question": "Click the statements that best describe the characteristics of this device.", - "description": "This tells us about how this device is managed remotely.", - "type": "select-multiple", - "answer": [ - 0, - 1, - 2 - ], - "options": [ - "PII/PHI, or confidential business data is accessible from the device without authentication", - "Unrecoverable actions (e.g. disk wipe) can be performed remotely", - "Authentication is required for remote access", - "The management interface is accessible from the public internet", - "Static credentials are used for administration" - ] - }, - { - "question": "Are any of the following statements true about this device?", - "description": "This informs us about what other systems and processes this device is a part of.", - "type": "select-multiple", - "answer": [ - 2, - 3 - ], - "options": [ - "The device monitors an environment for active risks to human life.", - "The device is used to convey people, or critical property.", - "The device controls robotics in human-accessible spaces.", - "The device controls physical access systems.", - "The device is involved in processes required by regulations, or compliance. (ex. privacy, security, safety regulations)", - "The device's failure would cause faults in other high-criticality processes." - ] - } + { + "question": "What type of device is this?", + "answer": "IoT Gateway" + }, + { + "question": "How will this device be used at Google?", + "answer": "Monitoring" + }, + { + "question": "Is this device going to be managed by Google or a third party?", + "answer": "Google" + }, + { + "question": "Will the third-party device administrator be able to grant access to authorized Google personnel upon request?", + "answer": "N/A" + }, + { + "question": "Are any of the following statements true about your device?", + "answer": [ + 0 + ] + }, + { + "question": "Which of the following statements are true about this device?", + "answer": [ + 0 + ] + }, + { + "question": "Does the network protocol assure server-to-client identity verification?", + "answer": "Yes" + }, + { + "question": "Click the statements that best describe the characteristics of this device.", + "answer": [ + 0 + ] + }, + { + "question": "Are any of the following statements true about this device?", + "answer": [ + 0 + ] + }, + { + "question": "Comments", + "answer": "" + } ] -} \ No newline at end of file + } \ No newline at end of file diff --git a/testing/api/profiles/new_profile_2.json b/testing/api/profiles/new_profile_2.json index 51782e187..2ac93dc17 100644 --- a/testing/api/profiles/new_profile_2.json +++ b/testing/api/profiles/new_profile_2.json @@ -1,144 +1,55 @@ { "name": "New Profile 2", "status": "Draft", - "created": "2024-05-23 12:38:26", - "version": "v1.3", "questions": [ { "question": "What type of device is this?", - "type": "select", - "options": [ - "IoT Sensor", - "IoT Controller", - "Smart Device", - "Something else" - ], - "answer": "IoT Sensor", - "validation": { - "required": true - } + "answer": "IoT Gateway" }, { "question": "How will this device be used at Google?", - "type": "text-long", - "answer": "Installed in a building", - "validation": { - "max": "128", - "required": true - } - }, - { - "question": "What is the email of the device owner(s)?", - "type": "email-multiple", - "answer": "boddey@google.com, cmeredith@google.com", - "validation": { - "required": true, - "max": "128" - } + "answer": "Installed in a building" }, { "question": "Is this device going to be managed by Google or a third party?", - "type": "select", - "options": [ - "Google", - "Third Party" - ], - "answer": "Google", - "validation": { - "required": true - } + "answer": "Google" }, { "question": "Will the third-party device administrator be able to grant access to authorized Google personnel upon request?", - "type": "select", - "options": [ - "Yes", - "No", - "N/A" - ], - "default": "N/A", - "answer": "Yes", - "validation": { - "required": true - } + "answer": "Yes" }, { "question": "Are any of the following statements true about your device?", - "description": "This tells us about the data your device will collect", - "type": "select-multiple", "answer": [ 0, 2 - ], - "options": [ - "The device collects any Personal Identifiable Information (PII) or Personal Health Information (PHI)", - "The device collects intellectual property and trade secrets, sensitive business data, critical infrastructure data, identity assets", - "The device stream confidential business data in real-time (seconds)?" ] }, { "question": "Which of the following statements are true about this device?", - "description": "This tells us about the types of data that are transmitted from this device and how the transmission is performed from a technical standpoint.", - "type": "select-multiple", "answer": [ 0, 1, 5 - ], - "options": [ - "PII/PHI, confidential business data, or crown jewel data is transmitted to a destination outside Alphabet's ownership", - "Data transmission occurs across less-trusted networks (e.g. the internet).", - "A failure in data transmission would likely have a substantial negative impact (https://www.rra.rocks/docs/standard_levels#levels-definitions)", - "A confidentiality breach during transmission would have a substantial negative impact", - "The device encrypts data during transmission", - "The device network protocol is well-established and currently used by Google" ] }, { "question": "Does the network protocol assure server-to-client identity verification?", - "type": "select", - "answer": "Yes", - "options": [ - "Yes", - "No", - "I don't know" - ], - "validation": { - "required": true - } + "answer": "Yes" }, { "question": "Click the statements that best describe the characteristics of this device.", - "description": "This tells us about how this device is managed remotely.", - "type": "select-multiple", "answer": [ 0, 1, 2 - ], - "options": [ - "PII/PHI, or confidential business data is accessible from the device without authentication", - "Unrecoverable actions (e.g. disk wipe) can be performed remotely", - "Authentication is required for remote access", - "The management interface is accessible from the public internet", - "Static credentials are used for administration" ] }, { "question": "Are any of the following statements true about this device?", - "description": "This informs us about what other systems and processes this device is a part of.", - "type": "select-multiple", "answer": [ 2, 3 - ], - "options": [ - "The device monitors an environment for active risks to human life.", - "The device is used to convey people, or critical property.", - "The device controls robotics in human-accessible spaces.", - "The device controls physical access systems.", - "The device is involved in processes required by regulations, or compliance. (ex. privacy, security, safety regulations)", - "The device's failure would cause faults in other high-criticality processes." ] } ] diff --git a/testing/api/profiles/updated_profile.json b/testing/api/profiles/updated_profile.json index a659cd937..91714bcfa 100644 --- a/testing/api/profiles/updated_profile.json +++ b/testing/api/profiles/updated_profile.json @@ -2,144 +2,55 @@ "name": "New Profile", "rename": "Updated Profile", "status": "Draft", - "created": "2024-05-23 12:38:26", - "version": "v1.3", "questions": [ { "question": "What type of device is this?", - "type": "select", - "options": [ - "IoT Sensor", - "IoT Controller", - "Smart Device", - "Something else" - ], - "answer": "IoT Sensor", - "validation": { - "required": true - } + "answer": "IoT Gateway" }, { "question": "How will this device be used at Google?", - "type": "text-long", - "answer": "Installed in a building", - "validation": { - "max": "128", - "required": true - } - }, - { - "question": "What is the email of the device owner(s)?", - "type": "email-multiple", - "answer": "boddey@google.com, cmeredith@google.com", - "validation": { - "required": true, - "max": "128" - } + "answer": "Installed in a building" }, { "question": "Is this device going to be managed by Google or a third party?", - "type": "select", - "options": [ - "Google", - "Third Party" - ], - "answer": "Google", - "validation": { - "required": true - } + "answer": "Google" }, { "question": "Will the third-party device administrator be able to grant access to authorized Google personnel upon request?", - "type": "select", - "options": [ - "Yes", - "No", - "N/A" - ], - "default": "N/A", - "answer": "Yes", - "validation": { - "required": true - } + "answer": "Yes" }, { "question": "Are any of the following statements true about your device?", - "description": "This tells us about the data your device will collect", - "type": "select-multiple", "answer": [ 0, 2 - ], - "options": [ - "The device collects any Personal Identifiable Information (PII) or Personal Health Information (PHI)", - "The device collects intellectual property and trade secrets, sensitive business data, critical infrastructure data, identity assets", - "The device stream confidential business data in real-time (seconds)?" ] }, { "question": "Which of the following statements are true about this device?", - "description": "This tells us about the types of data that are transmitted from this device and how the transmission is performed from a technical standpoint.", - "type": "select-multiple", "answer": [ 0, 1, 5 - ], - "options": [ - "PII/PHI, confidential business data, or crown jewel data is transmitted to a destination outside Alphabet's ownership", - "Data transmission occurs across less-trusted networks (e.g. the internet).", - "A failure in data transmission would likely have a substantial negative impact (https://www.rra.rocks/docs/standard_levels#levels-definitions)", - "A confidentiality breach during transmission would have a substantial negative impact", - "The device encrypts data during transmission", - "The device network protocol is well-established and currently used by Google" ] }, { "question": "Does the network protocol assure server-to-client identity verification?", - "type": "select", - "answer": "Yes", - "options": [ - "Yes", - "No", - "I don't know" - ], - "validation": { - "required": true - } + "answer": "Yes" }, { "question": "Click the statements that best describe the characteristics of this device.", - "description": "This tells us about how this device is managed remotely.", - "type": "select-multiple", "answer": [ 0, 1, 2 - ], - "options": [ - "PII/PHI, or confidential business data is accessible from the device without authentication", - "Unrecoverable actions (e.g. disk wipe) can be performed remotely", - "Authentication is required for remote access", - "The management interface is accessible from the public internet", - "Static credentials are used for administration" ] }, { "question": "Are any of the following statements true about this device?", - "description": "This informs us about what other systems and processes this device is a part of.", - "type": "select-multiple", "answer": [ 2, 3 - ], - "options": [ - "The device monitors an environment for active risks to human life.", - "The device is used to convey people, or critical property.", - "The device controls robotics in human-accessible spaces.", - "The device controls physical access systems.", - "The device is involved in processes required by regulations, or compliance. (ex. privacy, security, safety regulations)", - "The device's failure would cause faults in other high-criticality processes." ] } ] diff --git a/testing/api/test_api.py b/testing/api/test_api.py index 33163cecc..70c1a617f 100644 --- a/testing/api/test_api.py +++ b/testing/api/test_api.py @@ -29,6 +29,7 @@ import pytest import requests + ALL_DEVICES = "*" API = "http://127.0.0.1:8000" LOG_PATH = "/tmp/testrun.log" @@ -38,6 +39,8 @@ TESTING_DEVICES = "../device_configs" PROFILES_DIRECTORY = "local/risk_profiles" SYSTEM_CONFIG_PATH = "local/system.json" +SYSTEM_CONFIG_RESTORE_PATH = "testing/api/system.json" +PROFILES_PATH = "testing/api/profiles" BASELINE_MAC_ADDR = "02:42:aa:00:01:01" ALL_MAC_ADDR = "02:42:aa:00:00:01" @@ -46,21 +49,18 @@ def pretty_print(dictionary: dict): """ Pretty print dictionary """ print(json.dumps(dictionary, indent=4)) - def query_system_status() -> str: """Query system status from API and returns this""" r = requests.get(f"{API}/system/status", timeout=5) response = r.json() return response["status"] - def query_test_count() -> int: """Queries status and returns number of test results""" r = requests.get(f"{API}/system/status", timeout=5) response = r.json() return len(response["tests"]["results"]) - def start_test_device( device_name, mac_address, image_name="test-run/ci_device_1", args="" ): @@ -75,7 +75,6 @@ def start_test_device( ) print(cmd.stdout) - def stop_test_device(device_name): """ Stop docker container with given name """ cmd = subprocess.run( @@ -97,13 +96,23 @@ def docker_logs(device_name): ) print(cmd.stdout) +def load_json(file_name, directory): + """Utility method to load json files' """ + # Construct the base path relative to the main folder + base_path = Path(__file__).resolve().parent.parent.parent + # Construct the full file path + file_path = base_path / directory / file_name + + # Open the file in read mode + with open(file_path, "r", encoding="utf-8") as file: + # Return the file content + return json.load(file) @pytest.fixture def empty_devices_dir(): """ Use e,pty devices directory """ local_delete_devices(ALL_DEVICES) - @pytest.fixture def testing_devices(): """ Use devices from the testing/device_configs directory """ @@ -115,7 +124,6 @@ def testing_devices(): ) return local_get_devices() - @pytest.fixture def testrun(request): # pylint: disable=W0613 """ Start intstance of testrun """ @@ -166,7 +174,6 @@ def testrun(request): # pylint: disable=W0613 ) print(cmd.stdout) - def until_true(func: Callable, message: str, timeout: int): """ Blocks until given func returns True @@ -180,7 +187,6 @@ def until_true(func: Callable, message: str, timeout: int): time.sleep(1) raise TimeoutError(f"Timed out waiting {timeout}s for {message}") - def dict_paths(thing: dict, stem: str = "") -> Iterator[str]: """Returns json paths (in dot notation) from a given dictionary""" for k, v in thing.items(): @@ -190,7 +196,6 @@ def dict_paths(thing: dict, stem: str = "") -> Iterator[str]: else: yield path - def get_network_interfaces(): """return list of network interfaces on machine @@ -205,7 +210,6 @@ def get_network_interfaces(): ifaces.append(i.stem) return ifaces - def local_delete_devices(path): """ Deletes all local devices """ @@ -215,7 +219,6 @@ def local_delete_devices(path): else: shutil.rmtree(thing) - def local_get_devices(): """ Returns path to device configs of devices in local/devices directory""" return sorted( @@ -224,25 +227,240 @@ def local_get_devices(): ) ) +# Tests for system endpoints + +@pytest.fixture() +def restore_config(): + """Restore the original configuration (system.json) after the test""" + yield + + # Restore system.json from 'testing/api/' after the test + if os.path.exists(SYSTEM_CONFIG_RESTORE_PATH): + shutil.copy(SYSTEM_CONFIG_RESTORE_PATH, SYSTEM_CONFIG_PATH) def test_get_system_interfaces(testrun): # pylint: disable=W0613 """Tests API system interfaces against actual local interfaces""" + + # Send a GET request to the API to retrieve system interfaces r = requests.get(f"{API}/system/interfaces", timeout=5) + + # Check if status code is 200 (OK) + assert r.status_code == 200 + + # Parse the JSON response response = r.json() + + # Retrieve the actual network interfaces local_interfaces = get_network_interfaces() - assert set(response.keys()) == set(local_interfaces) - # schema expects a flat list + # Check if the key are in the response + assert set(response.keys()) == set(local_interfaces) + # Ensure that all values in the response are strings assert all(isinstance(x, str) for x in response) +def test_update_system_config(testrun, restore_config): # pylint: disable=W0613 + """Test update system configuration endpoint ('/system/config')""" -def test_status_idle(testrun): # pylint: disable=W0613 - until_true( - lambda: query_system_status().lower() == "idle", - "system status is `idle`", - 30, + # Configuration data to update + updated_system_config = { + "network": { + "device_intf": "updated_endev0a", + "internet_intf": "updated_wlan1" + }, + "log_level": "DEBUG" + } + + # Send the post request to update the system configuration + r = requests.post(f"{API}/system/config", + data=json.dumps(updated_system_config), + timeout=5) + + # Check if status code is 200 (OK) + assert r.status_code == 200 + + # Parse the JSON response + response = r.json() + + # Check if the response["network"]["device_intf"] has been updated + assert ( + response["network"]["device_intf"] + == updated_system_config["network"]["device_intf"] + ) + + # Check if the response["network"]["internet_intf"] has been updated + assert ( + response["network"]["internet_intf"] + == updated_system_config["network"]["internet_intf"] ) + # Check if the response["log_level"] has been updated + assert ( + response["log_level"] + == updated_system_config["log_level"] + ) + +def test_update_system_config_invalid_config(testrun, restore_config): # pylint: disable=W0613 + """Test invalid configuration file for update system configuration""" + + # Configuration data to update with missing "log_level" field + updated_system_config = { + "network": { + "device_intf": "updated_endev0a", + "internet_intf": "updated_wlan1" + } + } + + # Send the post request to update the system configuration + r = requests.post(f"{API}/system/config", + data=json.dumps(updated_system_config), + timeout=5) + + # Check if status code is 400 (Invalid config) + assert r.status_code == 400 + +def test_get_system_config(testrun): # pylint: disable=W0613 + """Tests get system configuration endpoint ('/system/config')""" + + # Send a GET request to the API to retrieve system configuration + r = requests.get(f"{API}/system/config", timeout=5) + + # Load system configuration file + local_config = load_json("system.json", directory="local") + + # Parse the JSON response + api_config = r.json() + + # Check if status code is 200 (OK) + assert r.status_code == 200 + + # Validate structure + assert set(dict_paths(api_config)) | set(dict_paths(local_config)) == set( + dict_paths(api_config) + ) + + # Check if the device interface in the local config matches the API config + assert ( + local_config["network"]["device_intf"] + == api_config["network"]["device_intf"] + ) + + # Check if the internet interface in the local config matches the API config + assert ( + local_config["network"]["internet_intf"] + == api_config["network"]["internet_intf"] + ) + +def test_start_testrun_started_successfully(testing_devices, testrun): # pylint: disable=W0613 + """Test for testrun started successfully """ + + # Payload with device details + payload = {"device": { + "mac_addr": BASELINE_MAC_ADDR, + "firmware": "asd", + "test_modules": { + "dns": {"enabled": False}, + "connection": {"enabled": True}, + "ntp": {"enabled": False}, + "baseline": {"enabled": False}, + "nmap": {"enabled": False} + }}} + + # Send the post request + r = requests.post(f"{API}/system/start", data=json.dumps(payload), timeout=10) + + # Check if the response status code is 200 (OK) + assert r.status_code == 200 + + # Parse the json response + response = r.json() + + # Check that device is in response + assert "device" in response + + # Check that mac_addr in response + assert "mac_addr" in response["device"] + + # Check that firmware in response + assert "firmware" in response["device"] + +def test_start_testrun_missing_device(testing_devices, testrun): # pylint: disable=W0613 + """Test for missing device when testrun is started """ + + # Payload empty dict (no device) + payload = {} + + # Send the post request + r = requests.post(f"{API}/system/start", data=json.dumps(payload), timeout=10) + + # Check if the response status code is 400 (bad request) + assert r.status_code == 400 + + # Parse the json response + response = r.json() + + # Check if 'error' in response + assert "error" in response + +def test_start_testrun_already_started(testing_devices, testrun): # pylint: disable=W0613 + """Test for testrun already started """ + + # Payload with device details + payload = {"device": { + "mac_addr": BASELINE_MAC_ADDR, + "firmware": "asd", + "test_modules": { + "dns": {"enabled": False}, + "connection": {"enabled": True}, + "ntp": {"enabled": False}, + "baseline": {"enabled": False}, + "nmap": {"enabled": False} + }}} + + # Send the post request (start test) + r = requests.post(f"{API}/system/start", data=json.dumps(payload), timeout=10) + + # Check if the response status code is 200 (OK) + assert r.status_code == 200 + + # Send the second post request (start test again) + r = requests.post(f"{API}/system/start", data=json.dumps(payload), timeout=10) + + # Parse the json response + response = r.json() + + # Check if the response status code is 409 (Conflict) + assert r.status_code == 409 + + # Check if 'error' in response + assert "error" in response + +def test_start_testrun_device_not_found(testing_devices, testrun): # pylint: disable=W0613 + """Test for start testrun device not found """ + + # Payload with device details with no mac address assigned + payload = {"device": { + "mac_addr": "", + "firmware": "asd", + "test_modules": { + "dns": {"enabled": False}, + "connection": {"enabled": True}, + "ntp": {"enabled": False}, + "baseline": {"enabled": False}, + "nmap": {"enabled": False} + }}} + + # Send the post request + r = requests.post(f"{API}/system/start", data=json.dumps(payload), timeout=10) + + # Check if the response status code is 404 (not found) + assert r.status_code == 404 + + # Parse the json response + response = r.json() + + # Check if 'error' in response + assert "error" in response + # Currently not working due to blocking during monitoring period @pytest.mark.skip() def test_status_in_progress(testing_devices, testrun): # pylint: disable=W0613 @@ -265,6 +483,268 @@ def test_status_in_progress(testing_devices, testrun): # pylint: disable=W0613 600, ) +# Currently not working due to blocking during monitoring period +@pytest.mark.skip() +def test_start_testrun_already_in_progress( + testing_devices, # pylint: disable=W0613 + testrun): # pylint: disable=W0613 + payload = {"device": {"mac_addr": BASELINE_MAC_ADDR, "firmware": "asd"}} + r = requests.post(f"{API}/system/start", data=json.dumps(payload), timeout=10) + + until_true( + lambda: query_system_status().lower() == "waiting for device", + "system status is `waiting for device`", + 30, + ) + + start_test_device("x123", BASELINE_MAC_ADDR) + + until_true( + lambda: query_system_status().lower() == "in progress", + "system status is `in progress`", + 600, + ) + r = requests.post(f"{API}/system/start", data=json.dumps(payload), timeout=10) + assert r.status_code == 409 + +@pytest.mark.skip() +def test_trigger_run(testing_devices, testrun): # pylint: disable=W0613 + payload = {"device": {"mac_addr": BASELINE_MAC_ADDR, "firmware": "asd"}} + r = requests.post(f"{API}/system/start", data=json.dumps(payload), timeout=10) + assert r.status_code == 200 + + until_true( + lambda: query_system_status().lower() == "waiting for device", + "system status is `waiting for device`", + 30, + ) + + start_test_device("x123", BASELINE_MAC_ADDR) + + until_true( + lambda: query_system_status().lower() == "compliant", + "system status is `complete`", + 600, + ) + + stop_test_device("x123") + + # Validate response + r = requests.get(f"{API}/system/status", timeout=5) + response = r.json() + pretty_print(response) + + # Validate results + results = {x["name"]: x for x in response["tests"]["results"]} + print(results) + # there are only 3 baseline tests + assert len(results) == 3 + + # Validate structure + with open( + os.path.join( + os.path.dirname(__file__), "mockito/running_system_status.json" + ), encoding="utf-8" + ) as f: + mockito = json.load(f) + + # validate structure + assert set(dict_paths(mockito)).issubset(set(dict_paths(response))) + + # Validate results structure + assert set(dict_paths(mockito["tests"]["results"][0])).issubset( + set(dict_paths(response["tests"]["results"][0])) + ) + + # Validate a result + assert results["baseline.compliant"]["result"] == "Compliant" + +@pytest.mark.skip() +def test_stop_running_test(testing_devices, testrun): # pylint: disable=W0613 + payload = {"device": {"mac_addr": ALL_MAC_ADDR, "firmware": "asd"}} + r = requests.post(f"{API}/system/start", data=json.dumps(payload), + timeout=10) + assert r.status_code == 200 + + until_true( + lambda: query_system_status().lower() == "waiting for device", + "system status is `waiting for device`", + 30, + ) + + start_test_device("x12345", ALL_MAC_ADDR) + + until_true( + lambda: query_test_count() > 1, + "system status is `complete`", + 1000, + ) + + stop_test_device("x12345") + + # Validate response + r = requests.post(f"{API}/system/stop", timeout=5) + response = r.json() + pretty_print(response) + assert response == {"success": "Testrun stopped"} + time.sleep(1) + + # Validate response + r = requests.get(f"{API}/system/status", timeout=5) + response = r.json() + pretty_print(response) + + assert response["status"] == "Cancelled" + +def test_stop_running_not_running(testrun): # pylint: disable=W0613 + # Validate response + r = requests.post(f"{API}/system/stop", + timeout=10) + response = r.json() + pretty_print(response) + + assert r.status_code == 404 + assert response["error"] == "Testrun is not currently running" + +@pytest.mark.skip() +def test_multiple_runs(testing_devices, testrun): # pylint: disable=W0613 + payload = {"device": {"mac_addr": BASELINE_MAC_ADDR, "firmware": "asd"}} + r = requests.post(f"{API}/system/start", data=json.dumps(payload), + timeout=10) + assert r.status_code == 200 + print(r.text) + + until_true( + lambda: query_system_status().lower() == "waiting for device", + "system status is `waiting for device`", + 30, + ) + + start_test_device("x123", BASELINE_MAC_ADDR) + + until_true( + lambda: query_system_status().lower() == "compliant", + "system status is `complete`", + 900, + ) + + stop_test_device("x123") + + # Validate response + r = requests.get(f"{API}/system/status", timeout=5) + response = r.json() + pretty_print(response) + + # Validate results + results = {x["name"]: x for x in response["tests"]["results"]} + print(results) + # there are only 3 baseline tests + assert len(results) == 3 + + payload = {"device": {"mac_addr": BASELINE_MAC_ADDR, "firmware": "asd"}} + r = requests.post(f"{API}/system/start", data=json.dumps(payload), + timeout=10) + # assert r.status_code == 200 + # returns 409 + print(r.text) + + until_true( + lambda: query_system_status().lower() == "waiting for device", + "system status is `waiting for device`", + 30, + ) + + start_test_device("x123", BASELINE_MAC_ADDR) + + until_true( + lambda: query_system_status().lower() == "compliant", + "system status is `complete`", + 900, + ) + + stop_test_device("x123") + +def test_status_idle(testrun): # pylint: disable=W0613 + """Test system status 'idle' endpoint (/system/status)""" + until_true( + lambda: query_system_status().lower() == "idle", + "system status is `idle`", + 30, + ) + +def test_system_shutdown(testrun): # pylint: disable=W0613 + """Test the shutdown system endpoint""" + # Send a POST request to initiate the system shutdown + r = requests.post(f"{API}/system/shutdown", timeout=5) + + # Check if the response status code is 200 (OK) + assert r.status_code == 200, f"Expected status code 200, got {r.status_code}" + +def test_system_shutdown_in_progress(testrun): # pylint: disable=W0613 + """Test system shutdown during an in-progress test""" + # Payload with device details to start a test + payload = { + "device": { + "mac_addr": BASELINE_MAC_ADDR, + "firmware": "asd", + "test_modules": { + "dns": {"enabled": False}, + "connection": {"enabled": True}, + "ntp": {"enabled": False}, + "baseline": {"enabled": False}, + "nmap": {"enabled": False} + } + } + } + + # Start a test + r = requests.post(f"{API}/system/start", data=json.dumps(payload), timeout=10) + + # Check if status code is not 200 (OK) + if r.status_code != 200: + raise ValueError(f"Api request failed with code: {r.status_code}") + + # Attempt to shutdown while the test is running + r = requests.post(f"{API}/system/shutdown", timeout=5) + + # Check if the response status code is 400 (test in progress) + assert r.status_code == 400 + +def test_system_latest_version(testrun): # pylint: disable=W0613 + """Test for testrun version when the latest version is installed""" + + # Send the get request to the API + r = requests.get(f"{API}/system/version", timeout=5) + + # Parse the response + response = r.json() + + # Check if status code is 200 (update available) + assert r.status_code == 200 + # Check if an update is available + assert response["update_available"] is False + +# Tests for reports endpoints + +def test_get_reports_no_reports(testrun): # pylint: disable=W0613 + """Test get reports when no reports exist.""" + + # Send a GET request to the /reports endpoint + r = requests.get(f"{API}/reports", timeout=5) + + # Check if the status code is 200 (OK) + assert r.status_code == 200 + + # Parse the JSON response + response = r.json() + + # Check if the response is a list + assert isinstance(response, list) + + # Check if the response is an empty list + assert response == [] + +# Tests for device endpoints @pytest.mark.skip() def test_status_non_compliant(testing_devices, testrun): # pylint: disable=W0613 @@ -298,7 +778,6 @@ def test_status_non_compliant(testing_devices, testrun): # pylint: disable=W0613 stop_test_device("x123") - def test_create_get_devices(empty_devices_dir, testrun): # pylint: disable=W0613 device_1 = { "manufacturer": "Google", @@ -361,7 +840,6 @@ def test_create_get_devices(empty_devices_dir, testrun): # pylint: disable=W0613 [device_1[key], device_2[key]] ) - def test_delete_device_success(empty_devices_dir, testrun): # pylint: disable=W0613 device_1 = { "manufacturer": "Google", @@ -439,7 +917,6 @@ def test_delete_device_success(empty_devices_dir, testrun): # pylint: disable=W0 [device_2[key]] ) - def test_delete_device_not_found(empty_devices_dir, testrun): # pylint: disable=W0613 device_1 = { "manufacturer": "Google", @@ -478,7 +955,6 @@ def test_delete_device_not_found(empty_devices_dir, testrun): # pylint: disable= assert r.status_code == 404 assert len(local_get_devices()) == 0 - def test_delete_device_no_mac(empty_devices_dir, testrun): # pylint: disable=W0613 device_1 = { "manufacturer": "Google", @@ -506,77 +982,19 @@ def test_delete_device_no_mac(empty_devices_dir, testrun): # pylint: disable=W06 device_1.pop("mac_addr") # Test that device_1 can't delete with no mac address - r = requests.delete(f"{API}/device/", - data=json.dumps(device_1), - timeout=5) - assert r.status_code == 400 - assert len(local_get_devices()) == 1 - - -# Currently not working due to blocking during monitoring period -@pytest.mark.skip() -def test_delete_device_testrun_running(testing_devices, testrun): # pylint: disable=W0613 - - payload = {"device": {"mac_addr": BASELINE_MAC_ADDR, "firmware": "asd"}} - r = requests.post(f"{API}/system/start", data=json.dumps(payload), timeout=10) - assert r.status_code == 200 - - until_true( - lambda: query_system_status().lower() == "waiting for device", - "system status is `waiting for device`", - 30, - ) - - start_test_device("x123", BASELINE_MAC_ADDR) - - until_true( - lambda: query_system_status().lower() == "in progress", - "system status is `in progress`", - 600, - ) - - device_1 = { - "manufacturer": "Google", - "model": "First", - "mac_addr": BASELINE_MAC_ADDR, - "test_modules": { - "dns": {"enabled": True}, - "connection": {"enabled": True}, - "ntp": {"enabled": True}, - "baseline": {"enabled": True}, - "nmap": {"enabled": True}, - }, - } - r = requests.delete(f"{API}/device/", - data=json.dumps(device_1), - timeout=5) - assert r.status_code == 403 - - -def test_start_testrun_started_successfully( - testing_devices, # pylint: disable=W0613 - testrun): # pylint: disable=W0613 - payload = {"device": { - "mac_addr": BASELINE_MAC_ADDR, - "firmware": "asd", - "test_modules": { - "dns": {"enabled": False}, - "connection": {"enabled": True}, - "ntp": {"enabled": False}, - "baseline": {"enabled": False}, - "nmap": {"enabled": False} - }}} - r = requests.post(f"{API}/system/start", data=json.dumps(payload), timeout=10) - assert r.status_code == 200 - + r = requests.delete(f"{API}/device/", + data=json.dumps(device_1), + timeout=5) + assert r.status_code == 400 + assert len(local_get_devices()) == 1 # Currently not working due to blocking during monitoring period @pytest.mark.skip() -def test_start_testrun_already_in_progress( - testing_devices, # pylint: disable=W0613 - testrun): # pylint: disable=W0613 +def test_delete_device_testrun_running(testing_devices, testrun): # pylint: disable=W0613 + payload = {"device": {"mac_addr": BASELINE_MAC_ADDR, "firmware": "asd"}} r = requests.post(f"{API}/system/start", data=json.dumps(payload), timeout=10) + assert r.status_code == 200 until_true( lambda: query_system_status().lower() == "waiting for device", @@ -591,9 +1009,23 @@ def test_start_testrun_already_in_progress( "system status is `in progress`", 600, ) - r = requests.post(f"{API}/system/start", data=json.dumps(payload), timeout=10) - assert r.status_code == 409 + device_1 = { + "manufacturer": "Google", + "model": "First", + "mac_addr": BASELINE_MAC_ADDR, + "test_modules": { + "dns": {"enabled": True}, + "connection": {"enabled": True}, + "ntp": {"enabled": True}, + "baseline": {"enabled": True}, + "nmap": {"enabled": True}, + }, + } + r = requests.delete(f"{API}/device/", + data=json.dumps(device_1), + timeout=5) + assert r.status_code == 403 def test_start_system_not_configured_correctly( empty_devices_dir, # pylint: disable=W0613 @@ -623,7 +1055,6 @@ def test_start_system_not_configured_correctly( timeout=10) assert r.status_code == 500 - def test_start_device_not_found(empty_devices_dir, # pylint: disable=W0613 testrun): # pylint: disable=W0613 device_1 = { @@ -656,7 +1087,6 @@ def test_start_device_not_found(empty_devices_dir, # pylint: disable=W0613 timeout=10) assert r.status_code == 404 - def test_start_missing_device_information( empty_devices_dir, # pylint: disable=W0613 testrun): # pylint: disable=W0613 @@ -685,7 +1115,6 @@ def test_start_missing_device_information( timeout=10) assert r.status_code == 400 - def test_create_device_already_exists( empty_devices_dir, # pylint: disable=W0613 testrun): # pylint: disable=W0613 @@ -715,7 +1144,6 @@ def test_create_device_already_exists( print(r.text) assert r.status_code == 409 - def test_create_device_invalid_json( empty_devices_dir, # pylint: disable=W0613 testrun): # pylint: disable=W0613 @@ -728,7 +1156,6 @@ def test_create_device_invalid_json( print(r.text) assert r.status_code == 400 - def test_create_device_invalid_request( empty_devices_dir, # pylint: disable=W0613 testrun): # pylint: disable=W0613 @@ -739,7 +1166,6 @@ def test_create_device_invalid_request( print(r.text) assert r.status_code == 400 - def test_device_edit_device( testing_devices, # pylint: disable=W0613 testrun): # pylint: disable=W0613 @@ -788,7 +1214,6 @@ def test_device_edit_device( assert updated_device_api["model"] == new_model assert updated_device_api["test_modules"] == new_test_modules - def test_device_edit_device_not_found( empty_devices_dir, # pylint: disable=W0613 testrun): # pylint: disable=W0613 @@ -826,7 +1251,6 @@ def test_device_edit_device_not_found( assert r.status_code == 404 - def test_device_edit_device_incorrect_json_format( empty_devices_dir, # pylint: disable=W0613 testrun): # pylint: disable=W0613 @@ -840,289 +1264,93 @@ def test_device_edit_device_incorrect_json_format( "ntp": {"enabled": True}, "baseline": {"enabled": True}, "nmap": {"enabled": True}, - }, - } - - r = requests.post(f"{API}/device", - data=json.dumps(device_1), - timeout=5) - print(r.text) - assert r.status_code == 201 - assert len(local_get_devices()) == 1 - - updated_device_payload = {} - - - r = requests.post(f"{API}/device/edit", - data=json.dumps(updated_device_payload), - timeout=5) - - assert r.status_code == 400 - - -def test_device_edit_device_with_mac_already_exists( - empty_devices_dir, # pylint: disable=W0613 - testrun): # pylint: disable=W0613 - device_1 = { - "manufacturer": "Google", - "model": "First", - "mac_addr": "00:1e:42:35:73:c4", - "test_modules": { - "dns": {"enabled": True}, - "connection": {"enabled": True}, - "ntp": {"enabled": True}, - "baseline": {"enabled": True}, - "nmap": {"enabled": True}, - }, - } - - r = requests.post(f"{API}/device", - data=json.dumps(device_1), - timeout=5) - print(r.text) - assert r.status_code == 201 - assert len(local_get_devices()) == 1 - - device_2 = { - "manufacturer": "Google", - "model": "Second", - "mac_addr": "00:1e:42:35:73:c6", - "test_modules": { - "dns": {"enabled": True}, - "connection": {"enabled": True}, - "ntp": {"enabled": True}, - "baseline": {"enabled": True}, - "nmap": {"enabled": True}, - }, - } - r = requests.post(f"{API}/device", - data=json.dumps(device_2), - timeout=5) - assert r.status_code == 201 - assert len(local_get_devices()) == 2 - - updated_device = copy.deepcopy(device_1) - - updated_device_payload = {} - updated_device_payload = {} - updated_device_payload["device"] = updated_device - updated_device_payload["mac_addr"] = "00:1e:42:35:73:c6" - updated_device_payload["model"] = "Alphabet" - - - r = requests.post(f"{API}/device/edit", - data=json.dumps(updated_device_payload), - timeout=5) - - assert r.status_code == 409 - - -def test_system_latest_version(testrun): # pylint: disable=W0613 - r = requests.get(f"{API}/system/version", timeout=5) - assert r.status_code == 200 - updated_system_version = r.json()["update_available"] - assert updated_system_version is False - - -def test_get_system_config(testrun): # pylint: disable=W0613 - r = requests.get(f"{API}/system/config", timeout=5) - - with open( - SYSTEM_CONFIG_PATH, - encoding="utf-8" - ) as f: - local_config = json.load(f) - - api_config = r.json() - - # validate structure - assert set(dict_paths(api_config)) | set(dict_paths(local_config)) == set( - dict_paths(api_config) - ) - - assert ( - local_config["network"]["device_intf"] - == api_config["network"]["device_intf"] - ) - assert ( - local_config["network"]["internet_intf"] - == api_config["network"]["internet_intf"] - ) - - -def test_invalid_path_get(testrun): # pylint: disable=W0613 - r = requests.get(f"{API}/blah/blah", timeout=5) - response = r.json() - assert r.status_code == 404 - with open( - os.path.join(os.path.dirname(__file__), "mockito/invalid_request.json"), - encoding="utf-8" - ) as f: - mockito = json.load(f) - - # validate structure - assert set(dict_paths(mockito)) == set(dict_paths(response)) - - -@pytest.mark.skip() -def test_trigger_run(testing_devices, testrun): # pylint: disable=W0613 - payload = {"device": {"mac_addr": BASELINE_MAC_ADDR, "firmware": "asd"}} - r = requests.post(f"{API}/system/start", data=json.dumps(payload), timeout=10) - assert r.status_code == 200 - - until_true( - lambda: query_system_status().lower() == "waiting for device", - "system status is `waiting for device`", - 30, - ) - - start_test_device("x123", BASELINE_MAC_ADDR) - - until_true( - lambda: query_system_status().lower() == "compliant", - "system status is `complete`", - 600, - ) - - stop_test_device("x123") - - # Validate response - r = requests.get(f"{API}/system/status", timeout=5) - response = r.json() - pretty_print(response) - - # Validate results - results = {x["name"]: x for x in response["tests"]["results"]} - print(results) - # there are only 3 baseline tests - assert len(results) == 3 - - # Validate structure - with open( - os.path.join( - os.path.dirname(__file__), "mockito/running_system_status.json" - ), encoding="utf-8" - ) as f: - mockito = json.load(f) - - # validate structure - assert set(dict_paths(mockito)).issubset(set(dict_paths(response))) - - # Validate results structure - assert set(dict_paths(mockito["tests"]["results"][0])).issubset( - set(dict_paths(response["tests"]["results"][0])) - ) - - # Validate a result - assert results["baseline.compliant"]["result"] == "Compliant" - - -@pytest.mark.skip() -def test_stop_running_test(testing_devices, testrun): # pylint: disable=W0613 - payload = {"device": {"mac_addr": ALL_MAC_ADDR, "firmware": "asd"}} - r = requests.post(f"{API}/system/start", data=json.dumps(payload), - timeout=10) - assert r.status_code == 200 - - until_true( - lambda: query_system_status().lower() == "waiting for device", - "system status is `waiting for device`", - 30, - ) - - start_test_device("x12345", ALL_MAC_ADDR) - - until_true( - lambda: query_test_count() > 1, - "system status is `complete`", - 1000, - ) - - stop_test_device("x12345") - - # Validate response - r = requests.post(f"{API}/system/stop", timeout=5) - response = r.json() - pretty_print(response) - assert response == {"success": "Testrun stopped"} - time.sleep(1) - - # Validate response - r = requests.get(f"{API}/system/status", timeout=5) - response = r.json() - pretty_print(response) - - assert response["status"] == "Cancelled" - - -def test_stop_running_not_running(testrun): # pylint: disable=W0613 - # Validate response - r = requests.post(f"{API}/system/stop", - timeout=10) - response = r.json() - pretty_print(response) - - assert r.status_code == 404 - assert response["error"] == "Testrun is not currently running" + }, + } -@pytest.mark.skip() -def test_multiple_runs(testing_devices, testrun): # pylint: disable=W0613 - payload = {"device": {"mac_addr": BASELINE_MAC_ADDR, "firmware": "asd"}} - r = requests.post(f"{API}/system/start", data=json.dumps(payload), - timeout=10) - assert r.status_code == 200 + r = requests.post(f"{API}/device", + data=json.dumps(device_1), + timeout=5) print(r.text) + assert r.status_code == 201 + assert len(local_get_devices()) == 1 - until_true( - lambda: query_system_status().lower() == "waiting for device", - "system status is `waiting for device`", - 30, - ) - - start_test_device("x123", BASELINE_MAC_ADDR) + updated_device_payload = {} - until_true( - lambda: query_system_status().lower() == "compliant", - "system status is `complete`", - 900, - ) - stop_test_device("x123") + r = requests.post(f"{API}/device/edit", + data=json.dumps(updated_device_payload), + timeout=5) - # Validate response - r = requests.get(f"{API}/system/status", timeout=5) - response = r.json() - pretty_print(response) + assert r.status_code == 400 - # Validate results - results = {x["name"]: x for x in response["tests"]["results"]} - print(results) - # there are only 3 baseline tests - assert len(results) == 3 +def test_device_edit_device_with_mac_already_exists( + empty_devices_dir, # pylint: disable=W0613 + testrun): # pylint: disable=W0613 + device_1 = { + "manufacturer": "Google", + "model": "First", + "mac_addr": "00:1e:42:35:73:c4", + "test_modules": { + "dns": {"enabled": True}, + "connection": {"enabled": True}, + "ntp": {"enabled": True}, + "baseline": {"enabled": True}, + "nmap": {"enabled": True}, + }, + } - payload = {"device": {"mac_addr": BASELINE_MAC_ADDR, "firmware": "asd"}} - r = requests.post(f"{API}/system/start", data=json.dumps(payload), - timeout=10) - # assert r.status_code == 200 - # returns 409 + r = requests.post(f"{API}/device", + data=json.dumps(device_1), + timeout=5) print(r.text) + assert r.status_code == 201 + assert len(local_get_devices()) == 1 - until_true( - lambda: query_system_status().lower() == "waiting for device", - "system status is `waiting for device`", - 30, - ) + device_2 = { + "manufacturer": "Google", + "model": "Second", + "mac_addr": "00:1e:42:35:73:c6", + "test_modules": { + "dns": {"enabled": True}, + "connection": {"enabled": True}, + "ntp": {"enabled": True}, + "baseline": {"enabled": True}, + "nmap": {"enabled": True}, + }, + } + r = requests.post(f"{API}/device", + data=json.dumps(device_2), + timeout=5) + assert r.status_code == 201 + assert len(local_get_devices()) == 2 - start_test_device("x123", BASELINE_MAC_ADDR) + updated_device = copy.deepcopy(device_1) - until_true( - lambda: query_system_status().lower() == "compliant", - "system status is `complete`", - 900, - ) + updated_device_payload = {} + updated_device_payload = {} + updated_device_payload["device"] = updated_device + updated_device_payload["mac_addr"] = "00:1e:42:35:73:c6" + updated_device_payload["model"] = "Alphabet" - stop_test_device("x123") + r = requests.post(f"{API}/device/edit", + data=json.dumps(updated_device_payload), + timeout=5) + + assert r.status_code == 409 + +def test_invalid_path_get(testrun): # pylint: disable=W0613 + r = requests.get(f"{API}/blah/blah", timeout=5) + response = r.json() + assert r.status_code == 404 + with open( + os.path.join(os.path.dirname(__file__), "mockito/invalid_request.json"), + encoding="utf-8" + ) as f: + mockito = json.load(f) + + # validate structure + assert set(dict_paths(mockito)) == set(dict_paths(response)) def test_create_invalid_chars(empty_devices_dir, testrun): # pylint: disable=W0613 # local_delete_devices(ALL_DEVICES) @@ -1149,10 +1377,26 @@ def test_create_invalid_chars(empty_devices_dir, testrun): # pylint: disable=W06 print(r.text) print(r.status_code) +def test_get_test_modules(testrun): # pylint: disable=W0613 + """Test the /system/modules endpoint to get the test modules""" + + # Send a GET request to the API endpoint + r = requests.get(f"{API}/system/modules", timeout=5) + + # Check if status code is 200 (OK) + assert r.status_code == 200 + + # Parse the JSON response + response = r.json() + + # Check if the response is a list + assert isinstance(response, list) # Tests for profile endpoints def delete_all_profiles(): """Utility method to delete all profiles from risk_profiles folder""" + + # Assign the profiles directory profiles_path = Path(PROFILES_DIRECTORY) try: @@ -1177,8 +1421,10 @@ def delete_all_profiles(): def create_profile(file_name): """Utility method to create the profile""" + # Load the profile - new_profile = load_profile(file_name) + new_profile = load_json(file_name, directory=PROFILES_PATH) + # Assign the profile name to profile_name profile_name = new_profile["name"] @@ -1196,13 +1442,15 @@ def create_profile(file_name): # Return the profile return new_profile - @pytest.fixture() def reset_profiles(): """Delete the profiles before and after each test""" + # Delete before the test delete_all_profiles() + yield + # Delete after the test delete_all_profiles() @@ -1212,15 +1460,6 @@ def add_profile(): # Returning the reference to create_profile return create_profile -def load_profile(file_name): - """Utility method to load the profiles from 'testing/api/profiles' """ - # Construct the file path - file_path = os.path.join(os.path.dirname(__file__), "profiles", file_name) - # Open the file in read mode - with open(file_path, "r", encoding="utf-8") as file: - # Return the file content - return json.load(file) - def profile_exists(profile_name): """Utility method to check if profile exists""" # Send the get request @@ -1235,12 +1474,16 @@ def profile_exists(profile_name): def test_get_profiles_format(testrun): # pylint: disable=W0613 """Test profiles format""" + # Send the get request r = requests.get(f"{API}/profiles/format", timeout=5) + # Check if status code is 200 (OK) assert r.status_code == 200 + # Parse the response response = r.json() + # Check if the response is a list assert isinstance(response, list) @@ -1256,12 +1499,16 @@ def test_get_profiles(testrun, reset_profiles, add_profile): # pylint: disable= # Send the get request to "/profiles" endpoint r = requests.get(f"{API}/profiles", timeout=5) + # Check if status code is 200 (OK) assert r.status_code == 200 + # Parse the response (profiles) response = r.json() + # Check if response is a list assert isinstance(response, list) + # Check if the list is empty assert len(response) == 0 @@ -1272,39 +1519,35 @@ def test_get_profiles(testrun, reset_profiles, add_profile): # pylint: disable= # Send get request to the "/profiles" endpoint r = requests.get(f"{API}/profiles", timeout=5) + # Check if status code is 200 (OK) assert r.status_code == 200 + # Parse the response (profiles) response = r.json() + # Check if response is a list assert isinstance(response, list) + # Check if response contains one profile assert len(response) == 1 # Check that each profile has the expected fields for profile in response: - # Check if "name" key exists in profile - assert "name" in profile - # Check if "status" key exists in profile - assert "status" in profile - # Check if "created" key exists in profile - assert "created" in profile - # Check if "version" key exists in profile - assert "version" in profile - # Check if "questions" key exists in profile - assert "questions" in profile + for field in ["name", "status", "created", "version", "questions", "risk"]: + assert field in profile # Check if "questions" value is a list assert isinstance(profile["questions"], list) - #check that "questions" value has the expected fields + # Check that "questions" value has the expected fields for element in profile["questions"]: # Check if each element is dict assert isinstance(element, dict) + # Check if "question" key is in dict element assert "question" in element - # Check if "type" key is in dict element - assert "type" in element + # Check if "asnswer" key is in dict element assert "answer" in element @@ -1316,12 +1559,15 @@ def test_get_profiles(testrun, reset_profiles, add_profile): # pylint: disable= # Send the get request to "/profiles" endpoint r = requests.get(f"{API}/profiles", timeout=5) - # Check if status code is 200 (OK) - assert r.status_code == 200 # Parse the response (profiles) response = r.json() + + # Check if status code is 200 (OK) + assert r.status_code == 200 + # Check if response is a list assert isinstance(response, list) + # Check if response contains two profiles assert len(response) == 2 @@ -1329,7 +1575,8 @@ def test_create_profile(testrun, reset_profiles): # pylint: disable=W0613 """Test for create profile if not exists""" # Load the profile - new_profile = load_profile("new_profile.json") + new_profile = load_json("new_profile.json", directory=PROFILES_PATH) + # Assign the profile name to profile_name profile_name = new_profile["name"] @@ -1342,8 +1589,10 @@ def test_create_profile(testrun, reset_profiles): # pylint: disable=W0613 # Check if status code is 201 (Created) assert r.status_code == 201 + # Parse the response response = r.json() + # Check if "success" key in response assert "success" in response @@ -1352,6 +1601,7 @@ def test_create_profile(testrun, reset_profiles): # pylint: disable=W0613 # Check if status code is 200 (OK) assert r.status_code == 200 + # Parse the response profiles = r.json() @@ -1359,19 +1609,23 @@ def test_create_profile(testrun, reset_profiles): # pylint: disable=W0613 created_profile = next( (p for p in profiles if p["name"] == profile_name), None ) + # Check if profile was created assert created_profile is not None def test_update_profile(testrun, reset_profiles, add_profile): # pylint: disable=W0613 - """test for update profile when exists""" + """Test for update profile when exists""" # Load the new profile using add_profile fixture new_profile = add_profile("new_profile.json") - # Load the updated profile using load_profile utility method - updated_profile = load_profile("updated_profile.json") + + # Load the updated profile using load_json utility method + updated_profile = load_json("updated_profile.json", + directory=PROFILES_PATH) # Assign the new_profile name profile_name = new_profile["name"] + # Assign the updated_profile name updated_profile_name = updated_profile["rename"] @@ -1387,8 +1641,10 @@ def test_update_profile(testrun, reset_profiles, add_profile): # pylint: disable # Check if status code is 200 (OK) assert r.status_code == 200 + # Parse the response response = r.json() + # Check if "success" key in response assert "success" in response @@ -1397,6 +1653,7 @@ def test_update_profile(testrun, reset_profiles, add_profile): # pylint: disable # Check if status code is 200 (OK) assert r.status_code == 200 + # Parse the response profiles = r.json() @@ -1408,11 +1665,57 @@ def test_update_profile(testrun, reset_profiles, add_profile): # pylint: disable # Check if profile was updated assert updated_profile_check is not None +def test_update_profile_invalid_json(testrun, reset_profiles, add_profile): # pylint: disable=W0613 + """Test for update profile invalid JSON payload (no 'name')""" + + # Load the new profile using add_profile fixture + add_profile("new_profile.json") + + # invalid JSON + updated_profile = {} + + # Send the post request to update the profile + r = requests.post( + f"{API}/profiles", + data=json.dumps(updated_profile), + timeout=5) + + # Parse the response + response = r.json() + + # Check if status code is 400 (Bad request) + assert r.status_code == 400 + + # Check if "error" key in response + assert "error" in response + +def test_create_profile_invalid_json(testrun, reset_profiles): # pylint: disable=W0613 + """Test for create profile invalid JSON payload """ + + # invalid JSON + new_profile = {} + + # Send the post request to update the profile + r = requests.post( + f"{API}/profiles", + data=json.dumps(new_profile), + timeout=5) + + # Parse the response + response = r.json() + + # Check if status code is 400 (Bad request) + assert r.status_code == 400 + + # Check if "error" key in response + assert "error" in response + def test_delete_profile(testrun, reset_profiles, add_profile): # pylint: disable=W0613 """Test for delete profile""" # Assign the profile from the fixture profile_to_delete = add_profile("new_profile.json") + # Assign the profile name profile_name = profile_to_delete["name"] @@ -1424,8 +1727,10 @@ def test_delete_profile(testrun, reset_profiles, add_profile): # pylint: disable # Check if status code is 200 (OK) assert r.status_code == 200 + # Parse the JSON response response = r.json() + # Check if the response contains "success" key assert "success" in response @@ -1434,6 +1739,7 @@ def test_delete_profile(testrun, reset_profiles, add_profile): # pylint: disable # Check if status code is 200 (OK) assert r.status_code == 200 + # Parse the JSON response profiles = r.json() @@ -1444,3 +1750,86 @@ def test_delete_profile(testrun, reset_profiles, add_profile): # pylint: disable ) # Check if profile was deleted assert deleted_profile is None + +def test_delete_profile_no_profile(testrun, reset_profiles): # pylint: disable=W0613 + """Test delete profile if the profile does not exists""" + + # Assign the profile to delete + profile_to_delete = {"name": "New Profile"} + + # Delete the profile + r = requests.delete( + f"{API}/profiles", + data=json.dumps(profile_to_delete), + timeout=5) + + # Check if status code is 404 (Profile does not exist) + assert r.status_code == 404 + +def test_delete_profile_invalid_json(testrun, reset_profiles): # pylint: disable=W0613 + """Test for delete profile wrong JSON payload""" + + profile_to_delete = {} + + # Delete the profile + r = requests.delete( + f"{API}/profiles", + data=json.dumps(profile_to_delete), + timeout=5) + + # Parse the response + response = r.json() + + # Check if status code is 400 (bad request) + assert r.status_code == 400 + + # Check if "error" key in response + assert "error" in response + + profile_to_delete_2 = {"status": "Draft"} + # Delete the profile + r = requests.delete( + f"{API}/profiles", + data=json.dumps(profile_to_delete_2), + timeout=5) + + # Parse the response + response = r.json() + + # Check if status code is 400 (bad request) + assert r.status_code == 400 + + # Check if "error" key in response + assert "error" in response + +def test_delete_profile_internal_server_error(testrun, # pylint: disable=W0613 + reset_profiles, # pylint: disable=W0613 + add_profile ): + """Test for delete profile causing internal server error""" + + # Assign the profile from the fixture + profile_to_delete = add_profile("new_profile.json") + + # Assign the profile name to profile_name + profile_name = profile_to_delete["name"] + + # Construct the path to the profile JSON file in local/risk_profiles + risk_profile_path = os.path.join(PROFILES_DIRECTORY, f"{profile_name}.json") + + # Delete the profile JSON file before making the DELETE request + if os.path.exists(risk_profile_path): + os.remove(risk_profile_path) + + # Send the DELETE request to delete the profile + r = requests.delete(f"{API}/profiles", + json={"name": profile_to_delete["name"]}, + timeout=5) + + # Check if status code is 500 (Internal Server Error) + assert r.status_code == 500 + + # Parse the json response + response = r.json() + + # Check if error in response + assert "error" in response diff --git a/testing/unit/ntp/reports/ntp_report_local.html b/testing/unit/ntp/reports/ntp_report_local.html index a08c42f9d..c9715fba5 100644 --- a/testing/unit/ntp/reports/ntp_report_local.html +++ b/testing/unit/ntp/reports/ntp_report_local.html @@ -25,7 +25,8 @@

NTP Module

Destination Type Version - Timestamp + Count + Sync Request Average @@ -34,1435 +35,80 @@

NTP Module

216.239.35.12 Client 4 - Feb 15, 2024 22:12:28.681 + 8 + 37.942 seconds 216.239.35.12 10.10.10.15 Server 4 - Feb 15, 2024 22:12:28.728 + 8 + N/A 10.10.10.15 216.239.35.4 Client 4 - Feb 15, 2024 22:12:28.842 + 8 + 37.834 seconds 216.239.35.4 10.10.10.15 Server 4 - Feb 15, 2024 22:12:28.888 + 8 + N/A 10.10.10.15 216.239.35.8 Client 4 - Feb 15, 2024 22:12:29.042 + 8 + 38.056 seconds 216.239.35.8 10.10.10.15 Server 4 - Feb 15, 2024 22:12:29.089 + 8 + N/A 10.10.10.15 216.239.35.0 Client 4 - Feb 15, 2024 22:12:29.243 + 14 + 20.601 seconds 216.239.35.0 10.10.10.15 Server 4 - Feb 15, 2024 22:12:29.290 + 17 + N/A 10.10.10.15 10.10.10.5 Client 4 - Feb 15, 2024 22:12:29.447 + 63 + 13.057 seconds 10.10.10.5 10.10.10.15 Server 4 - Feb 15, 2024 22:12:29.448 - - - 10.10.10.15 - 216.239.35.12 - Client - 4 - Feb 15, 2024 22:12:30.802 - - - 216.239.35.12 - 10.10.10.15 - Server - 4 - Feb 15, 2024 22:12:30.850 - - - 10.10.10.15 - 216.239.35.4 - Client - 4 - Feb 15, 2024 22:12:30.973 - - - 216.239.35.4 - 10.10.10.15 - Server - 4 - Feb 15, 2024 22:12:31.032 - - - 10.10.10.15 - 216.239.35.8 - Client - 4 - Feb 15, 2024 22:12:31.173 - - - 216.239.35.8 - 10.10.10.15 - Server - 4 - Feb 15, 2024 22:12:31.220 - - - 10.10.10.15 - 216.239.35.0 - Client - 4 - Feb 15, 2024 22:12:31.376 - - - 216.239.35.0 - 10.10.10.15 - Server - 4 - Feb 15, 2024 22:12:31.423 - - - 10.10.10.15 - 10.10.10.5 - Client - 4 - Feb 15, 2024 22:12:31.577 - - - 10.10.10.5 - 10.10.10.15 - Server - 4 - Feb 15, 2024 22:12:31.577 - - - 10.10.10.15 - 216.239.35.12 - Client - 4 - Feb 15, 2024 22:12:32.867 - - - 216.239.35.12 - 10.10.10.15 - Server - 4 - Feb 15, 2024 22:12:32.914 - - - 10.10.10.15 - 216.239.35.4 - Client - 4 - Feb 15, 2024 22:12:33.112 - - - 216.239.35.4 - 10.10.10.15 - Server - 4 - Feb 15, 2024 22:12:33.159 - - - 10.10.10.15 - 216.239.35.8 - Client - 4 - Feb 15, 2024 22:12:33.271 - - - 216.239.35.8 - 10.10.10.15 - Server - 4 - Feb 15, 2024 22:12:33.318 - - - 10.10.10.15 - 216.239.35.0 - Client - 4 - Feb 15, 2024 22:12:33.475 - - - 216.239.35.0 - 10.10.10.15 - Server - 4 - Feb 15, 2024 22:12:33.522 - - - 10.10.10.15 - 10.10.10.5 - Client - 4 - Feb 15, 2024 22:12:33.694 - - - 10.10.10.5 - 10.10.10.15 - Server - 4 - Feb 15, 2024 22:12:33.694 - - - 10.10.10.15 - 216.239.35.12 - Client - 4 - Feb 15, 2024 22:12:34.956 - - - 216.239.35.12 - 10.10.10.15 - Server - 4 - Feb 15, 2024 22:12:35.002 - - - 10.10.10.15 - 216.239.35.4 - Client - 4 - Feb 15, 2024 22:12:35.182 - - - 216.239.35.4 - 10.10.10.15 - Server - 4 - Feb 15, 2024 22:12:35.228 - - - 10.10.10.15 - 216.239.35.8 - Client - 4 - Feb 15, 2024 22:12:35.398 - - - 216.239.35.8 - 10.10.10.15 - Server - 4 - Feb 15, 2024 22:12:35.445 - - - 10.10.10.15 - 216.239.35.0 - Client - 4 - Feb 15, 2024 22:12:35.625 - - - 216.239.35.0 - 10.10.10.15 - Server - 4 - Feb 15, 2024 22:12:35.673 - - - 10.10.10.15 - 10.10.10.5 - Client - 4 - Feb 15, 2024 22:12:35.785 - - - 10.10.10.5 - 10.10.10.15 - Server - 4 - Feb 15, 2024 22:12:35.786 - - - 10.10.10.15 - 10.10.10.5 - Client - 4 - Feb 15, 2024 22:12:37.806 - - - 10.10.10.5 - 10.10.10.15 - Server - 4 - Feb 15, 2024 22:12:37.806 - - - 10.10.10.15 - 10.10.10.5 - Client - 4 - Feb 15, 2024 22:12:39.856 - - - 10.10.10.5 - 10.10.10.15 - Server - 4 - Feb 15, 2024 22:12:39.856 - - - 10.10.10.15 - 10.10.10.5 - Client - 4 - Feb 15, 2024 22:12:41.931 - - - 10.10.10.5 - 10.10.10.15 - Server - 4 - Feb 15, 2024 22:12:41.932 - - - 10.10.10.15 - 10.10.10.5 - Client - 4 - Feb 15, 2024 22:12:43.954 - - - 10.10.10.5 - 10.10.10.15 - Server - 4 - Feb 15, 2024 22:12:43.956 - - - 10.10.10.15 - 10.10.10.5 - Client - 4 - Feb 15, 2024 22:13:06.439 - - - 10.10.10.15 - 216.239.35.0 - Client - 4 - Feb 15, 2024 22:13:06.439 - - - 10.10.10.5 - 10.10.10.15 - Server - 4 - Feb 15, 2024 22:13:06.439 - - - 216.239.35.0 - 10.10.10.15 - Server - 4 - Feb 15, 2024 22:13:06.489 - - - 10.10.10.15 - 10.10.10.5 - Client - 4 - Feb 15, 2024 22:13:08.492 - - - 10.10.10.5 - 10.10.10.15 - Server - 4 - Feb 15, 2024 22:13:08.494 - - - 216.239.35.0 - 10.10.10.15 - Server - 4 - Feb 15, 2024 22:13:08.543 - - - 10.10.10.15 - 216.239.35.12 - Client - 4 - Feb 15, 2024 22:13:40.310 - - - 216.239.35.12 - 10.10.10.15 - Server - 4 - Feb 15, 2024 22:13:40.357 - - - 10.10.10.15 - 216.239.35.4 - Client - 4 - Feb 15, 2024 22:13:40.512 - - - 10.10.10.15 - 10.10.10.5 - Client - 4 - Feb 15, 2024 22:13:40.536 - - - 10.10.10.5 - 10.10.10.15 - Server - 4 - Feb 15, 2024 22:13:40.542 - - - 216.239.35.4 - 10.10.10.15 - Server - 4 - Feb 15, 2024 22:13:40.574 - - - 216.239.35.0 - 10.10.10.15 - Server - 4 - Feb 15, 2024 22:13:40.583 - - - 10.10.10.15 - 216.239.35.8 - Client - 4 - Feb 15, 2024 22:13:40.714 - - - 216.239.35.8 - 10.10.10.15 - Server - 4 - Feb 15, 2024 22:13:40.764 - - - 10.10.10.15 - 216.239.35.0 - Client - 4 - Feb 15, 2024 22:13:40.917 - - - 216.239.35.0 - 10.10.10.15 - Server - 4 - Feb 15, 2024 22:13:40.965 - - - 10.10.10.15 - 10.10.10.5 - Client - 4 - Feb 15, 2024 22:13:48.274 - - - 10.10.10.5 - 10.10.10.15 - Server - 4 - Feb 15, 2024 22:13:48.277 - - - 10.10.10.15 - 10.10.10.5 - Client - 4 - Feb 15, 2024 22:14:12.619 - - - 10.10.10.5 - 10.10.10.15 - Server - 4 - Feb 15, 2024 22:14:12.624 - - - 216.239.35.0 - 10.10.10.15 - Server - 4 - Feb 15, 2024 22:14:12.668 - - - 10.10.10.15 - 216.239.35.12 - Client - 4 - Feb 15, 2024 22:14:44.515 - - - 216.239.35.12 - 10.10.10.15 - Server - 4 - Feb 15, 2024 22:14:44.562 - - - 10.10.10.15 - 10.10.10.5 - Client - 4 - Feb 15, 2024 22:14:44.702 - - - 10.10.10.5 - 10.10.10.15 - Server - 4 - Feb 15, 2024 22:14:44.704 - - - 10.10.10.15 - 216.239.35.4 - Client - 4 - Feb 15, 2024 22:14:45.158 - - - 216.239.35.4 - 10.10.10.15 - Server - 4 - Feb 15, 2024 22:14:45.219 - - - 10.10.10.15 - 216.239.35.0 - Client - 4 - Feb 15, 2024 22:14:45.359 - - - 216.239.35.0 - 10.10.10.15 - Server - 4 - Feb 15, 2024 22:14:45.406 - - - 10.10.10.15 - 216.239.35.0 - Client - 4 - Feb 15, 2024 22:14:45.707 - - - 216.239.35.0 - 10.10.10.15 - Server - 4 - Feb 15, 2024 22:14:45.755 - - - 10.10.10.15 - 216.239.35.8 - Client - 4 - Feb 15, 2024 22:14:45.980 - - - 216.239.35.8 - 10.10.10.15 - Server - 4 - Feb 15, 2024 22:14:46.027 - - - 10.10.10.15 - 10.10.10.5 - Client - 4 - Feb 15, 2024 22:14:53.026 - - - 10.10.10.5 - 10.10.10.15 - Server - 4 - Feb 15, 2024 22:14:53.029 - - - 10.10.10.15 - 10.10.10.5 - Client - 4 - Feb 15, 2024 22:15:16.786 - - - 10.10.10.5 - 10.10.10.15 - Server - 4 - Feb 15, 2024 22:15:16.791 - - - 10.10.10.15 - 216.239.35.0 - Client - 4 - Feb 15, 2024 22:15:18.794 - - - 216.239.35.0 - 10.10.10.15 - Server - 4 - Feb 15, 2024 22:15:18.843 - - - 10.10.10.15 - 10.10.10.5 - Client - 4 - Feb 15, 2024 22:15:48.884 - - - 10.10.10.5 - 10.10.10.15 - Server - 4 - Feb 15, 2024 22:15:48.887 - - - 10.10.10.15 - 216.239.35.12 - Client - 4 - Feb 15, 2024 22:15:49.063 - - - 216.239.35.12 - 10.10.10.15 - Server - 4 - Feb 15, 2024 22:15:49.110 - - - 10.10.10.15 - 216.239.35.4 - Client - 4 - Feb 15, 2024 22:15:49.462 - - - 216.239.35.4 - 10.10.10.15 - Server - 4 - Feb 15, 2024 22:15:49.509 - - - 10.10.10.15 - 216.239.35.0 - Client - 4 - Feb 15, 2024 22:15:50.127 - - - 216.239.35.0 - 10.10.10.15 - Server - 4 - Feb 15, 2024 22:15:50.175 - - - 10.10.10.15 - 216.239.35.8 - Client - 4 - Feb 15, 2024 22:15:51.107 - - - 216.239.35.8 - 10.10.10.15 - Server - 4 - Feb 15, 2024 22:15:51.154 - - - 10.10.10.15 - 216.239.35.0 - Client - 4 - Feb 15, 2024 22:15:51.890 - - - 216.239.35.0 - 10.10.10.15 - Server - 4 - Feb 15, 2024 22:15:51.938 - - - 10.10.10.15 - 10.10.10.5 - Client - 4 - Feb 15, 2024 22:15:57.829 - - - 10.10.10.5 - 10.10.10.15 - Server - 4 - Feb 15, 2024 22:15:57.829 - - - 10.10.10.15 - 10.10.10.5 - Client - 4 - Feb 15, 2024 22:16:20.970 - - - 10.10.10.5 - 10.10.10.15 - Server - 4 - Feb 15, 2024 22:16:20.971 - - - 10.10.10.15 - 216.239.35.0 - Client - 4 - Feb 15, 2024 22:16:24.975 - - - 216.239.35.0 - 10.10.10.15 - Server - 4 - Feb 15, 2024 22:16:25.023 - - - 10.10.10.15 - 216.239.35.4 - Client - 4 - Feb 15, 2024 22:16:53.677 - - - 216.239.35.4 - 10.10.10.15 - Server - 4 - Feb 15, 2024 22:16:53.739 - - - 10.10.10.15 - 10.10.10.5 - Client - 4 - Feb 15, 2024 22:16:54.054 - - - 10.10.10.5 - 10.10.10.15 - Server - 4 - Feb 15, 2024 22:16:54.054 - - - 10.10.10.15 - 216.239.35.12 - Client - 4 - Feb 15, 2024 22:16:54.276 - - - 216.239.35.12 - 10.10.10.15 - Server - 4 - Feb 15, 2024 22:16:54.322 - - - 10.10.10.15 - 216.239.35.0 - Client - 4 - Feb 15, 2024 22:16:54.593 - - - 216.239.35.0 - 10.10.10.15 - Server - 4 - Feb 15, 2024 22:16:54.648 - - - 10.10.10.15 - 216.239.35.8 - Client - 4 - Feb 15, 2024 22:16:55.435 - - - 216.239.35.8 - 10.10.10.15 - Server - 4 - Feb 15, 2024 22:16:55.481 - - - 10.10.10.15 - 216.239.35.0 - Client - 4 - Feb 15, 2024 22:16:57.059 - - - 216.239.35.0 - 10.10.10.15 - Server - 4 - Feb 15, 2024 22:16:57.107 - - - 10.10.10.15 - 10.10.10.5 - Client - 4 - Feb 15, 2024 22:17:02.738 - - - 10.10.10.5 - 10.10.10.15 - Server - 4 - Feb 15, 2024 22:17:02.740 - - - 10.10.10.15 - 10.10.10.5 - Client - 4 - Feb 15, 2024 22:17:26.136 - - - 10.10.10.5 - 10.10.10.15 - Server - 4 - Feb 15, 2024 22:17:26.139 - - - 10.10.10.15 - 10.10.10.5 - Client - 4 - Feb 15, 2024 22:12:29.447 - - - 10.10.10.5 - 10.10.10.15 - Server - 4 - Feb 15, 2024 22:12:29.448 - - - 10.10.10.15 - 10.10.10.5 - Client - 4 - Feb 15, 2024 22:12:31.577 - - - 10.10.10.5 - 10.10.10.15 - Server - 4 - Feb 15, 2024 22:12:31.577 - - - 10.10.10.15 - 10.10.10.5 - Client - 4 - Feb 15, 2024 22:12:33.694 - - - 10.10.10.5 - 10.10.10.15 - Server - 4 - Feb 15, 2024 22:12:33.694 - - - 10.10.10.15 - 10.10.10.5 - Client - 4 - Feb 15, 2024 22:12:35.785 - - - 10.10.10.5 - 10.10.10.15 - Server - 4 - Feb 15, 2024 22:12:35.786 - - - 10.10.10.15 - 10.10.10.5 - Client - 4 - Feb 15, 2024 22:12:37.806 - - - 10.10.10.5 - 10.10.10.15 - Server - 4 - Feb 15, 2024 22:12:37.806 - - - 10.10.10.15 - 10.10.10.5 - Client - 4 - Feb 15, 2024 22:12:39.856 - - - 10.10.10.5 - 10.10.10.15 - Server - 4 - Feb 15, 2024 22:12:39.856 - - - 10.10.10.15 - 10.10.10.5 - Client - 4 - Feb 15, 2024 22:12:41.931 - - - 10.10.10.5 - 10.10.10.15 - Server - 4 - Feb 15, 2024 22:12:41.932 - - - 10.10.10.15 - 10.10.10.5 - Client - 4 - Feb 15, 2024 22:12:43.954 - - - 10.10.10.5 - 10.10.10.15 - Server - 4 - Feb 15, 2024 22:12:43.956 - - - 10.10.10.15 - 10.10.10.5 - Client - 4 - Feb 15, 2024 22:13:06.439 - - - 10.10.10.5 - 10.10.10.15 - Server - 4 - Feb 15, 2024 22:13:06.439 - - - 10.10.10.15 - 10.10.10.5 - Client - 4 - Feb 15, 2024 22:13:08.492 - - - 10.10.10.5 - 10.10.10.15 - Server - 4 - Feb 15, 2024 22:13:08.494 - - - 10.10.10.15 - 10.10.10.5 - Client - 4 - Feb 15, 2024 22:13:40.536 - - - 10.10.10.5 - 10.10.10.15 - Server - 4 - Feb 15, 2024 22:13:40.541 - - - 10.10.10.15 - 10.10.10.5 - Client - 4 - Feb 15, 2024 22:13:48.274 - - - 10.10.10.5 - 10.10.10.15 - Server - 4 - Feb 15, 2024 22:13:48.277 - - - 10.10.10.15 - 10.10.10.5 - Client - 4 - Feb 15, 2024 22:14:12.619 - - - 10.10.10.5 - 10.10.10.15 - Server - 4 - Feb 15, 2024 22:14:12.624 - - - 10.10.10.15 - 10.10.10.5 - Client - 4 - Feb 15, 2024 22:14:44.702 - - - 10.10.10.5 - 10.10.10.15 - Server - 4 - Feb 15, 2024 22:14:44.703 - - - 10.10.10.15 - 10.10.10.5 - Client - 4 - Feb 15, 2024 22:14:53.026 - - - 10.10.10.5 - 10.10.10.15 - Server - 4 - Feb 15, 2024 22:14:53.029 - - - 10.10.10.15 - 10.10.10.5 - Client - 4 - Feb 15, 2024 22:15:16.786 - - - 10.10.10.5 - 10.10.10.15 - Server - 4 - Feb 15, 2024 22:15:16.791 - - - 10.10.10.15 - 10.10.10.5 - Client - 4 - Feb 15, 2024 22:15:48.884 - - - 10.10.10.5 - 10.10.10.15 - Server - 4 - Feb 15, 2024 22:15:48.887 - - - 10.10.10.15 - 10.10.10.5 - Client - 4 - Feb 15, 2024 22:15:57.829 - - - 10.10.10.5 - 10.10.10.15 - Server - 4 - Feb 15, 2024 22:15:57.829 - - - 10.10.10.15 - 10.10.10.5 - Client - 4 - Feb 15, 2024 22:16:20.970 - - - 10.10.10.5 - 10.10.10.15 - Server - 4 - Feb 15, 2024 22:16:20.970 - - - 10.10.10.15 - 10.10.10.5 - Client - 4 - Feb 15, 2024 22:16:54.054 - - - 10.10.10.5 - 10.10.10.15 - Server - 4 - Feb 15, 2024 22:16:54.054 - - - 10.10.10.15 - 10.10.10.5 - Client - 4 - Feb 15, 2024 22:17:02.738 - - - 10.10.10.5 - 10.10.10.15 - Server - 4 - Feb 15, 2024 22:17:02.740 - - - 10.10.10.15 - 10.10.10.5 - Client - 4 - Feb 15, 2024 22:17:26.136 - - - 10.10.10.5 - 10.10.10.15 - Server - 4 - Feb 15, 2024 22:17:26.139 - - - 10.10.10.15 - 10.10.10.5 - Client - 4 - Feb 15, 2024 22:17:59.293 - - - 10.10.10.5 - 10.10.10.15 - Server - 4 - Feb 15, 2024 22:17:59.293 - - - 10.10.10.15 - 10.10.10.5 - Client - 4 - Feb 15, 2024 22:18:07.242 - - - 10.10.10.5 - 10.10.10.15 - Server - 4 - Feb 15, 2024 22:18:07.242 - - - 10.10.10.15 - 10.10.10.5 - Client - 4 - Feb 15, 2024 22:18:32.379 - - - 10.10.10.5 - 10.10.10.15 - Server - 4 - Feb 15, 2024 22:18:32.379 - - - 10.10.10.15 - 10.10.10.5 - Client - 4 - Feb 15, 2024 22:20:06.908 - - - 10.10.10.5 - 10.10.10.15 - Server - 4 - Feb 15, 2024 22:20:06.908 - - - 10.10.10.15 - 10.10.10.5 - Client - 4 - Feb 15, 2024 22:20:08.936 - - - 10.10.10.5 - 10.10.10.15 - Server - 4 - Feb 15, 2024 22:20:08.937 - - - 10.10.10.15 - 10.10.10.5 - Client - 4 - Feb 15, 2024 22:20:10.974 - - - 10.10.10.5 - 10.10.10.15 - Server - 4 - Feb 15, 2024 22:20:10.974 - - - 10.10.10.15 - 10.10.10.5 - Client - 4 - Feb 15, 2024 22:20:12.998 - - - 10.10.10.5 - 10.10.10.15 - Server - 4 - Feb 15, 2024 22:20:12.999 - - - 10.10.10.15 - 10.10.10.5 - Client - 4 - Feb 15, 2024 22:20:59.581 - - - 10.10.10.5 - 10.10.10.15 - Server - 4 - Feb 15, 2024 22:20:59.582 - - - 10.10.10.15 - 10.10.10.5 - Client - 4 - Feb 15, 2024 22:21:34.063 - - - 10.10.10.5 - 10.10.10.15 - Server - 4 - Feb 15, 2024 22:21:34.063 - - - 10.10.10.15 - 10.10.10.5 - Client - 4 - Feb 15, 2024 22:21:36.121 - - - 10.10.10.5 - 10.10.10.15 - Server - 4 - Feb 15, 2024 22:21:36.121 - - - 10.10.10.15 - 10.10.10.5 - Client - 4 - Feb 15, 2024 22:21:38.176 - - - 10.10.10.5 - 10.10.10.15 - Server - 4 - Feb 15, 2024 22:21:38.176 - - - 10.10.10.15 - 10.10.10.5 - Client - 4 - Feb 15, 2024 22:21:40.277 - - - 10.10.10.5 - 10.10.10.15 - Server - 4 - Feb 15, 2024 22:21:40.277 - - - 10.10.10.15 - 10.10.10.5 - Client - 4 - Feb 15, 2024 22:22:05.704 - - - 10.10.10.5 - 10.10.10.15 - Server - 4 - Feb 15, 2024 22:22:05.706 - - - 10.10.10.15 - 10.10.10.5 - Client - 4 - Feb 15, 2024 22:22:45.469 - - - 10.10.10.5 - 10.10.10.15 - Server - 4 - Feb 15, 2024 22:22:45.470 - - - 10.10.10.15 - 10.10.10.5 - Client - 4 - Feb 15, 2024 22:23:09.826 - - - 10.10.10.5 - 10.10.10.15 - Server - 4 - Feb 15, 2024 22:23:09.828 - - - 10.10.10.15 - 10.10.10.5 - Client - 4 - Feb 15, 2024 22:23:50.337 - - - 10.10.10.5 - 10.10.10.15 - Server - 4 - Feb 15, 2024 22:23:50.343 - - - 10.10.10.15 - 10.10.10.5 - Client - 4 - Feb 15, 2024 22:24:13.945 - - - 10.10.10.5 - 10.10.10.15 - Server - 4 - Feb 15, 2024 22:24:13.946 - - - 10.10.10.15 - 10.10.10.5 - Client - 4 - Feb 15, 2024 22:24:54.876 - - - 10.10.10.5 - 10.10.10.15 - Server - 4 - Feb 15, 2024 22:24:54.877 - - - 10.10.10.15 - 10.10.10.5 - Client - 4 - Feb 15, 2024 22:25:59.000 - - - 10.10.10.5 - 10.10.10.15 - Server - 4 - Feb 15, 2024 22:25:59.001 + 63 + N/A