From 9bdfe6f38567d5eda0c3abde58bcabdf62504c6c Mon Sep 17 00:00:00 2001 From: Alden Hilton <106177711+adhilto@users.noreply.github.com> Date: Mon, 23 Sep 2024 14:45:41 -0700 Subject: [PATCH 01/20] Update Config.md with section on omitting policies --- docs/usage/Config.md | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/docs/usage/Config.md b/docs/usage/Config.md index 143eba01..6e26b2ad 100644 --- a/docs/usage/Config.md +++ b/docs/usage/Config.md @@ -21,6 +21,19 @@ It can also be invoked while overriding the `baselines` parameter. scubagoggles gws --config basic_config.yaml -b gmail chat ``` +## Omit Policies + +In some cases, it may be appropriate to omit specific policies from ScubaGoggles evaluation. For example: +- When a policy is implemented by a third-party service that ScubaGoggles does not audit +- When a policy is not applicable to your organization (e.g., policy GWS.GMAIL.4.3v0.3 is only applicable to federal, executive branch, departments and agencies) + +The `OmitPolicy` top-level key, shown in this [example ScubaGoggles configuration file](https://github.com/cisagov/ScubaGear/blob/main/PowerShell/ScubaGear/Sample-Config-Files/omit_policies.yaml), allows the user to specify the policies that should be omitted from the ScubaGear report. Omitted policies will show up as "Omitted" in the HTML report and will be colored gray. Omitting policies must only be done if the omissions are approved within an organization's security risk management process. **Exercise care when omitting policies because this can inadvertently introduce blind spots when assessing your system.** + +For each omitted policy, the config file allows you to indicate the following: +- `Rationale`: The reason the policy should be omitted from the report. This value will be displayed in the "Details" column of the report. ScubaGear will output a warning if no rationale is provided. +- `Expiration`: Optional. A date after which the policy should no longer be omitted from the report. The expected format is yyyy-mm-dd. + + ## Navigation - Continue to [Usage: Examples](/docs/usage/Examples.md) -- Return to [Documentation Home](/README.md) \ No newline at end of file +- Return to [Documentation Home](/README.md) From 2d62a7e05784a1096efe9e67862f63c3ec2ff2ac Mon Sep 17 00:00:00 2001 From: Alden Hilton <106177711+adhilto@users.noreply.github.com> Date: Mon, 23 Sep 2024 14:53:44 -0700 Subject: [PATCH 02/20] Create omit_policies.yaml --- sample-config-files/omit_policies.yaml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 sample-config-files/omit_policies.yaml diff --git a/sample-config-files/omit_policies.yaml b/sample-config-files/omit_policies.yaml new file mode 100644 index 00000000..108ad202 --- /dev/null +++ b/sample-config-files/omit_policies.yaml @@ -0,0 +1,16 @@ +# YAML configuration demonstrating omitting policies from ScubaGoggles evaluation. +# Any omitted policies should be carefully considered and documented as part of an +# organization's cybersecurity risk management program process and practices. + +baselines: [gmail, chat, drive] + +omitpolicy: + GWS.GMAIL.3.1v0.3: + rationale: "Known false positive; our SPF policy currently cannot to be retrieved via ScubaGoggles due to a split + horizon setup but is available publicly." + expiration: "2023-12-31" + GWS.CHAT.5.1v0.3: + rationale: &DLPRationale "The DLP capability required by the baselines is implemented by third party product, [x], + which ScubaGoggles does not have the ability to check." + GWS.DRIVEDOCS.7.1v0.3: + rationale: *DLPRationale From eee121aff7fef94307eb3039cc8c0504480e1388 Mon Sep 17 00:00:00 2001 From: Alden Hilton <106177711+adhilto@users.noreply.github.com> Date: Mon, 23 Sep 2024 14:55:18 -0700 Subject: [PATCH 03/20] Update Config.md with example config specific to scubagoggles --- docs/usage/Config.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/usage/Config.md b/docs/usage/Config.md index 6e26b2ad..357aaf97 100644 --- a/docs/usage/Config.md +++ b/docs/usage/Config.md @@ -27,11 +27,11 @@ In some cases, it may be appropriate to omit specific policies from ScubaGoggles - When a policy is implemented by a third-party service that ScubaGoggles does not audit - When a policy is not applicable to your organization (e.g., policy GWS.GMAIL.4.3v0.3 is only applicable to federal, executive branch, departments and agencies) -The `OmitPolicy` top-level key, shown in this [example ScubaGoggles configuration file](https://github.com/cisagov/ScubaGear/blob/main/PowerShell/ScubaGear/Sample-Config-Files/omit_policies.yaml), allows the user to specify the policies that should be omitted from the ScubaGear report. Omitted policies will show up as "Omitted" in the HTML report and will be colored gray. Omitting policies must only be done if the omissions are approved within an organization's security risk management process. **Exercise care when omitting policies because this can inadvertently introduce blind spots when assessing your system.** +The `omitpolicy` top-level key, shown in this [example ScubaGoggles configuration file](/Sample-Config-Files/omit_policies.yaml), allows the user to specify the policies that should be omitted from the ScubaGear report. Omitted policies will show up as "Omitted" in the HTML report and will be colored gray. Omitting policies must only be done if the omissions are approved within an organization's security risk management process. **Exercise care when omitting policies because this can inadvertently introduce blind spots when assessing your system.** For each omitted policy, the config file allows you to indicate the following: -- `Rationale`: The reason the policy should be omitted from the report. This value will be displayed in the "Details" column of the report. ScubaGear will output a warning if no rationale is provided. -- `Expiration`: Optional. A date after which the policy should no longer be omitted from the report. The expected format is yyyy-mm-dd. +- `rationale`: The reason the policy should be omitted from the report. This value will be displayed in the "Details" column of the report. ScubaGear will output a warning if no rationale is provided. +- `expiration`: Optional. A date after which the policy should no longer be omitted from the report. The expected format is yyyy-mm-dd. ## Navigation From 63f3cc4b80dc3d63a6a75be6292f54c96dee6e99 Mon Sep 17 00:00:00 2001 From: Alden Hilton <106177711+adhilto@users.noreply.github.com> Date: Mon, 23 Sep 2024 14:59:21 -0700 Subject: [PATCH 04/20] Update Config.md minor grammatical changes --- docs/usage/Config.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/usage/Config.md b/docs/usage/Config.md index 357aaf97..6889b575 100644 --- a/docs/usage/Config.md +++ b/docs/usage/Config.md @@ -24,13 +24,13 @@ scubagoggles gws --config basic_config.yaml -b gmail chat ## Omit Policies In some cases, it may be appropriate to omit specific policies from ScubaGoggles evaluation. For example: -- When a policy is implemented by a third-party service that ScubaGoggles does not audit -- When a policy is not applicable to your organization (e.g., policy GWS.GMAIL.4.3v0.3 is only applicable to federal, executive branch, departments and agencies) +- When a policy is implemented by a third-party service that ScubaGoggles does not audit. +- When a policy is not applicable to your organization (e.g., policy GWS.GMAIL.4.3v0.3, which is only applicable to federal, executive branch, departments and agencies). -The `omitpolicy` top-level key, shown in this [example ScubaGoggles configuration file](/Sample-Config-Files/omit_policies.yaml), allows the user to specify the policies that should be omitted from the ScubaGear report. Omitted policies will show up as "Omitted" in the HTML report and will be colored gray. Omitting policies must only be done if the omissions are approved within an organization's security risk management process. **Exercise care when omitting policies because this can inadvertently introduce blind spots when assessing your system.** +The `omitpolicy` top-level key, shown in this [example ScubaGoggles configuration file](/Sample-Config-Files/omit_policies.yaml), allows the user to specify the policies that should be omitted from the ScubaGoggles report. Omitted policies will show up as "Omitted" in the HTML report and will be colored gray. Omitting policies must only be done if the omissions are approved within an organization's security risk management process. **Exercise care when omitting policies because this can inadvertently introduce blind spots when assessing your system.** For each omitted policy, the config file allows you to indicate the following: -- `rationale`: The reason the policy should be omitted from the report. This value will be displayed in the "Details" column of the report. ScubaGear will output a warning if no rationale is provided. +- `rationale`: The reason the policy should be omitted from the report. This value will be displayed in the "Details" column of the report. ScubaGoggles will output a warning if no rationale is provided. - `expiration`: Optional. A date after which the policy should no longer be omitted from the report. The expected format is yyyy-mm-dd. From 272fb4b182d122d37455f18137b4f20bce36f655 Mon Sep 17 00:00:00 2001 From: Alden Hilton <106177711+adhilto@users.noreply.github.com> Date: Mon, 23 Sep 2024 15:01:07 -0700 Subject: [PATCH 05/20] Update Config.md correct link --- docs/usage/Config.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/usage/Config.md b/docs/usage/Config.md index 6889b575..ff6a9b40 100644 --- a/docs/usage/Config.md +++ b/docs/usage/Config.md @@ -27,7 +27,7 @@ In some cases, it may be appropriate to omit specific policies from ScubaGoggles - When a policy is implemented by a third-party service that ScubaGoggles does not audit. - When a policy is not applicable to your organization (e.g., policy GWS.GMAIL.4.3v0.3, which is only applicable to federal, executive branch, departments and agencies). -The `omitpolicy` top-level key, shown in this [example ScubaGoggles configuration file](/Sample-Config-Files/omit_policies.yaml), allows the user to specify the policies that should be omitted from the ScubaGoggles report. Omitted policies will show up as "Omitted" in the HTML report and will be colored gray. Omitting policies must only be done if the omissions are approved within an organization's security risk management process. **Exercise care when omitting policies because this can inadvertently introduce blind spots when assessing your system.** +The `omitpolicy` top-level key, shown in this [example ScubaGoggles configuration file](/sample-config-files/omit_policies.yaml), allows the user to specify the policies that should be omitted from the ScubaGoggles report. Omitted policies will show up as "Omitted" in the HTML report and will be colored gray. Omitting policies must only be done if the omissions are approved within an organization's security risk management process. **Exercise care when omitting policies because this can inadvertently introduce blind spots when assessing your system.** For each omitted policy, the config file allows you to indicate the following: - `rationale`: The reason the policy should be omitted from the report. This value will be displayed in the "Details" column of the report. ScubaGoggles will output a warning if no rationale is provided. From 0a9d2ffa0fefd8ce53bc7f70e7e3799a0e226a5a Mon Sep 17 00:00:00 2001 From: Alden Hilton <106177711+adhilto@users.noreply.github.com> Date: Mon, 23 Sep 2024 15:02:23 -0700 Subject: [PATCH 06/20] Update Config.md correct minor typo --- docs/usage/Config.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/usage/Config.md b/docs/usage/Config.md index ff6a9b40..a75189e1 100644 --- a/docs/usage/Config.md +++ b/docs/usage/Config.md @@ -11,7 +11,7 @@ All ScubaGoggles [parameters](/docs/usage/Parameters.md) can be placed into a co ### Basic Usage The [basic use](/sample-config-files/basic_config.yaml) example config file specifies the `outpath`, `baselines`, and `quiet` parameters. -ScubaGoggles can be invokes with this config file: +ScubaGoggles can be invoked with this config file: ``` scubagoggles gws --config basic_config.yaml ``` From c8043489b7ed222f654bf72267c4e69a1374f54c Mon Sep 17 00:00:00 2001 From: Alden Hilton Date: Mon, 23 Sep 2024 17:03:22 -0700 Subject: [PATCH 07/20] Add config file support for omitting policies --- scubagoggles/orchestrator.py | 25 ++++- scubagoggles/reporter/reporter.py | 102 +++++++++++++++++- scubagoggles/reporter/scripts/main.js | 3 + .../reporter/styles/FrontPageStyle.css | 2 +- 4 files changed, 122 insertions(+), 10 deletions(-) diff --git a/scubagoggles/orchestrator.py b/scubagoggles/orchestrator.py index 9c0ae31c..9a50947f 100644 --- a/scubagoggles/orchestrator.py +++ b/scubagoggles/orchestrator.py @@ -155,9 +155,10 @@ def _generate_summary(cls, stats: dict) -> str: n_fail = stats["Failures"] n_manual = stats["Manual"] n_error = stats["Errors"] + n_omit = stats['Omit'] pass_summary = (f"
{n_success}" - f" {cls._pluralize('test', 'tests', n_success)} passed
") + f" {cls._pluralize('pass', 'passes', n_success)}") # The warnings, failures, and manuals are only shown if they are # greater than zero. Reserve the space for them here. They will @@ -166,21 +167,29 @@ def _generate_summary(cls, stats: dict) -> str: failure_summary = "
" manual_summary = "
" error_summary = "
" + omit_summary = "
" if n_warn > 0: warning_summary = (f"
{n_warn}" f" {cls._pluralize('warning', 'warnings', n_warn)}
") if n_fail > 0: failure_summary = (f"
{n_fail}" - f" {cls._pluralize('test', 'tests', n_fail)} failed
") + f" {cls._pluralize('failure', 'failures', n_fail)}") if n_manual > 0: manual_summary = (f"
{n_manual} manual" - f" {cls._pluralize('check', 'checks', n_manual)} needed
") + f" {cls._pluralize('check', 'checks', n_manual)}") if n_error > 0: error_summary = (f"
{n_error}" f" {cls._pluralize('error', 'errors', n_error)}
") + if n_omit > 0: + omit_summary = (f"
{n_omit}" + " omitted
") - return f"{pass_summary}{warning_summary}{failure_summary}{manual_summary}{error_summary}" + # summary = f"{pass_summary}{warning_summary}{failure_summary}" + # summary += f"{manual_summary}{omit_summary}{error_summary}" + # return summary + return f"{pass_summary}{warning_summary}{failure_summary}" \ + f"{manual_summary}{omit_summary}{error_summary}" def _run_reporter(self): """ @@ -245,6 +254,11 @@ def _run_reporter(self): tenant_info = json.load(file)['tenant_info'] tenant_domain = tenant_info['domain'] + # Determine if any controls were omitted in the config file + omissions = {} + if 'omitpolicy' in args and args.omitpolicy is not None: + omissions = args.omitpolicy + # Create the individual report files out_jsonfile = args.outjsonfilename summary = {} @@ -284,7 +298,8 @@ def _run_reporter(self): prod_to_fullname, baseline_policies[product], successful_calls, - unsuccessful_calls) + unsuccessful_calls, + omissions) stats_and_data[product] = \ reporter.rego_json_to_ind_reports(test_results_data, out_folder) diff --git a/scubagoggles/reporter/reporter.py b/scubagoggles/reporter/reporter.py index 63a775b6..a3bf9bda 100644 --- a/scubagoggles/reporter/reporter.py +++ b/scubagoggles/reporter/reporter.py @@ -27,7 +27,8 @@ def __init__(self, prod_to_fullname: dict, product_policies: list, successful_calls: set, - unsuccessful_calls: set): + unsuccessful_calls: set, + omissions: dict): """Reporter class initialization @@ -39,6 +40,8 @@ def __init__(self, read from the baseline markdown :param successful_calls: set with the set of successful calls :param unsuccessful_calls: set with the set of unsuccessful calls + :param omissions: dict with the omissions specified in the config + file (empty dict if none omitted) """ self._product = product @@ -48,6 +51,10 @@ def __init__(self, self._successful_calls = successful_calls self._unsuccessful_calls = unsuccessful_calls self._full_name = prod_to_fullname[product] + self._omissions = { + # Lowercase all the keys for case-insensitive comparisons + key.lower(): value for key, value in omissions.items() + } @staticmethod def _get_test_result(requirement_met: bool, criticality: str, no_such_events: bool) -> str: @@ -132,6 +139,83 @@ def build_front_page_html(fragments: list, tenant_info: dict) -> str: html = html.replace('{{TENANT_DETAILS}}', meta_data) return html + def _is_control_omitted(self, control_id : str) -> bool: + ''' + Determine if the supplied control was marked for omission in the + config file and if the expiration date has passed, if applicable. + :param control_id: the control ID, e.g., GWS.GMAIL.1.1v1. Case- + insensitive. + ''' + # Lowercase for case-insensitive comparison + control_id = control_id.lower() + if control_id in self._omissions: + # The config indicates the control should be omitted + if self._omissions[control_id] is None: + # If a user doesn't provide either a rationale or expiration + # date, the control ID will be in the omissions dict but it + # will map to None. + return True + if 'expiration' not in self._omissions[control_id]: + return True + # An expiration date for the omission expiration was + # provided. Evaluate the date to see if the control should + # still be omitted. + raw_date = self._omissions[control_id]['expiration'] + if raw_date is None: + # Date left blank, omit the policy + return True + if raw_date == "": + # If the expiration date is an empty string, omit the + # policy + return True + try: + expiration_date = datetime.strptime(raw_date, '%Y-%m-%d') + except: + # Malformed date, don't omit the policy + warning = f"Config file indicates omitting {control_id}, " \ + f"but the provided expiration date, {raw_date}, is " \ + "malformed. The expected format is yyyy-mm-dd. Control" \ + " will not be omitted." + warnings.warn(warning, RuntimeWarning) + return False + now = datetime.now() + if expiration_date > now: + # The expiration date is in the future, omit the policy + return True + # The expiration date is passed, don't omit the policy + warning = f"Config file indicates omitting {control_id}, but " \ + f"the provided expiration date, {raw_date}, has passed. " \ + "Control will not be omitted." + warnings.warn(warning, RuntimeWarning) + return False + + def _get_omission_rationale(self, control_id : str) -> str: + ''' + Return the rationale indicated in the config file for the indicated + control, if provided. If not, return a string warning the user that + no rationale was provided. + :param control_id: the control ID, e.g., GWS.GMAIL.1.1v1. Case- + insensitive. + ''' + # Lowercase for case-insensitive comparison + control_id = control_id.lower() + if control_id not in self._omissions: + throw(f"{control_id} not omitted in config file, cannot fetch " \ + "rationale", RuntimeError) + # If any of the following conditions is true, no rationale was + # provided + no_rationale = \ + (self._omissions[control_id] is None) or \ + ('rationale' not in self._omissions[control_id]) or \ + (self._omissions[control_id]['rationale'] is None) or \ + (self._omissions[control_id]['rationale'] == "") + if no_rationale: + warning = f"Config file indicates omitting {control_id}, but " \ + "no rationale provided." + warnings.warn(warning, RuntimeWarning) + return "Rationale not provided." + return self._omissions[control_id]['rationale'] + def _build_report_html(self, fragments: list) -> str: ''' Adds data into HTML Template and formats the page accordingly @@ -275,15 +359,25 @@ def rego_json_to_ind_reports(self, test_results: list, out_path: str) -> list: "Errors": 0, "Failures": 0, "Warnings": 0, - # While ScubaGoggles doesn't currently have the capability to omit controls, - # this key is needed here to maintain parity with ScubaGear. "Omit": 0 } for baseline_group in self._product_policies: table_data = [] - results_data ={} + results_data = {} for control in baseline_group['Controls']: + if self._is_control_omitted(control['Id']): + # Handle the case where the control was omitted + report_stats['Omit'] += 1 + rationale = self._get_omission_rationale(control['Id']) + table_data.append({ + 'Control ID': control['Id'], + 'Requirement': control['Value'], + 'Result': "Omitted", + 'Criticality': test['Criticality'], + 'Details': f'Test omitted by user. {rationale}' + }) + continue tests = [test for test in test_results if test['PolicyId'] == control['Id']] if len(tests) == 0: # Handle the case where Rego doesn't output anything for a given control diff --git a/scubagoggles/reporter/scripts/main.js b/scubagoggles/reporter/scripts/main.js index b48881f8..2706e99c 100644 --- a/scubagoggles/reporter/scripts/main.js +++ b/scubagoggles/reporter/scripts/main.js @@ -24,6 +24,9 @@ const colorRows = () => { else if (rows[i].children[statusCol].innerHTML === "Pass") { rows[i].style.background = "var(--test-pass)"; } + else if (rows[i].children[statusCol].innerHTML === "Omitted") { + rows[i].style.background = "var(--test-other)"; + } else if (rows[i].children[criticalityCol].innerHTML.includes("Not-Implemented")) { rows[i].style.background = "var(--test-other)"; } diff --git a/scubagoggles/reporter/styles/FrontPageStyle.css b/scubagoggles/reporter/styles/FrontPageStyle.css index eea6f88b..c1f2daf4 100644 --- a/scubagoggles/reporter/styles/FrontPageStyle.css +++ b/scubagoggles/reporter/styles/FrontPageStyle.css @@ -14,7 +14,7 @@ footer { display: inline-block; padding: 5px; border-radius: 5px; - min-width: 140px; + min-width: 100px; text-align: center; } From 3a6532386b86756807493a2d4e68bb3cd652b7b8 Mon Sep 17 00:00:00 2001 From: Alden Hilton Date: Mon, 23 Sep 2024 17:26:58 -0700 Subject: [PATCH 08/20] Correct some linter errors --- scubagoggles/reporter/reporter.py | 37 ++++++++++++++----------------- 1 file changed, 17 insertions(+), 20 deletions(-) diff --git a/scubagoggles/reporter/reporter.py b/scubagoggles/reporter/reporter.py index a3bf9bda..b3b250b9 100644 --- a/scubagoggles/reporter/reporter.py +++ b/scubagoggles/reporter/reporter.py @@ -161,12 +161,9 @@ def _is_control_omitted(self, control_id : str) -> bool: # provided. Evaluate the date to see if the control should # still be omitted. raw_date = self._omissions[control_id]['expiration'] - if raw_date is None: - # Date left blank, omit the policy - return True - if raw_date == "": - # If the expiration date is an empty string, omit the - # policy + if raw_date is None or raw_date == "": + # If the expiration date is left blank or an empty string, + # omit the policy return True try: expiration_date = datetime.strptime(raw_date, '%Y-%m-%d') @@ -200,8 +197,8 @@ def _get_omission_rationale(self, control_id : str) -> str: # Lowercase for case-insensitive comparison control_id = control_id.lower() if control_id not in self._omissions: - throw(f"{control_id} not omitted in config file, cannot fetch " \ - "rationale", RuntimeError) + raise RuntimeError(f"{control_id} not omitted in config file, " \ + "cannot fetch rationale") # If any of the following conditions is true, no rationale was # provided no_rationale = \ @@ -366,18 +363,6 @@ def rego_json_to_ind_reports(self, test_results: list, out_path: str) -> list: table_data = [] results_data = {} for control in baseline_group['Controls']: - if self._is_control_omitted(control['Id']): - # Handle the case where the control was omitted - report_stats['Omit'] += 1 - rationale = self._get_omission_rationale(control['Id']) - table_data.append({ - 'Control ID': control['Id'], - 'Requirement': control['Value'], - 'Result': "Omitted", - 'Criticality': test['Criticality'], - 'Details': f'Test omitted by user. {rationale}' - }) - continue tests = [test for test in test_results if test['PolicyId'] == control['Id']] if len(tests) == 0: # Handle the case where Rego doesn't output anything for a given control @@ -391,6 +376,18 @@ def rego_json_to_ind_reports(self, test_results: list, out_path: str) -> list: 'Details': f'Report issue on {issues_link}'}) warnings.warn(f"No test results found for Control Id {control['Id']}", RuntimeWarning) + elif self._is_control_omitted(control['Id']): + # Handle the case where the control was omitted + report_stats['Omit'] += 1 + rationale = self._get_omission_rationale(control['Id']) + table_data.append({ + 'Control ID': control['Id'], + 'Requirement': control['Value'], + 'Result': "Omitted", + 'Criticality': tests[0]['Criticality'], + 'Details': f'Test omitted by user. {rationale}' + }) + continue else: for test in tests: failed_prereqs = self._get_failed_prereqs(test) From 92deea72b9bfa1492fc8680e2188f6dca04d4e62 Mon Sep 17 00:00:00 2001 From: Alden Hilton Date: Mon, 23 Sep 2024 17:35:26 -0700 Subject: [PATCH 09/20] Correct more linter errors --- scubagoggles/reporter/reporter.py | 114 +++++++++++++++--------------- 1 file changed, 58 insertions(+), 56 deletions(-) diff --git a/scubagoggles/reporter/reporter.py b/scubagoggles/reporter/reporter.py index b3b250b9..9e3175d1 100644 --- a/scubagoggles/reporter/reporter.py +++ b/scubagoggles/reporter/reporter.py @@ -44,6 +44,8 @@ def __init__(self, file (empty dict if none omitted) """ + # pylint: disable=too-many-instance-attributes + # Eight is reasonable in this case. self._product = product self._tenant_domain = tenant_domain self._main_report_name = main_report_name @@ -167,7 +169,7 @@ def _is_control_omitted(self, control_id : str) -> bool: return True try: expiration_date = datetime.strptime(raw_date, '%Y-%m-%d') - except: + except ValueError: # Malformed date, don't omit the policy warning = f"Config file indicates omitting {control_id}, " \ f"but the provided expiration date, {raw_date}, is " \ @@ -376,7 +378,8 @@ def rego_json_to_ind_reports(self, test_results: list, out_path: str) -> list: 'Details': f'Report issue on {issues_link}'}) warnings.warn(f"No test results found for Control Id {control['Id']}", RuntimeWarning) - elif self._is_control_omitted(control['Id']): + continue + if self._is_control_omitted(control['Id']): # Handle the case where the control was omitted report_stats['Omit'] += 1 rationale = self._get_omission_rationale(control['Id']) @@ -388,64 +391,63 @@ def rego_json_to_ind_reports(self, test_results: list, out_path: str) -> list: 'Details': f'Test omitted by user. {rationale}' }) continue - else: - for test in tests: - failed_prereqs = self._get_failed_prereqs(test) - if len(failed_prereqs) > 0: - report_stats["Errors"] += 1 - failed_details = self._get_failed_details(failed_prereqs) + for test in tests: + failed_prereqs = self._get_failed_prereqs(test) + if len(failed_prereqs) > 0: + report_stats["Errors"] += 1 + failed_details = self._get_failed_details(failed_prereqs) + table_data.append({ + 'Control ID': control['Id'], + 'Requirement': control['Value'], + 'Result': "Error", + 'Criticality': test['Criticality'], + 'Details': failed_details}) + else: + result = self._get_test_result(test['RequirementMet'], + test['Criticality'], + test['NoSuchEvent']) + + details = test['ReportDetails'] + + if result == "No events found": + warning_icon = "\ + " + details = warning_icon + " " + test['ReportDetails'] + # As rules doesn't have its own baseline, Rules and Common Controls + # need to be handled specially + if product_capitalized == "Rules": + if 'Not-Implemented' in test['Criticality']: + # The easiest way to identify the GWS.COMMONCONTROLS.13.1v1 + # results that belong to the Common Controls report is they're + # marked as Not-Implemented. This if excludes them from the + # rules report. + continue + report_stats[self._get_summary_category(result)] += 1 table_data.append({ 'Control ID': control['Id'], - 'Requirement': control['Value'], - 'Result': "Error", + 'Rule Name': test['Requirement'], + 'Result': result, 'Criticality': test['Criticality'], - 'Details': failed_details}) + 'Rule Description': test['ReportDetails']}) + elif product_capitalized == "Commoncontrols" \ + and baseline_group['GroupName'] == 'System-defined Rules' \ + and 'Not-Implemented' not in test['Criticality']: + # The easiest way to identify the System-defined Rules + # results that belong to the Common Controls report is they're + # marked as Not-Implemented. This if excludes the full results + # from the Common Controls report. + continue else: - result = self._get_test_result(test['RequirementMet'], - test['Criticality'], - test['NoSuchEvent']) - - details = test['ReportDetails'] - - if result == "No events found": - warning_icon = "\ - " - details = warning_icon + " " + test['ReportDetails'] - # As rules doesn't have its own baseline, Rules and Common Controls - # need to be handled specially - if product_capitalized == "Rules": - if 'Not-Implemented' in test['Criticality']: - # The easiest way to identify the GWS.COMMONCONTROLS.13.1v1 - # results that belong to the Common Controls report is they're - # marked as Not-Implemented. This if excludes them from the - # rules report. - continue - report_stats[self._get_summary_category(result)] += 1 - table_data.append({ - 'Control ID': control['Id'], - 'Rule Name': test['Requirement'], - 'Result': result, - 'Criticality': test['Criticality'], - 'Rule Description': test['ReportDetails']}) - elif product_capitalized == "Commoncontrols" \ - and baseline_group['GroupName'] == 'System-defined Rules' \ - and 'Not-Implemented' not in test['Criticality']: - # The easiest way to identify the System-defined Rules - # results that belong to the Common Controls report is they're - # marked as Not-Implemented. This if excludes the full results - # from the Common Controls report. - continue - else: - report_stats[self._get_summary_category(result)] += 1 - table_data.append({ - 'Control ID': control['Id'], - 'Requirement': control['Value'], - 'Result': result, - 'Criticality': test['Criticality'], - 'Details': details}) + report_stats[self._get_summary_category(result)] += 1 + table_data.append({ + 'Control ID': control['Id'], + 'Requirement': control['Value'], + 'Result': result, + 'Criticality': test['Criticality'], + 'Details': details}) markdown_group_name = "-".join(baseline_group['GroupName'].split()) md_basename = "commoncontrols" if self._product == "rules" else self._product group_reference_url = f'{github_url}/blob/v{tool_version}/baselines/'\ From 6fcd51c80291bebbe2a207192159e71c0749458f Mon Sep 17 00:00:00 2001 From: Alden Hilton Date: Mon, 23 Sep 2024 17:39:15 -0700 Subject: [PATCH 10/20] Move pylint disable comment --- scubagoggles/reporter/reporter.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scubagoggles/reporter/reporter.py b/scubagoggles/reporter/reporter.py index 9e3175d1..4fbe51ed 100644 --- a/scubagoggles/reporter/reporter.py +++ b/scubagoggles/reporter/reporter.py @@ -12,6 +12,8 @@ from scubagoggles.scuba_constants import API_LINKS +# Eight instance attributes is reasonable in this case. +# pylint: disable-next=too-many-instance-attributes class Reporter: """The Reporter class generates the HTML files containing the conformance @@ -44,8 +46,6 @@ def __init__(self, file (empty dict if none omitted) """ - # pylint: disable=too-many-instance-attributes - # Eight is reasonable in this case. self._product = product self._tenant_domain = tenant_domain self._main_report_name = main_report_name From c4ec618048f7634a5b97971d0bbd522aa30c2d30 Mon Sep 17 00:00:00 2001 From: Alden Hilton <106177711+adhilto@users.noreply.github.com> Date: Thu, 26 Sep 2024 08:13:26 -0700 Subject: [PATCH 11/20] Update Config.md, clarify usage of single config file --- docs/usage/Config.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/usage/Config.md b/docs/usage/Config.md index a75189e1..44b87fa1 100644 --- a/docs/usage/Config.md +++ b/docs/usage/Config.md @@ -6,7 +6,7 @@ All ScubaGoggles [parameters](/docs/usage/Parameters.md) can be placed into a co > If a parameter is specified both on the command-line and in a configuration file, the command-line parameter has precedence over the config file. ## Sample Configuration Files -[Sample config files](/sample-config-files) are available in the repo and are discussed below. +[Sample config files](/sample-config-files) are available in the repo and are discussed below. When executing ScubaGoggles, only a single config file can be read in; we recommend looking through the following examples and constructing a config file that best suits your use case. ### Basic Usage The [basic use](/sample-config-files/basic_config.yaml) example config file specifies the `outpath`, `baselines`, and `quiet` parameters. @@ -21,7 +21,7 @@ It can also be invoked while overriding the `baselines` parameter. scubagoggles gws --config basic_config.yaml -b gmail chat ``` -## Omit Policies +### Omit Policies In some cases, it may be appropriate to omit specific policies from ScubaGoggles evaluation. For example: - When a policy is implemented by a third-party service that ScubaGoggles does not audit. From 23c7e1292a769d9ddb7cb8873d12d34112c7c783 Mon Sep 17 00:00:00 2001 From: Alden Hilton <106177711+adhilto@users.noreply.github.com> Date: Thu, 26 Sep 2024 08:27:47 -0700 Subject: [PATCH 12/20] Update omit_policies.yaml, comment about usage of anchors --- sample-config-files/omit_policies.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/sample-config-files/omit_policies.yaml b/sample-config-files/omit_policies.yaml index 108ad202..8004fb70 100644 --- a/sample-config-files/omit_policies.yaml +++ b/sample-config-files/omit_policies.yaml @@ -14,3 +14,9 @@ omitpolicy: which ScubaGoggles does not have the ability to check." GWS.DRIVEDOCS.7.1v0.3: rationale: *DLPRationale + +# The "&" character used in the above example defines an anchor, which saves a value +# for future reference. This value can then be retrieved with the "*" character. See +# https://yaml.org/spec/1.2.2/#692-node-anchors for more details. In this case, the +# anchor allows you to configure multiple omissions that share the same rationale +# without repeating yourself. From 9c9fccdfdd6e46dfc8929748e8f62e7e01976067 Mon Sep 17 00:00:00 2001 From: Alden Hilton Date: Mon, 30 Sep 2024 17:07:28 -0700 Subject: [PATCH 13/20] Warn for unexpected control ids --- scubagoggles/scuba_argument_parser.py | 40 ++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/scubagoggles/scuba_argument_parser.py b/scubagoggles/scuba_argument_parser.py index 2bf89a51..3efcfca3 100644 --- a/scubagoggles/scuba_argument_parser.py +++ b/scubagoggles/scuba_argument_parser.py @@ -2,9 +2,15 @@ Class for parsing the config file and command-line arguments. """ +import re +import warnings import argparse import yaml +from scubagoggles.reporter.md_parser import read_baseline_docs +from scubagoggles.orchestrator import Orchestrator +from pathlib import Path + class ScubaArgumentParser: """ Class for parsing the config file and command-line arguments. @@ -58,7 +64,11 @@ def parse_args_with_config(self) -> argparse.Namespace: if param in cli_args: continue vars(args)[param] = config[param] - # Return the args (argparse.Namespace) as a dictionary + + # Check for logical errors in the resulting configuration + self._validate_config(args) + + # Return the args (argparse.Namespace) return args @classmethod @@ -84,3 +94,31 @@ def _get_explicit_cli_args(cls, args : argparse.Namespace) -> dict: aux_parser.add_argument(*dests) cli_args, _ = aux_parser.parse_known_args() return cli_args + + @classmethod + def _validate_config(cls, args : argparse.Namespace) -> None: + if 'omitpolicy' in args: + products = Orchestrator.gws_products()['prod_to_fullname'] + prod_to_fullname = { + key: products[key] + for key in args.baselines + if key in products + } + + # Parse the baselines to determine the set of valid control IDs + path = Path(args.documentpath).resolve() + baseline_policies = read_baseline_docs(path, prod_to_fullname) + control_ids = set() + for product in baseline_policies: + for group in baseline_policies[product]: + for control in group['Controls']: + control_ids.add(control['Id'].lower()) + + # Warn for any unexpected IDs + for control_id in args.omitpolicy: + if control_id.lower() not in control_ids: + warnings.warn("Config file indicates omitting " \ + f"{control_id}, but {control_id} is not one of the " \ + "controls encompassed by the baselines indicated " \ + "indicated by the productnames parameter. Control " \ + "will not be omitted.") \ No newline at end of file From 979ba451fcde379cfbd5d9a75c3bb1c43c4cf489 Mon Sep 17 00:00:00 2001 From: Alden Hilton Date: Mon, 30 Sep 2024 17:14:15 -0700 Subject: [PATCH 14/20] must appease the linter --- scubagoggles/scuba_argument_parser.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/scubagoggles/scuba_argument_parser.py b/scubagoggles/scuba_argument_parser.py index 3efcfca3..d9dbf3a6 100644 --- a/scubagoggles/scuba_argument_parser.py +++ b/scubagoggles/scuba_argument_parser.py @@ -2,14 +2,13 @@ Class for parsing the config file and command-line arguments. """ -import re import warnings import argparse -import yaml +from pathlib import Path -from scubagoggles.reporter.md_parser import read_baseline_docs +import yaml from scubagoggles.orchestrator import Orchestrator -from pathlib import Path +from scubagoggles.reporter.md_parser import read_baseline_docs class ScubaArgumentParser: """ @@ -109,8 +108,8 @@ def _validate_config(cls, args : argparse.Namespace) -> None: path = Path(args.documentpath).resolve() baseline_policies = read_baseline_docs(path, prod_to_fullname) control_ids = set() - for product in baseline_policies: - for group in baseline_policies[product]: + for product_baseline in baseline_policies.values(): + for group in product_baseline: for control in group['Controls']: control_ids.add(control['Id'].lower()) @@ -121,4 +120,4 @@ def _validate_config(cls, args : argparse.Namespace) -> None: f"{control_id}, but {control_id} is not one of the " \ "controls encompassed by the baselines indicated " \ "indicated by the productnames parameter. Control " \ - "will not be omitted.") \ No newline at end of file + "will not be omitted.") From 7a064c1bdefefa983fa380000c7964dea6fe9e62 Mon Sep 17 00:00:00 2001 From: Alden Hilton <106177711+adhilto@users.noreply.github.com> Date: Tue, 1 Oct 2024 08:51:22 -0700 Subject: [PATCH 15/20] Remove commented out code Co-authored-by: David Bui <105074908+buidav@users.noreply.github.com> --- scubagoggles/orchestrator.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/scubagoggles/orchestrator.py b/scubagoggles/orchestrator.py index 9a50947f..f4d0318c 100644 --- a/scubagoggles/orchestrator.py +++ b/scubagoggles/orchestrator.py @@ -185,9 +185,6 @@ def _generate_summary(cls, stats: dict) -> str: omit_summary = (f"
{n_omit}" " omitted
") - # summary = f"{pass_summary}{warning_summary}{failure_summary}" - # summary += f"{manual_summary}{omit_summary}{error_summary}" - # return summary return f"{pass_summary}{warning_summary}{failure_summary}" \ f"{manual_summary}{omit_summary}{error_summary}" From 90d4f4be477bf8899ed24ae3f49f32848d78f677 Mon Sep 17 00:00:00 2001 From: Alden Hilton Date: Tue, 1 Oct 2024 09:45:53 -0700 Subject: [PATCH 16/20] Break out omitpolicy validation into separate function. --- scubagoggles/scuba_argument_parser.py | 67 ++++++++++++++++----------- 1 file changed, 40 insertions(+), 27 deletions(-) diff --git a/scubagoggles/scuba_argument_parser.py b/scubagoggles/scuba_argument_parser.py index d9dbf3a6..c1fc13d2 100644 --- a/scubagoggles/scuba_argument_parser.py +++ b/scubagoggles/scuba_argument_parser.py @@ -65,7 +65,7 @@ def parse_args_with_config(self) -> argparse.Namespace: vars(args)[param] = config[param] # Check for logical errors in the resulting configuration - self._validate_config(args) + self.validate_config(args) # Return the args (argparse.Namespace) return args @@ -94,30 +94,43 @@ def _get_explicit_cli_args(cls, args : argparse.Namespace) -> dict: cli_args, _ = aux_parser.parse_known_args() return cli_args - @classmethod - def _validate_config(cls, args : argparse.Namespace) -> None: + @staticmethod + def validate_config(args : argparse.Namespace) -> None: + """ + Check for an logical errors in the advanced ScubaGoggles configuration + options. NOTE: "omitpolicy" is the only such option for now; more to + come. + """ if 'omitpolicy' in args: - products = Orchestrator.gws_products()['prod_to_fullname'] - prod_to_fullname = { - key: products[key] - for key in args.baselines - if key in products - } - - # Parse the baselines to determine the set of valid control IDs - path = Path(args.documentpath).resolve() - baseline_policies = read_baseline_docs(path, prod_to_fullname) - control_ids = set() - for product_baseline in baseline_policies.values(): - for group in product_baseline: - for control in group['Controls']: - control_ids.add(control['Id'].lower()) - - # Warn for any unexpected IDs - for control_id in args.omitpolicy: - if control_id.lower() not in control_ids: - warnings.warn("Config file indicates omitting " \ - f"{control_id}, but {control_id} is not one of the " \ - "controls encompassed by the baselines indicated " \ - "indicated by the productnames parameter. Control " \ - "will not be omitted.") + ScubaArgumentParser.validate_omissions(args) + + @staticmethod + def validate_omissions(args : argparse.Namespace) -> None: + """ + Warn for any control IDs configured for omission that aren't in the + set of IDs covered by the baselines specificied in --baselines. + """ + products = Orchestrator.gws_products()['prod_to_fullname'] + prod_to_fullname = { + key: products[key] + for key in args.baselines + if key in products + } + + # Parse the baselines to determine the set of valid control IDs + path = Path(args.documentpath).resolve() + baseline_policies = read_baseline_docs(path, prod_to_fullname) + control_ids = set() + for product_baseline in baseline_policies.values(): + for group in product_baseline: + for control in group['Controls']: + control_ids.add(control['Id'].lower()) + + # Warn for any unexpected IDs + for control_id in args.omitpolicy: + if control_id.lower() not in control_ids: + warnings.warn("Config file indicates omitting " \ + f"{control_id}, but {control_id} is not one of the " \ + "controls encompassed by the baselines indicated " \ + "indicated by the baselines parameter. Control " \ + "will not be omitted.") \ No newline at end of file From 032381a4b50819aebe9db328b9a164d34c3a4ef3 Mon Sep 17 00:00:00 2001 From: Alden Hilton Date: Tue, 1 Oct 2024 09:57:33 -0700 Subject: [PATCH 17/20] Add final newline to file for the linter --- scubagoggles/scuba_argument_parser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scubagoggles/scuba_argument_parser.py b/scubagoggles/scuba_argument_parser.py index c1fc13d2..48925022 100644 --- a/scubagoggles/scuba_argument_parser.py +++ b/scubagoggles/scuba_argument_parser.py @@ -133,4 +133,4 @@ def validate_omissions(args : argparse.Namespace) -> None: f"{control_id}, but {control_id} is not one of the " \ "controls encompassed by the baselines indicated " \ "indicated by the baselines parameter. Control " \ - "will not be omitted.") \ No newline at end of file + "will not be omitted.") From f8265abad1b9afc0844a6dd4beffdcb0fae289bd Mon Sep 17 00:00:00 2001 From: Alden Hilton Date: Tue, 1 Oct 2024 12:45:28 -0700 Subject: [PATCH 18/20] Add special handling for rules --- scubagoggles/reporter/reporter.py | 119 +++++++++++++++++++----------- 1 file changed, 75 insertions(+), 44 deletions(-) diff --git a/scubagoggles/reporter/reporter.py b/scubagoggles/reporter/reporter.py index 4fbe51ed..2b5c1a21 100644 --- a/scubagoggles/reporter/reporter.py +++ b/scubagoggles/reporter/reporter.py @@ -335,6 +335,31 @@ def _get_summary_category(result: str) -> str: return "Passes" raise ValueError(f"Unexpected result, {result}", RuntimeWarning) + def _handle_rules_omission(self, control_id : str, tests : list): + '''Process the test results for the rules report if the rules control + was omitted. + + :control_id: The control ID for the rules control. + :tests: A list of test result dictionaries. + ''' + table_data = [] + for test in tests: + if 'Not-Implemented' in test['Criticality']: + # The easiest way to identify the common controls "rules" + # results that belong to the Common Controls report is they're + # marked as Not-Implemented. This if excludes them from the + # rules report. + continue + rationale = self._get_omission_rationale(control_id) + table_data.append({ + 'Control ID': control_id, + 'Rule Name': test['Requirement'], + 'Result': 'Omitted', + 'Criticality': test['Criticality'], + 'Rule Description': f'N/A; test omitted by user. {rationale}' + }) + return table_data + def rego_json_to_ind_reports(self, test_results: list, out_path: str) -> list: ''' Transforms the Rego JSON output into individual HTML and JSON reports @@ -381,6 +406,12 @@ def rego_json_to_ind_reports(self, test_results: list, out_path: str) -> list: continue if self._is_control_omitted(control['Id']): # Handle the case where the control was omitted + if product_capitalized == "Rules": + # Rules is a special case + rules_data = self._handle_rules_omission(control['Id'], tests) + table_data.extend(rules_data) + report_stats['Omit'] += len(rules_data) + continue report_stats['Omit'] += 1 rationale = self._get_omission_rationale(control['Id']) table_data.append({ @@ -402,52 +433,52 @@ def rego_json_to_ind_reports(self, test_results: list, out_path: str) -> list: 'Result': "Error", 'Criticality': test['Criticality'], 'Details': failed_details}) - else: - result = self._get_test_result(test['RequirementMet'], - test['Criticality'], - test['NoSuchEvent']) - - details = test['ReportDetails'] - - if result == "No events found": - warning_icon = "\ - " - details = warning_icon + " " + test['ReportDetails'] - # As rules doesn't have its own baseline, Rules and Common Controls - # need to be handled specially - if product_capitalized == "Rules": - if 'Not-Implemented' in test['Criticality']: - # The easiest way to identify the GWS.COMMONCONTROLS.13.1v1 - # results that belong to the Common Controls report is they're - # marked as Not-Implemented. This if excludes them from the - # rules report. - continue - report_stats[self._get_summary_category(result)] += 1 - table_data.append({ - 'Control ID': control['Id'], - 'Rule Name': test['Requirement'], - 'Result': result, - 'Criticality': test['Criticality'], - 'Rule Description': test['ReportDetails']}) - elif product_capitalized == "Commoncontrols" \ - and baseline_group['GroupName'] == 'System-defined Rules' \ - and 'Not-Implemented' not in test['Criticality']: - # The easiest way to identify the System-defined Rules + continue + result = self._get_test_result(test['RequirementMet'], + test['Criticality'], + test['NoSuchEvent']) + + details = test['ReportDetails'] + + if result == "No events found": + warning_icon = "\ + " + details = warning_icon + " " + test['ReportDetails'] + # As rules doesn't have its own baseline, Rules and Common Controls + # need to be handled specially + if product_capitalized == "Rules": + if 'Not-Implemented' in test['Criticality']: + # The easiest way to identify the GWS.COMMONCONTROLS.13.1v1 # results that belong to the Common Controls report is they're - # marked as Not-Implemented. This if excludes the full results - # from the Common Controls report. + # marked as Not-Implemented. This if excludes them from the + # rules report. continue - else: - report_stats[self._get_summary_category(result)] += 1 - table_data.append({ - 'Control ID': control['Id'], - 'Requirement': control['Value'], - 'Result': result, - 'Criticality': test['Criticality'], - 'Details': details}) + report_stats[self._get_summary_category(result)] += 1 + table_data.append({ + 'Control ID': control['Id'], + 'Rule Name': test['Requirement'], + 'Result': result, + 'Criticality': test['Criticality'], + 'Rule Description': test['ReportDetails']}) + elif product_capitalized == "Commoncontrols" \ + and baseline_group['GroupName'] == 'System-defined Rules' \ + and 'Not-Implemented' not in test['Criticality']: + # The easiest way to identify the System-defined Rules + # results that belong to the Common Controls report is they're + # marked as Not-Implemented. This if excludes the full results + # from the Common Controls report. + continue + else: + report_stats[self._get_summary_category(result)] += 1 + table_data.append({ + 'Control ID': control['Id'], + 'Requirement': control['Value'], + 'Result': result, + 'Criticality': test['Criticality'], + 'Details': details}) markdown_group_name = "-".join(baseline_group['GroupName'].split()) md_basename = "commoncontrols" if self._product == "rules" else self._product group_reference_url = f'{github_url}/blob/v{tool_version}/baselines/'\ From 713a3b4edccf487b2baa68b2403a5d3248020c32 Mon Sep 17 00:00:00 2001 From: Alden Hilton <106177711+adhilto@users.noreply.github.com> Date: Tue, 1 Oct 2024 17:02:54 -0700 Subject: [PATCH 19/20] Remove duplicate word Co-authored-by: mitchelbaker-cisa <149098823+mitchelbaker-cisa@users.noreply.github.com> --- scubagoggles/scuba_argument_parser.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scubagoggles/scuba_argument_parser.py b/scubagoggles/scuba_argument_parser.py index 48925022..8bb7732d 100644 --- a/scubagoggles/scuba_argument_parser.py +++ b/scubagoggles/scuba_argument_parser.py @@ -132,5 +132,6 @@ def validate_omissions(args : argparse.Namespace) -> None: warnings.warn("Config file indicates omitting " \ f"{control_id}, but {control_id} is not one of the " \ "controls encompassed by the baselines indicated " \ - "indicated by the baselines parameter. Control " \ + "by the baselines parameter. Control " \ + "will not be omitted.") From 19e60ce122a3dfa9cd20b4c923de32fadbaa1353 Mon Sep 17 00:00:00 2001 From: Alden Hilton Date: Wed, 9 Oct 2024 12:57:03 -0700 Subject: [PATCH 20/20] Create warn wrapper function to clean tqdm output --- scubagoggles/orchestrator.py | 3 ++- scubagoggles/reporter/reporter.py | 29 +++++++++++++++++++++++------ 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/scubagoggles/orchestrator.py b/scubagoggles/orchestrator.py index f4d0318c..b5b68931 100644 --- a/scubagoggles/orchestrator.py +++ b/scubagoggles/orchestrator.py @@ -296,7 +296,8 @@ def _run_reporter(self): baseline_policies[product], successful_calls, unsuccessful_calls, - omissions) + omissions, + products_bar) stats_and_data[product] = \ reporter.rego_json_to_ind_reports(test_results_data, out_folder) diff --git a/scubagoggles/reporter/reporter.py b/scubagoggles/reporter/reporter.py index 2b5c1a21..d7bd8872 100644 --- a/scubagoggles/reporter/reporter.py +++ b/scubagoggles/reporter/reporter.py @@ -12,7 +12,7 @@ from scubagoggles.scuba_constants import API_LINKS -# Eight instance attributes is reasonable in this case. +# Nine instance attributes is reasonable in this case. # pylint: disable-next=too-many-instance-attributes class Reporter: @@ -30,7 +30,8 @@ def __init__(self, product_policies: list, successful_calls: set, unsuccessful_calls: set, - omissions: dict): + omissions: dict, + progress_bar = None): """Reporter class initialization @@ -44,6 +45,9 @@ def __init__(self, :param unsuccessful_calls: set with the set of unsuccessful calls :param omissions: dict with the omissions specified in the config file (empty dict if none omitted) + :param progress_bar: Optional TQDM instance. If provided, the + progress bar will be cleared before any warnings are printed + while generating the report, for cleaner output. """ self._product = product @@ -57,6 +61,7 @@ def __init__(self, # Lowercase all the keys for case-insensitive comparisons key.lower(): value for key, value in omissions.items() } + self.progress_bar = progress_bar @staticmethod def _get_test_result(requirement_met: bool, criticality: str, no_such_events: bool) -> str: @@ -175,7 +180,7 @@ def _is_control_omitted(self, control_id : str) -> bool: f"but the provided expiration date, {raw_date}, is " \ "malformed. The expected format is yyyy-mm-dd. Control" \ " will not be omitted." - warnings.warn(warning, RuntimeWarning) + self._warn(warning, RuntimeWarning) return False now = datetime.now() if expiration_date > now: @@ -185,7 +190,7 @@ def _is_control_omitted(self, control_id : str) -> bool: warning = f"Config file indicates omitting {control_id}, but " \ f"the provided expiration date, {raw_date}, has passed. " \ "Control will not be omitted." - warnings.warn(warning, RuntimeWarning) + self._warn(warning, RuntimeWarning) return False def _get_omission_rationale(self, control_id : str) -> str: @@ -211,7 +216,7 @@ def _get_omission_rationale(self, control_id : str) -> str: if no_rationale: warning = f"Config file indicates omitting {control_id}, but " \ "no rationale provided." - warnings.warn(warning, RuntimeWarning) + self._warn(warning, RuntimeWarning) return "Rationale not provided." return self._omissions[control_id]['rationale'] @@ -360,6 +365,18 @@ def _handle_rules_omission(self, control_id : str, tests : list): }) return table_data + def _warn(self, *args, **kwargs): + ''' + Wrapper for the warnings.warn function, that clears and refreshes the + progress bar if one has been provided, to keep the output clean. + Accepts all the arguments the warnings.warn function accepts. + ''' + if self.progress_bar is not None: + self.progress_bar.clear() + warnings.warn(*args, **kwargs) + if self.progress_bar is not None: + self.progress_bar.refresh() + def rego_json_to_ind_reports(self, test_results: list, out_path: str) -> list: ''' Transforms the Rego JSON output into individual HTML and JSON reports @@ -401,7 +418,7 @@ def rego_json_to_ind_reports(self, test_results: list, out_path: str) -> list: 'Result': "Error - Test results missing", 'Criticality': "-", 'Details': f'Report issue on {issues_link}'}) - warnings.warn(f"No test results found for Control Id {control['Id']}", + self._warn(f"No test results found for Control Id {control['Id']}", RuntimeWarning) continue if self._is_control_omitted(control['Id']):