Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
21ce171
changed the tests order in test_api.py
MariusBaldovin Jul 31, 2024
d57b399
added tests for '/system/config' POST endpoint
MariusBaldovin Jul 31, 2024
86f7350
Merge branch 'google:dev' into dev
MariusBaldovin Jul 31, 2024
5ac2a0c
added the tests for 'system/shutdown' endpoint
MariusBaldovin Jul 31, 2024
ebdc13f
added the test for GET '/reports' endpoint, updated 'test_update_syst…
MariusBaldovin Jul 31, 2024
8838c49
Merge branch 'google:dev' into dev
MariusBaldovin Jul 31, 2024
b1c1ecc
Check for missing fields
jboddey Jul 31, 2024
413f59c
added tests for delete profile (404, 400), added tests for create and…
MariusBaldovin Aug 1, 2024
c5275cd
added a new json file in '/testing/api/' used in 400 error tests
MariusBaldovin Aug 1, 2024
2beff3e
added error handling if 'name' and 'questions' not in profile json
MariusBaldovin Aug 1, 2024
07b27ba
Merge branch 'google:dev' into dev
MariusBaldovin Aug 1, 2024
2a0babd
fixed pylint
MariusBaldovin Aug 1, 2024
2db4e85
Merge branch 'google:dev' into dev
MariusBaldovin Aug 1, 2024
a312252
Merge branch 'dev' into dev
jboddey Aug 2, 2024
a012f79
Merge branch 'google:dev' into dev
MariusBaldovin Aug 2, 2024
18ef656
Added tests when update is available and 500 status code for '/system…
MariusBaldovin Aug 2, 2024
9f436ad
added responses library in requirements.txt
MariusBaldovin Aug 2, 2024
87904c9
fixed the requested changes in api.py
MariusBaldovin Aug 2, 2024
c445691
Renamed the load_profile method to load_json and changed the logic to…
MariusBaldovin Aug 2, 2024
d7cd175
updated restore_config fixture to run after the test
MariusBaldovin Aug 2, 2024
eadfcbd
added test for create/update profile (500 error)
MariusBaldovin Aug 2, 2024
ae880d6
Merge branch 'dev' into dev
jboddey Aug 4, 2024
1f7c493
Merge branch 'google:dev' into dev
MariusBaldovin Aug 5, 2024
5fe6533
fixed pylint
MariusBaldovin Aug 5, 2024
9212935
fixed spacing, removed get_report_one_report
MariusBaldovin Aug 5, 2024
22dfbe0
added tests: 500 error for delete '/profiles', 500 error for 'profle…
MariusBaldovin Aug 5, 2024
7b49ece
modified the tests for 500 response
MariusBaldovin Aug 6, 2024
5f886f6
added new profile with missing 'answer'
MariusBaldovin Aug 6, 2024
b8b97e9
removed the tests with mock response
MariusBaldovin Aug 6, 2024
99644b7
Update NTP report (#666)
jhughesoti Aug 6, 2024
5f389d8
Merge branch 'google:dev' into dev
MariusBaldovin Aug 7, 2024
aa7e18e
Merge branch 'google:dev' into dev
MariusBaldovin Aug 7, 2024
ab055da
modified update profile for bad request
MariusBaldovin Aug 7, 2024
9c60f61
changed validate_profile_json: handling empty spaces in name and ques…
MariusBaldovin Aug 8, 2024
0408130
updated the requested changes
MariusBaldovin Aug 8, 2024
f391c6f
Add further profile validation
jboddey Aug 8, 2024
0f45f85
Merge branch 'dev' of https://github.com/MariusBaldovin/testrun into dev
jboddey Aug 8, 2024
6844da1
Fix pylint issues
jboddey Aug 8, 2024
117a873
Merge branch 'google:dev' into dev
MariusBaldovin Aug 8, 2024
c4c1803
Fix profile tests
jboddey Aug 8, 2024
d66b210
Merge branch 'dev' of https://github.com/MariusBaldovin/testrun into dev
jboddey Aug 8, 2024
28c6c8e
Merge branch 'release/v1.4.1' into dev
jboddey Aug 9, 2024
d481813
Merge branch 'google:dev' into dev
MariusBaldovin Aug 12, 2024
a5b4fff
Move validation to session
jboddey Aug 13, 2024
bbab611
Fix pylint issue
jboddey Aug 13, 2024
b0ff6eb
Merge branch 'google:dev' into dev
MariusBaldovin Aug 13, 2024
0082d76
Merge branch 'google:dev' into dev
MariusBaldovin Aug 13, 2024
142fe65
Merge branch 'dev' into dev
jboddey Aug 14, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions framework/python/src/api/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
166 changes: 114 additions & 52 deletions framework/python/src/common/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -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'):
Expand All @@ -505,47 +491,23 @@ 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']

# Add version, timestamp and status
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
Expand Down Expand Up @@ -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:
Expand Down
2 changes: 2 additions & 0 deletions framework/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
76 changes: 52 additions & 24 deletions modules/test/ntp/python/src/ntp_module.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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'''
<table class="module-summary">
Expand All @@ -92,7 +119,6 @@ def generate_module_report(self):
''')

if total_requests + total_responses > 0:

table_content = '''
<table class="module-data">
<thead>
Expand All @@ -101,37 +127,39 @@ def generate_module_report(self):
<th>Destination</th>
<th>Type</th>
<th>Version</th>
<th>Timestamp</th>
<th>Count</th>
<th>Sync Request Average</th>
</tr>
</thead>
<tbody>'''

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'''
<tr>
<td>{row['Source']}</td>
<td>{row['Destination']}</td>
<td>{row['Type']}</td>
<td>{row['Version']}</td>
<td>{formatted_time}</td>
</tr>''')
<td>{src}</td>
<td>{dst}</td>
<td>{typ}</td>
<td>{version}</td>
<td>{cnt}</td>
<td>{avg_formatted_time}</td>
</tr>'''

table_content += '''
</tbody>
</table>
'''

html_content += table_content

else:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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'

Expand Down
Loading