From a9485352ea8b64bfeb2c5958f54d10de121e2101 Mon Sep 17 00:00:00 2001 From: Jacob Boddey Date: Sat, 29 Jun 2024 14:33:32 +0100 Subject: [PATCH 01/10] Fix bug when saving draft --- framework/python/src/common/risk_profile.py | 27 +++++++++-- framework/python/src/common/session.py | 50 ++++++++++++--------- 2 files changed, 53 insertions(+), 24 deletions(-) diff --git a/framework/python/src/common/risk_profile.py b/framework/python/src/common/risk_profile.py index ab3251694..b2e49cb26 100644 --- a/framework/python/src/common/risk_profile.py +++ b/framework/python/src/common/risk_profile.py @@ -38,7 +38,7 @@ def __init__(self, profile_json=None, profile_format=None): self.risk = None self._validate(profile_json, profile_format) - self._update_risk(profile_format) + self.update_risk(profile_format) # Load a profile without modifying the created date # but still validate the profile @@ -51,7 +51,7 @@ def load(self, profile_json, profile_format): self.status = None self._validate(profile_json, profile_format) - self._update_risk(profile_format) + self.update_risk(profile_format) return self @@ -76,11 +76,17 @@ def get_file_path(self): def _validate(self, profile_json, profile_format): if self._valid(profile_json, profile_format): - self.status = 'Expired' if self._expired() else 'Valid' + if self._expired(): + self.status = 'Expired' + # User only wants to save a draft + elif 'status' in profile_json and profile_json['status'] == 'Draft': + self.status = 'Draft' + else: + self.status = 'Valid' else: self.status = 'Draft' - def _update_risk(self, profile_format): + def update_risk(self, profile_format): if self.status == 'Valid': @@ -224,6 +230,19 @@ def _valid(self, profile_json, profile_format): LOGGER.error('Missing answer for question: ' + profile_question.get('question')) all_questions_answered = False + + answer = profile_question.get('answer') + + # Check if a multi-select answer has been completed + if isinstance(answer, list): + if len(answer) == 0: + all_questions_answered = False + + # Check if string answer has a length greater than 0 + elif isinstance(answer, str): + if required and len(answer) == 0: + all_questions_answered = False + elif required: LOGGER.error('Missing question: ' + format_question.get('question')) all_questions_present = False diff --git a/framework/python/src/common/session.py b/framework/python/src/common/session.py index 736f625a4..0863c0c5c 100644 --- a/framework/python/src/common/session.py +++ b/framework/python/src/common/session.py @@ -391,25 +391,25 @@ def _load_profiles(self): # Load existing profiles LOGGER.debug('Loading risk profiles') - try: - for risk_profile_file in os.listdir( - os.path.join(self._root_dir, PROFILES_DIR)): - LOGGER.debug(f'Discovered profile {risk_profile_file}') - - with open(os.path.join(self._root_dir, PROFILES_DIR, risk_profile_file), - encoding='utf-8') as f: - json_data = json.load(f) - risk_profile = RiskProfile() - risk_profile.load( - profile_json=json_data, - profile_format=self._profile_format - ) - risk_profile.status = self.check_profile_status(risk_profile) - self._profiles.append(risk_profile) - - except Exception as e: - LOGGER.error('An error occurred whilst loading risk profiles') - LOGGER.debug(e) + #try: + for risk_profile_file in os.listdir( + os.path.join(self._root_dir, PROFILES_DIR)): + LOGGER.debug(f'Discovered profile {risk_profile_file}') + + with open(os.path.join(self._root_dir, PROFILES_DIR, risk_profile_file), + encoding='utf-8') as f: + json_data = json.load(f) + risk_profile = RiskProfile() + risk_profile.load( + profile_json=json_data, + profile_format=self._profile_format + ) + risk_profile.status = self.check_profile_status(risk_profile) + self._profiles.append(risk_profile) + + #except Exception as e: + # LOGGER.error('An error occurred whilst loading risk profiles') + # LOGGER.debug(e) def get_profiles_format(self): return self._profile_format_json @@ -510,6 +510,16 @@ def update_profile(self, profile_json): risk_profile.name = new_name + # Update status + if 'status' in profile_json: + risk_profile.status = profile_json['status'] + + if risk_profile.status == 'Valid': + # Update created date + risk_profile.created = datetime.datetime.now() + # Update risk + risk_profile.update_risk(self._profile_format) + # Update questions and answers risk_profile.questions = profile_json.get('questions') @@ -517,7 +527,7 @@ def update_profile(self, profile_json): with open(os.path.join(PROFILES_DIR, risk_profile.name + '.json'), 'w', encoding='utf-8') as f: - f.write(json.dumps(risk_profile.to_json())) + f.write(risk_profile.to_json()) return risk_profile From 00282118faf1b4651541fff73d3ad85438e67ab0 Mon Sep 17 00:00:00 2001 From: Jacob Boddey Date: Sat, 29 Jun 2024 14:40:53 +0100 Subject: [PATCH 02/10] Remove risk when status changes to draft --- framework/python/src/common/session.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/framework/python/src/common/session.py b/framework/python/src/common/session.py index 0863c0c5c..b2373d548 100644 --- a/framework/python/src/common/session.py +++ b/framework/python/src/common/session.py @@ -519,6 +519,8 @@ def update_profile(self, profile_json): risk_profile.created = datetime.datetime.now() # Update risk risk_profile.update_risk(self._profile_format) + else: + risk_profile.risk = None # Update questions and answers risk_profile.questions = profile_json.get('questions') From ff69e49614602a187d6f60770e15b9f1e7ca21ed Mon Sep 17 00:00:00 2001 From: Jacob Boddey Date: Mon, 1 Jul 2024 15:45:09 +0100 Subject: [PATCH 03/10] Re-add exception handling --- framework/python/src/common/session.py | 38 +++++++++++++------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/framework/python/src/common/session.py b/framework/python/src/common/session.py index b2373d548..87e1fea61 100644 --- a/framework/python/src/common/session.py +++ b/framework/python/src/common/session.py @@ -391,25 +391,25 @@ def _load_profiles(self): # Load existing profiles LOGGER.debug('Loading risk profiles') - #try: - for risk_profile_file in os.listdir( - os.path.join(self._root_dir, PROFILES_DIR)): - LOGGER.debug(f'Discovered profile {risk_profile_file}') - - with open(os.path.join(self._root_dir, PROFILES_DIR, risk_profile_file), - encoding='utf-8') as f: - json_data = json.load(f) - risk_profile = RiskProfile() - risk_profile.load( - profile_json=json_data, - profile_format=self._profile_format - ) - risk_profile.status = self.check_profile_status(risk_profile) - self._profiles.append(risk_profile) - - #except Exception as e: - # LOGGER.error('An error occurred whilst loading risk profiles') - # LOGGER.debug(e) + try: + for risk_profile_file in os.listdir( + os.path.join(self._root_dir, PROFILES_DIR)): + LOGGER.debug(f'Discovered profile {risk_profile_file}') + + with open(os.path.join(self._root_dir, PROFILES_DIR, risk_profile_file), + encoding='utf-8') as f: + json_data = json.load(f) + risk_profile = RiskProfile() + risk_profile.load( + profile_json=json_data, + profile_format=self._profile_format + ) + risk_profile.status = self.check_profile_status(risk_profile) + self._profiles.append(risk_profile) + + except Exception as e: + LOGGER.error('An error occurred whilst loading risk profiles') + LOGGER.debug(e) def get_profiles_format(self): return self._profile_format_json From de92bbbc9acf204a0b193c296f8c5127a7e39faa Mon Sep 17 00:00:00 2001 From: Jacob Boddey Date: Mon, 1 Jul 2024 16:38:15 +0100 Subject: [PATCH 04/10] Fix formatting : --- framework/python/src/common/session.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/framework/python/src/common/session.py b/framework/python/src/common/session.py index 87e1fea61..605cfb6fd 100644 --- a/framework/python/src/common/session.py +++ b/framework/python/src/common/session.py @@ -408,8 +408,8 @@ def _load_profiles(self): self._profiles.append(risk_profile) except Exception as e: - LOGGER.error('An error occurred whilst loading risk profiles') - LOGGER.debug(e) + LOGGER.error('An error occurred whilst loading risk profiles') + LOGGER.debug(e) def get_profiles_format(self): return self._profile_format_json From 096d3e761cf49bdb2240e9a1699bb76ad28acf6e Mon Sep 17 00:00:00 2001 From: Jacob Boddey Date: Mon, 1 Jul 2024 22:52:03 +0100 Subject: [PATCH 05/10] Update questions before calculating risk --- framework/python/src/common/risk_profile.py | 5 ++--- framework/python/src/common/session.py | 6 +++--- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/framework/python/src/common/risk_profile.py b/framework/python/src/common/risk_profile.py index b2e49cb26..cc7109f03 100644 --- a/framework/python/src/common/risk_profile.py +++ b/framework/python/src/common/risk_profile.py @@ -109,8 +109,8 @@ def update_risk(self, profile_format): # We only want to check the select or select-multiple # questions for now if format_q['type'] in ['select', 'select-multiple']: - answer = question['answer'] + answer = question['answer'] question_risk = 'Limited' # The answer is a single string (select) @@ -149,11 +149,10 @@ def update_risk(self, profile_format): if 'risk' in question and question['risk'] == 'High': risk = 'High' - self.risk = risk - else: # Remove risk risk = None + self.risk = risk def _get_format_question(self, question, profile_format): diff --git a/framework/python/src/common/session.py b/framework/python/src/common/session.py index 605cfb6fd..4268fbfbb 100644 --- a/framework/python/src/common/session.py +++ b/framework/python/src/common/session.py @@ -510,6 +510,9 @@ def update_profile(self, profile_json): risk_profile.name = new_name + # Update questions and answers + risk_profile.questions = profile_json.get('questions') + # Update status if 'status' in profile_json: risk_profile.status = profile_json['status'] @@ -522,9 +525,6 @@ def update_profile(self, profile_json): else: risk_profile.risk = None - # Update questions and answers - risk_profile.questions = profile_json.get('questions') - # Write file to disk with open(os.path.join(PROFILES_DIR, risk_profile.name + '.json'), 'w', From fea454a7e08fcbe73f955bfc60c4e364daa6ba9e Mon Sep 17 00:00:00 2001 From: Jacob Boddey Date: Mon, 1 Jul 2024 23:19:10 +0100 Subject: [PATCH 06/10] Update unit testing --- modules/test/services/python/requirements.txt | 2 +- testing/unit/build.sh | 2 +- testing/unit/ntp/ntp_module_test.py | 2 +- testing/unit/report/report_test.py | 2 +- .../profiles/risk_profile_draft.json | 134 +++-------------- .../profiles/risk_profile_expired.json | 142 +++--------------- .../profiles/risk_profile_valid_high.json | 142 +++--------------- .../profiles/risk_profile_valid_limited.json | 138 +++-------------- .../unit/risk_profile/risk_profile_test.py | 4 +- testing/unit/run.sh | 2 +- testing/unit/run_tests.sh | 2 +- ... => services_report_all_closed_local.html} | 0 ..._local.html => services_report_local.html} | 0 testing/unit/unit_test.Dockerfile | 11 +- 14 files changed, 99 insertions(+), 484 deletions(-) rename testing/unit/services/reports/{nmap_report_all_closed_local.html => services_report_all_closed_local.html} (100%) rename testing/unit/services/reports/{nmap_report_local.html => services_report_local.html} (100%) diff --git a/modules/test/services/python/requirements.txt b/modules/test/services/python/requirements.txt index 42669b12c..a3fdd1857 100644 --- a/modules/test/services/python/requirements.txt +++ b/modules/test/services/python/requirements.txt @@ -1 +1 @@ -xmltodict \ No newline at end of file +xmltodict==0.13.0 \ No newline at end of file diff --git a/testing/unit/build.sh b/testing/unit/build.sh index 71f45f109..ae2b467e6 100644 --- a/testing/unit/build.sh +++ b/testing/unit/build.sh @@ -14,4 +14,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -docker build -f testing/unit/unit_test.Dockerfile -t test-run/unit-test . \ No newline at end of file +sudo docker build -f testing/unit/unit_test.Dockerfile -t test-run/unit-test . \ No newline at end of file diff --git a/testing/unit/ntp/ntp_module_test.py b/testing/unit/ntp/ntp_module_test.py index dec66f239..20dd88ef1 100644 --- a/testing/unit/ntp/ntp_module_test.py +++ b/testing/unit/ntp/ntp_module_test.py @@ -63,7 +63,7 @@ def ntp_module_report_test(self): formatted_report = self.add_formatting(report_out) # Write back the new formatted_report value - out_report_path = os.path.join(OUTPUT_DIR,'ntp_report_with_ntp.html') + out_report_path = os.path.join(OUTPUT_DIR, 'ntp_report_with_ntp.html') with open(out_report_path, 'w', encoding='utf-8') as file: file.write(formatted_report) diff --git a/testing/unit/report/report_test.py b/testing/unit/report/report_test.py index 180fabb9b..f92666b2c 100644 --- a/testing/unit/report/report_test.py +++ b/testing/unit/report/report_test.py @@ -43,7 +43,7 @@ def create_report(self, results_file_path): reports_md = [] #reports_md.append(self.get_module_html_report('tls')) reports_md.append(self.get_module_html_report('dns')) - reports_md.append(self.get_module_html_report('nmap')) + reports_md.append(self.get_module_html_report('services')) reports_md.append(self.get_module_html_report('ntp')) report.add_module_reports(reports_md) diff --git a/testing/unit/risk_profile/profiles/risk_profile_draft.json b/testing/unit/risk_profile/profiles/risk_profile_draft.json index 315ec3417..de3e9a46d 100644 --- a/testing/unit/risk_profile/profiles/risk_profile_draft.json +++ b/testing/unit/risk_profile/profiles/risk_profile_draft.json @@ -1,144 +1,56 @@ { "name": "Primary profile", - "status": "Valid", - "created": "2024-05-23", - "version": "v1.3", + "version": "1.3-alpha", + "created": "2024-07-01", + "status": "Draft", "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": "Sensor - Lighting" }, { "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": "sakjdhaskjdh" }, { "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": "N/A" + }, + { + "question": "Are any of the following statements true about your device?", + "answer": [ + 3 + ] }, { - "category": "Data Transmission", "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" - ], - "validation": { - "required": true - } + ] }, { - "category": "Data Transmission", "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" }, { - "category": "Remote Operation", "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" - ], - "validation": { - "required": true - } + 5 + ] }, { - "category": "Operating Environment", "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." - ], - "validation": { - "required": true - } + 6 + ] + }, + { + "question": "Comments", + "answer": "" } ] } \ No newline at end of file diff --git a/testing/unit/risk_profile/profiles/risk_profile_expired.json b/testing/unit/risk_profile/profiles/risk_profile_expired.json index 5bcedab07..7ce4d1053 100644 --- a/testing/unit/risk_profile/profiles/risk_profile_expired.json +++ b/testing/unit/risk_profile/profiles/risk_profile_expired.json @@ -1,162 +1,56 @@ { "name": "Primary profile", + "version": "1.3-alpha", + "created": "2023-06-01", "status": "Valid", - "created": "2022-05-23", - "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": "Sensor - Lighting" }, { "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": "sakjdhaskjdh" }, { "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": "N/A" }, { - "category": "Data Collection", "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)?" - ], - "validation": { - "required": true - } + 3 + ] }, { - "category": "Data Transmission", "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" - ], - "validation": { - "required": true - } + ] }, { - "category": "Data Transmission", "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" }, { - "category": "Remote Operation", "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" - ], - "validation": { - "required": true - } + 5 + ] }, { - "category": "Operating Environment", "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." - ], - "validation": { - "required": true - } + 6 + ] + }, + { + "question": "Comments", + "answer": "" } ] } \ No newline at end of file diff --git a/testing/unit/risk_profile/profiles/risk_profile_valid_high.json b/testing/unit/risk_profile/profiles/risk_profile_valid_high.json index 78ed9a5fa..338c91abb 100644 --- a/testing/unit/risk_profile/profiles/risk_profile_valid_high.json +++ b/testing/unit/risk_profile/profiles/risk_profile_valid_high.json @@ -1,162 +1,56 @@ { "name": "Primary profile", + "version": "1.3-alpha", + "created": "2024-07-01", "status": "Valid", - "created": "2024-05-23", - "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": "sakjdhaskjdh" }, { "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": "N/A" }, { - "category": "Data Collection", "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)?" - ], - "validation": { - "required": true - } + 3 + ] }, { - "category": "Data Transmission", "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" - ], - "validation": { - "required": true - } + ] }, { - "category": "Data Transmission", "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" }, { - "category": "Remote Operation", "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" - ], - "validation": { - "required": true - } + 5 + ] }, { - "category": "Operating Environment", "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." - ], - "validation": { - "required": true - } + 6 + ] + }, + { + "question": "Comments", + "answer": "" } ] } \ No newline at end of file diff --git a/testing/unit/risk_profile/profiles/risk_profile_valid_limited.json b/testing/unit/risk_profile/profiles/risk_profile_valid_limited.json index aa6c73297..fba02d4ba 100644 --- a/testing/unit/risk_profile/profiles/risk_profile_valid_limited.json +++ b/testing/unit/risk_profile/profiles/risk_profile_valid_limited.json @@ -1,148 +1,56 @@ { "name": "Primary profile", + "version": "1.3-alpha", + "created": "2024-07-01", "status": "Valid", - "created": "2024-05-23", - "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": "Sensor - Lighting" }, { "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": "sakjdhaskjdh" }, { "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": "N/A" }, { - "category": "Data Collection", "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": [], - "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)?" - ], - "validation": { - "required": true - } + "answer": [ + 3 + ] }, { - "category": "Data Transmission", "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": [], - "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" - ], - "validation": { - "required": true - } + "answer": [ + 5 + ] }, { - "category": "Data Transmission", "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" }, { - "category": "Remote Operation", "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": [], - "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" - ], - "validation": { - "required": true - } + "answer": [ + 5 + ] }, { - "category": "Operating Environment", "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": [], - "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." - ], - "validation": { - "required": true - } + "answer": [ + 6 + ] + }, + { + "question": "Comments", + "answer": "" } ] } \ No newline at end of file diff --git a/testing/unit/risk_profile/risk_profile_test.py b/testing/unit/risk_profile/risk_profile_test.py index c4c80c1e4..5f4ba6973 100644 --- a/testing/unit/risk_profile/risk_profile_test.py +++ b/testing/unit/risk_profile/risk_profile_test.py @@ -35,7 +35,9 @@ class RiskProfileTest(unittest.TestCase): def setUpClass(cls): # Create the output directories and ignore errors if it already exists os.makedirs(OUTPUT_DIR, exist_ok=True) - with open('resources/risk_assessment.json', 'r', encoding='utf-8') as file: + with open(os.path.join('resources', + 'risk_assessment.json'), + 'r', encoding='utf-8') as file: cls.profile_format = json.loads(file.read()) def risk_profile_high_test(self): diff --git a/testing/unit/run.sh b/testing/unit/run.sh index 1ae4d6287..fcb6c532b 100644 --- a/testing/unit/run.sh +++ b/testing/unit/run.sh @@ -14,4 +14,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -docker run --rm -it --name unit-test test-run/unit-test /bin/bash ./run_tests.sh \ No newline at end of file +sudo docker run --rm -it --name unit-test test-run/unit-test /bin/bash ./run_tests.sh \ No newline at end of file diff --git a/testing/unit/run_tests.sh b/testing/unit/run_tests.sh index 975627fc3..3a2fd9cc8 100644 --- a/testing/unit/run_tests.sh +++ b/testing/unit/run_tests.sh @@ -49,7 +49,7 @@ python3 -u $PWD/testing/unit/tls/tls_module_test.py # Run the DNS Module Unit Tests python3 -u $PWD/testing/unit/dns/dns_module_test.py -# Run the NMAP Module Unit Tests +# Run the Services Module Unit Tests python3 -u $PWD/testing/unit/services/services_module_test.py # Run the NTP Module Unit Tests diff --git a/testing/unit/services/reports/nmap_report_all_closed_local.html b/testing/unit/services/reports/services_report_all_closed_local.html similarity index 100% rename from testing/unit/services/reports/nmap_report_all_closed_local.html rename to testing/unit/services/reports/services_report_all_closed_local.html diff --git a/testing/unit/services/reports/nmap_report_local.html b/testing/unit/services/reports/services_report_local.html similarity index 100% rename from testing/unit/services/reports/nmap_report_local.html rename to testing/unit/services/reports/services_report_local.html diff --git a/testing/unit/unit_test.Dockerfile b/testing/unit/unit_test.Dockerfile index 3de2ec974..c8e0c468a 100644 --- a/testing/unit/unit_test.Dockerfile +++ b/testing/unit/unit_test.Dockerfile @@ -9,9 +9,10 @@ ENV DEBIAN_FRONTEND=noninteractive ARG MODULE_DIR=modules/test ARG UNIT_TEST_DIR=testing/unit ARG FRAMEWORK_DIR=framework +ARG RESOURCES_DIR=resources # Install common software -RUN apt-get install -yq net-tools iputils-ping tzdata tcpdump iproute2 jq python3 python3-pip dos2unix nmap wget --fix-missing +RUN apt-get install -yq net-tools iputils-ping tzdata tcpdump iproute2 jq python3 python3-pip dos2unix nmap wget libpangocairo-1.0-0 --fix-missing # Install framework python modules COPY $FRAMEWORK_DIR/ /testrun/$FRAMEWORK_DIR @@ -20,6 +21,9 @@ COPY $FRAMEWORK_DIR/ /testrun/$FRAMEWORK_DIR COPY $MODULE_DIR/ /testrun/$MODULE_DIR COPY $UNIT_TEST_DIR /testrun/$UNIT_TEST_DIR +# Copy resources folder +COPY $RESOURCES_DIR /testrun/resources + # Install required software for TLS module RUN apt-get update && apt-get install -y tshark @@ -29,6 +33,9 @@ RUN pip3 install -r /testrun/framework/requirements.txt # Install all python requirements for the TLS module RUN pip3 install -r /testrun/modules/test/tls/python/requirements.txt +# Install all python requirements for the services module +RUN pip3 install -r /testrun/modules/test/services/python/requirements.txt + # Remove incorrect line endings RUN dos2unix /testrun/modules/test/tls/bin/* RUN dos2unix /testrun/testing/unit/* @@ -38,5 +45,3 @@ RUN chmod u+x /testrun/modules/test/tls/bin/* RUN chmod u+x /testrun/testing/unit/run_tests.sh WORKDIR /testrun/testing/unit/ - -#ENTRYPOINT [ "./run_tests.sh" ] \ No newline at end of file From 5ea49fc2a6f8427cc4f4493a21e14571774d5dba Mon Sep 17 00:00:00 2001 From: Jacob Boddey Date: Tue, 2 Jul 2024 10:30:16 +0100 Subject: [PATCH 07/10] Update unit testing --- testing/unit/build.sh | 2 +- .../unit/risk_profile/risk_profile_test.py | 31 +++++++++++++++++++ testing/unit/run.sh | 2 +- testing/unit/run_tests.sh | 8 ++--- testing/unit/services/services_module_test.py | 30 +++++++++--------- 5 files changed, 53 insertions(+), 20 deletions(-) diff --git a/testing/unit/build.sh b/testing/unit/build.sh index ae2b467e6..db84e0299 100644 --- a/testing/unit/build.sh +++ b/testing/unit/build.sh @@ -14,4 +14,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -sudo docker build -f testing/unit/unit_test.Dockerfile -t test-run/unit-test . \ No newline at end of file +sudo docker build -f testing/unit/unit_test.Dockerfile -t testrun/unit-test . \ No newline at end of file diff --git a/testing/unit/risk_profile/risk_profile_test.py b/testing/unit/risk_profile/risk_profile_test.py index 5f4ba6973..23cce43d3 100644 --- a/testing/unit/risk_profile/risk_profile_test.py +++ b/testing/unit/risk_profile/risk_profile_test.py @@ -128,6 +128,36 @@ def risk_profile_expired_test(self): self.assertEqual(risk_profile.status, 'Expired') + def risk_profile_update_risk_test(self): + # Read the risk profile json file + risk_profile_path = os.path.join(PROFILE_DIR, + 'risk_profile_valid_high.json') + with open(risk_profile_path, 'r', encoding='utf-8') as file: + risk_profile_json = json.loads(file.read()) + + # Load the initial risk profile + risk_profile = RiskProfile().load(risk_profile_json, self.profile_format) + + # Check risk profile risk is high + self.assertEqual(risk_profile.risk, 'High') + + # Load updated risk profile path + risk_profile_path = os.path.join(PROFILE_DIR, + 'risk_profile_valid_limited.json') + + # Read the JSON for the updated profile + with open(risk_profile_path, 'r', encoding='utf-8') as file: + risk_profile_json = json.loads(file.read()) + + # Update the risk profile + risk_profile.update(risk_profile_json, self.profile_format) + + # Refresh the risk + risk_profile.update_risk(self.profile_format) + + # Risk should now be limited after update + self.assertEqual(risk_profile.risk, 'Limited') + if __name__ == '__main__': suite = unittest.TestSuite() @@ -136,6 +166,7 @@ def risk_profile_expired_test(self): suite.addTest(RiskProfileTest('risk_profile_rename_test')) suite.addTest(RiskProfileTest('risk_profile_draft_test')) suite.addTest(RiskProfileTest('risk_profile_expired_test')) + suite.addTest(RiskProfileTest('risk_profile_update_risk_test')) runner = unittest.TextTestRunner() runner.run(suite) diff --git a/testing/unit/run.sh b/testing/unit/run.sh index fcb6c532b..72ca9dcb0 100644 --- a/testing/unit/run.sh +++ b/testing/unit/run.sh @@ -14,4 +14,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -sudo docker run --rm -it --name unit-test test-run/unit-test /bin/bash ./run_tests.sh \ No newline at end of file +sudo docker run --rm -it --name unit-test testrun/unit-test /bin/bash ./run_tests.sh \ No newline at end of file diff --git a/testing/unit/run_tests.sh b/testing/unit/run_tests.sh index 3a2fd9cc8..8969b56f8 100644 --- a/testing/unit/run_tests.sh +++ b/testing/unit/run_tests.sh @@ -17,7 +17,7 @@ # This script should be run from within the unit_test directory. If # it is run outside this directory, paths will not be resolved correctly. -# Move into the root directory of test-run +# Move into the root directory of testrun pushd ../../ >/dev/null 2>&1 echo "Root dir: $PWD" @@ -52,11 +52,11 @@ python3 -u $PWD/testing/unit/dns/dns_module_test.py # Run the Services Module Unit Tests python3 -u $PWD/testing/unit/services/services_module_test.py -# Run the NTP Module Unit Tests -python3 -u $PWD/testing/unit/ntp/ntp_module_test.py +# Run the NTP Module Unit Tests - This is currently broken +# python3 -u $PWD/testing/unit/ntp/ntp_module_test.py # Run the Report Unit Tests -python3 -u $PWD/testing/unit/report/report_test.py + python3 -u $PWD/testing/unit/report/report_test.py # Run the RiskProfile Unit Tests python3 -u $PWD/testing/unit/risk_profile/risk_profile_test.py diff --git a/testing/unit/services/services_module_test.py b/testing/unit/services/services_module_test.py index a24a21d50..30c4928bf 100644 --- a/testing/unit/services/services_module_test.py +++ b/testing/unit/services/services_module_test.py @@ -11,7 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -"""Module run all the DNS related unit tests""" +"""Module run all the services related unit tests""" from services_module import ServicesModule import unittest import os @@ -26,13 +26,13 @@ REPORTS_DIR = os.path.join(TEST_FILES_DIR, 'reports/') RESULTS_DIR = os.path.join(TEST_FILES_DIR, 'results/') -LOCAL_REPORT = os.path.join(REPORTS_DIR, 'nmap_report_local.html') +LOCAL_REPORT = os.path.join(REPORTS_DIR, 'services_report_local.html') LOCAL_REPORT_ALL_CLOSED = os.path.join(REPORTS_DIR, - 'nmap_report_all_closed_local.html') + 'services_report_all_closed_local.html') CONF_FILE = 'modules/test/' + MODULE + '/conf/module_config.json' -class NMAPTest(unittest.TestCase): +class ServicesTest(unittest.TestCase): """Contains and runs all the unit tests concerning DNS behaviors""" @classmethod @@ -41,7 +41,7 @@ def setUpClass(cls): os.makedirs(OUTPUT_DIR, exist_ok=True) # Test the module report generation - def nmap_module_ports_open_report_test(self): + def services_module_ports_open_report_test(self): # Move test scan into expected folder src_scan_results_path = os.path.join(RESULTS_DIR, 'ports_open_scan_result.json') @@ -49,14 +49,14 @@ def nmap_module_ports_open_report_test(self): OUTPUT_DIR, 'services_scan_results.json') shutil.copy(src_scan_results_path, dst_scan_results_path) - nmap_module = ServicesModule(module=MODULE, + services_module = ServicesModule(module=MODULE, log_dir=OUTPUT_DIR, conf_file=CONF_FILE, results_dir=OUTPUT_DIR, run=False, nmap_scan_results_path=OUTPUT_DIR) - report_out_path = nmap_module.generate_module_report() + report_out_path = services_module.generate_module_report() # Read the generated report with open(report_out_path, 'r', encoding='utf-8') as file: @@ -64,7 +64,8 @@ def nmap_module_ports_open_report_test(self): formatted_report = self.add_formatting(report_out) # Write back the new formatted_report value - out_report_path = os.path.join(OUTPUT_DIR, 'nmap_report_ports_open.html') + out_report_path = os.path.join( + OUTPUT_DIR, 'services_report_ports_open.html') with open(out_report_path, 'w', encoding='utf-8') as file: file.write(formatted_report) @@ -75,21 +76,21 @@ def nmap_module_ports_open_report_test(self): self.assertEqual(report_out, report_local) # Test the module report generation with all ports closed - def nmap_module_report_all_closed_test(self): + def services_module_report_all_closed_test(self): src_scan_results_path = os.path.join(RESULTS_DIR, 'all_closed_scan_result.json') dst_scan_results_path = os.path.join( OUTPUT_DIR, 'services_scan_results.json') shutil.copy(src_scan_results_path, dst_scan_results_path) - nmap_module = ServicesModule(module=MODULE, + services_module = ServicesModule(module=MODULE, log_dir=OUTPUT_DIR, conf_file=CONF_FILE, results_dir=OUTPUT_DIR, run=False, nmap_scan_results_path=OUTPUT_DIR) - report_out_path = nmap_module.generate_module_report() + report_out_path = services_module.generate_module_report() # Read the generated report with open(report_out_path, 'r', encoding='utf-8') as file: @@ -97,7 +98,8 @@ def nmap_module_report_all_closed_test(self): formatted_report = self.add_formatting(report_out) # Write back the new formatted_report value - out_report_path = os.path.join(OUTPUT_DIR, 'nmap_report_all_closed.html') + out_report_path = os.path.join( + OUTPUT_DIR, 'services_report_all_closed.html') with open(out_report_path, 'w', encoding='utf-8') as file: file.write(formatted_report) @@ -121,8 +123,8 @@ def add_formatting(self, body): if __name__ == '__main__': suite = unittest.TestSuite() # Module report test - suite.addTest(NMAPTest('nmap_module_ports_open_report_test')) - suite.addTest(NMAPTest('nmap_module_report_all_closed_test')) + suite.addTest(ServicesTest('services_module_ports_open_report_test')) + suite.addTest(ServicesTest('services_module_report_all_closed_test')) runner = unittest.TextTestRunner() runner.run(suite) From 60d81157d87c758b5b29d222226c1f18b67b6b7c Mon Sep 17 00:00:00 2001 From: jhughesbiot Date: Tue, 2 Jul 2024 15:19:38 -0600 Subject: [PATCH 08/10] Invoke risk profile update method in favor of manually updating properties in session --- framework/python/src/common/session.py | 24 +++++------------------- 1 file changed, 5 insertions(+), 19 deletions(-) diff --git a/framework/python/src/common/session.py b/framework/python/src/common/session.py index 4268fbfbb..940237c55 100644 --- a/framework/python/src/common/session.py +++ b/framework/python/src/common/session.py @@ -501,29 +501,15 @@ def update_profile(self, profile_json): else: + # Update the profile + risk_profile.update(profile_json) + # Check if name has changed if 'rename' in profile_json: - new_name = profile_json.get('rename') + old_name = profile_json.get('name') # Delete the original file - os.remove(os.path.join(PROFILES_DIR, risk_profile.name + '.json')) - - risk_profile.name = new_name - - # Update questions and answers - risk_profile.questions = profile_json.get('questions') - - # Update status - if 'status' in profile_json: - risk_profile.status = profile_json['status'] - - if risk_profile.status == 'Valid': - # Update created date - risk_profile.created = datetime.datetime.now() - # Update risk - risk_profile.update_risk(self._profile_format) - else: - risk_profile.risk = None + os.remove(os.path.join(PROFILES_DIR, old_name + '.json')) # Write file to disk with open(os.path.join(PROFILES_DIR, risk_profile.name + '.json'), From 4298817352bf9049dc7d9f34d9e04bd25f32100b Mon Sep 17 00:00:00 2001 From: Jacob Boddey Date: Tue, 2 Jul 2024 23:08:25 +0100 Subject: [PATCH 09/10] Update risk after update and fix type error --- framework/python/src/common/risk_profile.py | 2 ++ framework/python/src/common/session.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/framework/python/src/common/risk_profile.py b/framework/python/src/common/risk_profile.py index cc7109f03..6af529dfa 100644 --- a/framework/python/src/common/risk_profile.py +++ b/framework/python/src/common/risk_profile.py @@ -70,6 +70,8 @@ def update(self, profile_json, profile_format): self.questions = new_profile.questions self.status = new_profile.status + self.update_risk(profile_format=profile_format) + def get_file_path(self): return os.path.join(PROFILES_PATH, self.name + '.json') diff --git a/framework/python/src/common/session.py b/framework/python/src/common/session.py index 940237c55..4e8511a57 100644 --- a/framework/python/src/common/session.py +++ b/framework/python/src/common/session.py @@ -502,7 +502,7 @@ def update_profile(self, profile_json): else: # Update the profile - risk_profile.update(profile_json) + risk_profile.update(profile_json, profile_format=self._profile_format) # Check if name has changed if 'rename' in profile_json: From 03745dd243d4c4759c4ec426e96942e8afe49343 Mon Sep 17 00:00:00 2001 From: Jacob Boddey Date: Tue, 2 Jul 2024 23:13:46 +0100 Subject: [PATCH 10/10] Update risk correctly --- framework/python/src/common/risk_profile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework/python/src/common/risk_profile.py b/framework/python/src/common/risk_profile.py index 6af529dfa..453e09beb 100644 --- a/framework/python/src/common/risk_profile.py +++ b/framework/python/src/common/risk_profile.py @@ -70,7 +70,7 @@ def update(self, profile_json, profile_format): self.questions = new_profile.questions self.status = new_profile.status - self.update_risk(profile_format=profile_format) + self.risk = new_profile.risk def get_file_path(self): return os.path.join(PROFILES_PATH,