From 22ea2010a351edfdbdda37d293c06b53d26075c1 Mon Sep 17 00:00:00 2001
From: Roy Lane <rlane@mitre.org>
Date: Fri, 12 Jul 2024 15:27:01 -0400
Subject: [PATCH] Orchestrator: convert to class implementation

---
 scubagoggles/main.py         |   6 +-
 scubagoggles/orchestrator.py | 722 ++++++++++++++++++-----------------
 2 files changed, 375 insertions(+), 353 deletions(-)

diff --git a/scubagoggles/main.py b/scubagoggles/main.py
index b024ae25..bea30aec 100644
--- a/scubagoggles/main.py
+++ b/scubagoggles/main.py
@@ -6,7 +6,7 @@
 """
 
 import argparse
-from scubagoggles.orchestrator import gws_products, start_automation
+from scubagoggles.orchestrator import Orchestrator
 
 def get_gws_args(parser):
     """
@@ -14,7 +14,7 @@ def get_gws_args(parser):
 
     :param parser: argparse object
     """
-    gws = gws_products()
+    gws = Orchestrator.gws_products()
     gws_baselines = gws["gws_baselines"]
 
     default_file_output_names = {
@@ -140,6 +140,6 @@ def dive():
     args = parser.parse_args()
 
     if args.scuba_cmd == 'gws':
-        start_automation(args)
+        Orchestrator(args).start_automation()
     else:
         raise Exception("Invalid subparser. Run scubagoggles -h to see a list of valid subparsers")
diff --git a/scubagoggles/orchestrator.py b/scubagoggles/orchestrator.py
index f55ffec2..167083f0 100644
--- a/scubagoggles/orchestrator.py
+++ b/scubagoggles/orchestrator.py
@@ -2,6 +2,7 @@
 orchestrator.py is the main module that starts and handles the output of the
 provider, rego, and report modules of the SCuBA tool
 """
+import argparse
 import shutil
 import os
 import json
@@ -17,11 +18,16 @@
 from scubagoggles.reporter import reporter, md_parser
 from scubagoggles.utils import rel_abs_path
 
-def gws_products() -> dict:
-    """
-    Dictionary of the SCuBA GWS baselines short names plus full names
+
+class Orchestrator:
+
+    """The Orchestrator class runs the provider to get the GWS configuration
+    data, then runs OPA on the configuration data, and finally invokes the
+    reporter to produce the conformance reports.
     """
-    gws_baselines = [
+
+    # Dictionary of the SCuBA GWS baselines short names plus full names.
+    _gws_baselines = [
         "gmail",
         "calendar",
         "groups",
@@ -33,7 +39,7 @@ def gws_products() -> dict:
         "rules",
         "classroom"
     ]
-    prod_to_fullname = {
+    _prod_to_fullname = {
         "gmail": "Gmail",
         "calendar": "Google Calendar",
         "groups": "Groups for Business",
@@ -45,356 +51,372 @@ def gws_products() -> dict:
         "rules": "Rules",
         "classroom": "Google Classroom"
     }
-    gws = {
-        "gws_baselines": gws_baselines,
-        "prod_to_fullname": prod_to_fullname
+    _gws = {
+        "gws_baselines": _gws_baselines,
+        "prod_to_fullname": _prod_to_fullname
     }
-    return gws
-
-def run_gws_providers(args, services : dict):
-    """
-    Runs the provider scripts and outputs a json to path
-
-    :param args: the command line arguments to this script
-    :param services: a dictionary of Google API service objects
-    """
-
-    products = args.baselines
-    out_folder = args.outputpath
-    provider_dict = {}
-
-    provider = Provider(services, args.customerid)
-    provider_dict = provider.call_gws_providers(products, args.quiet)
-    provider_dict['successful_calls'] = list(provider.successful_calls)
-    provider_dict['unsuccessful_calls'] = list(provider.unsuccessful_calls)
-
-    settings_json = json.dumps(provider_dict, indent = 4)
-    out_path = out_folder + f'/{args.outputproviderfilename}.json'
-    with open(out_path, mode="w", encoding='UTF-8') as outfile:
-        outfile.write(settings_json)
-
-def rego_eval(args):
-    """
-    Executes the OPA executable with provider json input against
-    specified rego files and outputs a json to path
-
-    :param args: the command line arguments to this script
-    """
-
-    products = args.baselines
-    products_bar = tqdm(products, leave=False, disable=args.quiet)
-    out_folder = args.outputpath
-    results = []
-    for product in products_bar:
-        product_name = product
-        input_file = f'{out_folder}/{args.outputproviderfilename}.json'
-        opa_path = args.opapath
-        rego_path = args.regopath
-
-        products_bar.set_description(f"Running Rego verification for {product}...")
-        product_tests = opa_eval(
-        product_name=product_name,
-        input_file=input_file,
-        opa_path=opa_path,
-        rego_path=rego_path,
-        omit_sudo=args.omitsudo,
-        debug=args.debug
-        )
-        try:
-            results.extend(product_tests[0])
-        except Exception as exc:
-            raise Exception("run_rego error") from exc
-
-    settings_json = json.dumps(results,sort_keys=True ,indent = 4)
-    out_path = out_folder + f'/{args.outputregofilename}.json'
-    with open(out_path, mode="w", encoding='UTF-8') as outfile:
-        outfile.write(settings_json)
-
-def pluralize(singular : str, plural : str, count : int) -> str:
-    """
-    If count is 1, returns the singular version of the word.
-    Else returns the plural version.
-    :param singular: string value in singular tense
-    :param plural: string value in plural tense
-    :param count: how many of string value
-    """
-    if count == 1:
-        return singular
-    return plural
 
-def generate_summary(stats : dict) -> str:
-    """
-    Craft the html-formatted summary from the stats dictionary.
-    """
-    n_success = stats["Passes"]
-    n_warn = stats["Warnings"]
-    n_fail = stats["Failures"]
-    n_manual = stats["Manual"]
-    n_error = stats["Errors"]
-
-    pass_summary = (f"<div class='summary pass'>{n_success}"
-    f" {pluralize('test', 'tests', n_success)} passed</div>")
-
-    # The warnings, failures, and manuals are only shown if they are
-    # greater than zero. Reserve the space for them here. They will
-    # be filled next if needed.
-    warning_summary = "<div class='summary'></div>"
-    failure_summary = "<div class='summary'></div>"
-    manual_summary = "<div class='summary'></div>"
-    error_summary = "<div class='summary'></div>"
-
-    if n_warn > 0:
-        warning_summary = (f"<div class='summary warning'>{n_warn}"
-        f" {pluralize('warning', 'warnings', n_warn)}</div>")
-    if n_fail > 0:
-        failure_summary = (f"<div class='summary failure'>{n_fail}"
-        f" {pluralize('test', 'tests', n_fail)} failed</div>")
-    if n_manual > 0:
-        manual_summary = (f"<div class='summary manual'>{n_manual} manual"
-        f" {pluralize('check', 'checks', n_manual)} needed</div>")
-    if n_error > 0:
-        error_summary = (f"<div class='summary error'>{n_error}"
-        f" {pluralize('error', 'errors', n_error)}</div>")
-
-    return f"{pass_summary}{warning_summary}{failure_summary}{manual_summary}{error_summary}"
-
-def run_reporter(args):
-    """
-    Creates the indvididual reports and the front page
-    :param args: list of arguments to run report on
-    """
-
-    # Make the report output folders
-    out_folder = args.outputpath
-    individual_reports_path = out_folder + "/IndividualReports"
-    reports_images_path = individual_reports_path + "/images"
-    Path(individual_reports_path).mkdir(parents=True, exist_ok=True)
-    Path(reports_images_path).mkdir(parents=True, exist_ok=True)
-
-    # Copy the CISA logo to the repo folder so that it can be accessed
-    # from there
-    cisa_logo = str(rel_abs_path(__file__,"./reporter/images/cisa_logo.png"))
-    triangle_svg = str(rel_abs_path(__file__,"./reporter/images/triangle-exclamation-solid.svg"))
-    shutil.copy2(cisa_logo, reports_images_path)
-    shutil.copy2(triangle_svg, reports_images_path)
-
-    # we should load the testresults json here
-    products = args.baselines
-    prod_to_fullname = args.fullnamesdict
-    test_results_json = out_folder + f'/{args.outputregofilename}.json'
-    with open(test_results_json, mode='r', encoding='UTF-8') as file:
-        test_results_data = json.load(file)
-
-    # Get the successful/unsuccessful commands
-    settings_name = f'{out_folder}/{args.outputproviderfilename}.json'
-    with open(settings_name, mode='r', encoding='UTF-8') as file:
-        settings_data = json.load(file)
-    successful_calls = set(settings_data['successful_calls'])
-    unsuccessful_calls = set(settings_data['unsuccessful_calls'])
-
-    # baseline_path
-    subset_prod_to_fullname = {
-        key: prod_to_fullname[key]
-        for key in args.baselines
-        if key in prod_to_fullname
-    }
-
-    baseline_policies = md_parser.read_baseline_docs(args.documentpath,subset_prod_to_fullname)
-
-    if "rules" in args.baselines:
-        # There's no baseline specific to rules, so this case
-        # needs to be handled separately
-        baseline_policies["rules"] = []
-        for group in baseline_policies['commoncontrols']:
-            if group['GroupName'] == 'System-defined Rules':
-                baseline_policies["rules"].append(group)
-                break
+    def __init__(self, args: argparse.Namespace):
+
+        """Orchestrator class initialization
+
+        :param args: command arguments parsed by the argparse module.  See
+            the GWS parser definition (get_gws_args()) in main.py for
+            information about the arguments.
+        """
+
+        self._args = args
+
+    @classmethod
+    def gws_products(cls) -> dict:
+        """
+        Dictionary of the SCuBA GWS baselines short names plus full names
+        """
+        return cls._gws
+
+    def _run_gws_providers(self, services: dict):
+        """
+        Runs the provider scripts and outputs a json to path
+
+        :param services: a dictionary of Google API service objects
+        """
+
+        args = self._args
+        products = args.baselines
+        out_folder = args.outputpath
+
+        provider = Provider(services, args.customerid)
+        provider_dict = provider.call_gws_providers(products, args.quiet)
+        provider_dict['successful_calls'] = list(provider.successful_calls)
+        provider_dict['unsuccessful_calls'] = list(provider.unsuccessful_calls)
+
+        settings_json = json.dumps(provider_dict, indent = 4)
+        out_path = out_folder + f'/{args.outputproviderfilename}.json'
+        with open(out_path, mode="w", encoding='UTF-8') as outfile:
+            outfile.write(settings_json)
+
+    def _rego_eval(self):
+        """
+        Executes the OPA executable with provider json input against
+        specified rego files and outputs a json to path
+        """
+
+        args = self._args
+        products = args.baselines
+        products_bar = tqdm(products, leave=False, disable=args.quiet)
+        out_folder = args.outputpath
+        results = []
+        for product in products_bar:
+            product_name = product
+            input_file = f'{out_folder}/{args.outputproviderfilename}.json'
+            opa_path = args.opapath
+            rego_path = args.regopath
+
+            products_bar.set_description(f"Running Rego verification for {product}...")
+            product_tests = opa_eval(
+            product_name=product_name,
+            input_file=input_file,
+            opa_path=opa_path,
+            rego_path=rego_path,
+            omit_sudo=args.omitsudo,
+            debug=args.debug
+            )
+            try:
+                results.extend(product_tests[0])
+            except Exception as exc:
+                raise Exception("run_rego error") from exc
+
+        settings_json = json.dumps(results,sort_keys=True ,indent = 4)
+        out_path = out_folder + f'/{args.outputregofilename}.json'
+        with open(out_path, mode="w", encoding='UTF-8') as outfile:
+            outfile.write(settings_json)
+
+    @staticmethod
+    def _pluralize(singular: str, plural: str, count: int) -> str:
+        """
+        If count is 1, returns the singular version of the word.
+        Else returns the plural version.
+        :param singular: string value in singular tense
+        :param plural: string value in plural tense
+        :param count: how many of string value
+        """
+        if count == 1:
+            return singular
+        return plural
+
+    @classmethod
+    def _generate_summary(cls, stats: dict) -> str:
+        """
+        Craft the html-formatted summary from the stats dictionary.
+        """
+        n_success = stats["Passes"]
+        n_warn = stats["Warnings"]
+        n_fail = stats["Failures"]
+        n_manual = stats["Manual"]
+        n_error = stats["Errors"]
+
+        pass_summary = (f"<div class='summary pass'>{n_success}"
+        f" {cls._pluralize('test', 'tests', n_success)} passed</div>")
+
+        # The warnings, failures, and manuals are only shown if they are
+        # greater than zero. Reserve the space for them here. They will
+        # be filled next if needed.
+        warning_summary = "<div class='summary'></div>"
+        failure_summary = "<div class='summary'></div>"
+        manual_summary = "<div class='summary'></div>"
+        error_summary = "<div class='summary'></div>"
+
+        if n_warn > 0:
+            warning_summary = (f"<div class='summary warning'>{n_warn}"
+            f" {cls._pluralize('warning', 'warnings', n_warn)}</div>")
+        if n_fail > 0:
+            failure_summary = (f"<div class='summary failure'>{n_fail}"
+            f" {cls._pluralize('test', 'tests', n_fail)} failed</div>")
+        if n_manual > 0:
+            manual_summary = (f"<div class='summary manual'>{n_manual} manual"
+            f" {cls._pluralize('check', 'checks', n_manual)} needed</div>")
+        if n_error > 0:
+            error_summary = (f"<div class='summary error'>{n_error}"
+            f" {cls._pluralize('error', 'errors', n_error)}</div>")
+
+        return f"{pass_summary}{warning_summary}{failure_summary}{manual_summary}{error_summary}"
+
+    def _run_reporter(self):
+        """
+        Creates the individual reports and the front page
+        """
+
+        # Make the report output folders
+        args = self._args
+        out_folder = args.outputpath
+        individual_reports_path = out_folder + "/IndividualReports"
+        reports_images_path = individual_reports_path + "/images"
+        Path(individual_reports_path).mkdir(parents=True, exist_ok=True)
+        Path(reports_images_path).mkdir(parents=True, exist_ok=True)
+
+        # Copy the CISA logo to the repo folder so that it can be accessed
+        # from there
+        images_dir = Path(__file__).parent / 'reporter' / 'images'
+        cisa_logo = images_dir / 'cisa_logo.png'
+        triangle_svg = images_dir / 'triangle-exclamation-solid.svg'
+        shutil.copy2(cisa_logo, reports_images_path)
+        shutil.copy2(triangle_svg, reports_images_path)
+
+        # we should load the test results json here
+        products = args.baselines
+        prod_to_fullname = args.fullnamesdict
+        test_results_json = out_folder + f'/{args.outputregofilename}.json'
+        with open(test_results_json, mode='r', encoding='UTF-8') as file:
+            test_results_data = json.load(file)
+
+        # Get the successful/unsuccessful commands
+        settings_name = f'{out_folder}/{args.outputproviderfilename}.json'
+        with open(settings_name, mode='r', encoding='UTF-8') as file:
+            settings_data = json.load(file)
+        successful_calls = set(settings_data['successful_calls'])
+        unsuccessful_calls = set(settings_data['unsuccessful_calls'])
+
+        # baseline_path
+        subset_prod_to_fullname = {
+            key: prod_to_fullname[key]
+            for key in args.baselines
+            if key in prod_to_fullname
+        }
+
+        baseline_policies = md_parser.read_baseline_docs(args.documentpath,
+                                                         subset_prod_to_fullname)
+
+        if "rules" in args.baselines:
+            # There's no baseline specific to rules, so this case
+            # needs to be handled separately
+            baseline_policies["rules"] = []
+            for group in baseline_policies['commoncontrols']:
+                if group['GroupName'] == 'System-defined Rules':
+                    baseline_policies["rules"].append(group)
+                    break
+            else:
+                raise RuntimeError("Unable to process 'rules' as no policy group named "
+                    "'System-defined Rules' found in the Common Controls baseline.")
+
+        # Load Org metadata from provider
+        with open(f'{out_folder}/{args.outputproviderfilename}.json',
+        mode='r',encoding='UTF-8') as file:
+            tenant_info = json.load(file)['tenant_info']
+            tenant_domain = tenant_info['domain']
+
+        # Create the individual report files
+        out_jsonfile = args.outjsonfilename
+        summary = {}
+        results = {}
+        total_output = {}
+        stats_and_data = {}
+
+        products_assessed = [prod_to_fullname[product] for product in products
+                             if product in prod_to_fullname]
+        product_abbreviation_mapping = {fullname: shortname for shortname,
+                                        fullname in prod_to_fullname.items()}
+
+        timestamp_utc = datetime.utcnow()
+        timestamp_zulu = timestamp_utc.strftime('%Y-%m-%dT%H:%M:%S.%f')[:-3] + 'Z'
+
+        report_metadata = {
+            "TenantId":  None,
+            "DisplayName":  None,
+            "DomainName":  tenant_domain,
+            "ProductSuite":  "GWS",
+            "ProductsAssessed": products_assessed,
+            "ProductAbbreviationMapping": product_abbreviation_mapping,
+            "Tool":  "ScubaGoggles",
+            "ToolVersion":  "0.2.0",
+            "TimeStampZulu": timestamp_zulu
+        }
+
+        total_output.update({"MetaData": report_metadata})
+
+        main_report_name = args.outputreportfilename
+        products_bar = tqdm(products, leave=False, disable=args.quiet)
+        for product in products_bar:
+            products_bar.set_description(f"Creating the HTML and JSON Report for {product}...")
+            stats_and_data[product] = reporter.rego_json_to_ind_reports(
+                test_results_data,
+                product,
+                out_folder,
+                tenant_domain,
+                main_report_name,
+                prod_to_fullname,
+                baseline_policies[product],
+                successful_calls,
+                unsuccessful_calls
+            )
+            baseline_product_summary = {product:stats_and_data[product][0]}
+            baseline_product_results_json = {product:stats_and_data[product][1]}
+            summary.update(baseline_product_summary)
+            results.update(baseline_product_results_json)
+            total_output.update({"Summary": summary})
+            total_output.update({"Results": results})
+
+        # Create the ScubaResults files
+        with open(f'{out_folder}/{args.outputproviderfilename}.json', encoding='UTF-8') as file:
+            raw_data = json.load(file)
+        total_output.update({"Raw": raw_data})
+        report = json.dumps(total_output, indent = 4)
+        with open(f"{out_folder}/{out_jsonfile}.json", mode='w', encoding='UTF-8') as results_file:
+            results_file.write(report)
+
+        # Delete the ProviderOutput file as it's now encapsulated in the ScubaResults file
+        os.remove(f"{out_folder}/{args.outputproviderfilename}.json")
+
+        # Make the report front page
+        report_path = out_folder + "/" + f'{args.outputreportfilename}.html'
+        abs_report_path = os.path.abspath(report_path)
+
+        fragments = []
+        table_data = []
+        for product, stats in stats_and_data.items():
+            ## Build the "Baseline Conformance Reports" column
+            product_capitalize = product.capitalize()
+            full_name = prod_to_fullname[product]
+            link_path =  "./IndividualReports/" f"{product_capitalize}Report.html"
+            link = f"<a class=\"individual_reports\" href={link_path}>{full_name}</a>"
+            table_data.append({
+                "Baseline Conformance Reports": link,
+                "Details": self._generate_summary(stats[0])
+            })
+
+        fragments.append(reporter.create_html_table(table_data))
+        with open(f"{report_path}", mode='w', encoding='UTF-8') as file:
+            file.write(reporter.build_front_page_html(fragments, tenant_info))
+
+        # suppress opening the report in the browser
+        if args.quiet:
+            return
+        # Open the report in the client's default web browser
+        # pylint: disable=E1101
+        if os.name == 'nt':
+            os.startfile(abs_report_path)
         else:
-            raise RuntimeError("Unable to process 'rules' as no policy group named "
-                "'System-defined Rules' found in the Common Controls baseline.")
-
-    # Load Org metadata from provider
-    with open(f'{out_folder}/{args.outputproviderfilename}.json',
-    mode='r',encoding='UTF-8') as file:
-        tenant_info = json.load(file)['tenant_info']
-        tenant_domain = tenant_info['domain']
-
-
-    # Create the the individual report files
-    out_jsonfile = args.outjsonfilename
-    summary = {}
-    results = {}
-    total_output = {}
-    stats_and_data = {}
-
-    products_assessed = [prod_to_fullname[product] for product in products
-                         if product in prod_to_fullname]
-    product_abbreviation_mapping = {fullname: shortname for shortname,
-                                    fullname in prod_to_fullname.items()}
-
-    timestamp_utc = datetime.utcnow()
-    timestamp_zulu = timestamp_utc.strftime('%Y-%m-%dT%H:%M:%S.%f')[:-3] + 'Z'
-
-    report_metadata = {
-        "TenantId":  None,
-        "DisplayName":  None,
-        "DomainName":  tenant_domain,
-        "ProductSuite":  "GWS",
-        "ProductsAssessed": products_assessed,
-        "ProductAbbreviationMapping": product_abbreviation_mapping,
-        "Tool":  "ScubaGoggles",
-        "ToolVersion":  "0.2.0",
-        "TimeStampZulu": timestamp_zulu
-    }
+            report_path = "file:///" + abs_report_path
+            webbrowser.get().open(report_path, new=2)
 
-    total_output.update({"MetaData": report_metadata})
-
-    main_report_name = args.outputreportfilename
-    products_bar = tqdm(products, leave=False, disable=args.quiet)
-    for product in products_bar:
-        products_bar.set_description(f"Creating the HTML and JSON Report for {product}...")
-        stats_and_data[product] = reporter.rego_json_to_ind_reports(
-            test_results_data,
-            product,
-            out_folder,
-            tenant_domain,
-            main_report_name,
-            prod_to_fullname,
-            baseline_policies[product],
-            successful_calls,
-            unsuccessful_calls
-        )
-        baseline_product_summary = {product:stats_and_data[product][0]}
-        baseline_product_results_json = {product:stats_and_data[product][1]}
-        summary.update(baseline_product_summary)
-        results.update(baseline_product_results_json)
-        total_output.update({"Summary": summary})
-        total_output.update({"Results": results})
-
-    # Create the ScubaResults files
-    with open(f'{out_folder}/{args.outputproviderfilename}.json', encoding='UTF-8') as file:
-        raw_data = json.load(file)
-    total_output.update({"Raw": raw_data})
-    report = json.dumps(total_output, indent = 4)
-    with open(f"{out_folder}/{out_jsonfile}.json", mode='w', encoding='UTF-8') as results_file:
-        results_file.write(report)
-
-    # Delete the ProviderOutput file as it's now encapsulated in the ScubaResults file
-    os.remove(f"{out_folder}/{args.outputproviderfilename}.json")
-
-    # Make the report front page
-    report_path = out_folder + "/" + f'{args.outputreportfilename}.html'
-    abs_report_path = os.path.abspath(report_path)
-
-    fragments = []
-    table_data = []
-    for product, stats in stats_and_data.items():
-        ## Build the "Baseline Conformance Reports" column
-        product_capitalize = product.capitalize()
-        full_name = prod_to_fullname[product]
-        link_path =  "./IndividualReports/" f"{product_capitalize}Report.html"
-        link = f"<a class=\"individual_reports\" href={link_path}>{full_name}</a>"
-        table_data.append({
-            "Baseline Conformance Reports": link,
-            "Details": generate_summary(stats[0])
-        })
-
-    fragments.append(reporter.create_html_table(table_data))
-    with open(f"{report_path}", mode='w', encoding='UTF-8') as file:
-        file.write(reporter.build_front_page_html(fragments, tenant_info))
-
-    # suppress opening the report in the browser
-    if args.quiet:
-        return
-    # Open the report in the client's default web browser
-    # pylint: disable=E1101
-    if os.name == 'nt':
-        os.startfile(abs_report_path)
-    else:
-        report_path = "file:///" + abs_report_path
-        webbrowser.get().open(report_path, new=2)
-
-def run_cached(args):
-    """
-    Has the ability to run scuba on a cached provider json
-
-    :param args: argparse object containing arguments to run
-    """
-
-    args.outputpath = str(rel_abs_path(__file__,args.outputpath))
-    Path(args.outputpath).mkdir(parents=True, exist_ok=True)
-    args.outputpath = os.path.abspath(args.outputpath)
-
-    if not args.skipexport:
-        creds = gws_auth(args.credentials)
-        services = {}
-        services['reports'] = build('admin', 'reports_v1', credentials=creds)
-        services['directory'] = build('admin', 'directory_v1', credentials=creds)
-        services['groups'] = build('groupssettings', 'v1', credentials=creds)
-        run_gws_providers(args, services)
-
-    if not os.path.exists(f'{args.outputpath}/{args.outputproviderfilename}.json'):
-        # When running run_cached, the provider output might not exist as a stand-alone
-        # file depending what version of ScubaGoggles created the output. If the provider
-        # ouptut doesn't exist as a standa-lone file, create it from the scuba results
-        # file so the other functions can execute as normal.
-        with open(f'{args.outputpath}/{args.outjsonfilename}.json', 'r',
-                encoding='UTF-8') as scuba_results:
-            provider_output = json.load(scuba_results)['Raw']
-        with open(f'{args.outputpath}/{args.outputproviderfilename}.json', 'w',
-                encoding='UTF-8') as provider_file:
-            json.dump(provider_output, provider_file)
-    rego_eval(args)
-    run_reporter(args)
-
-def start_automation(args):
-    """
-    Main orchestration function
-
-    :param args: argparse object containing arguments to run
-    """
+    def _run_cached(self):
+        """
+        Has the ability to run scuba on a cached provider json
+        """
 
-    if "commoncontrols" in args.baselines and "rules" not in args.baselines:
-        args.baselines.append("rules")
-    if "rules" in args.baselines and "commoncontrols" not in args.baselines:
-        args.baselines.append("commoncontrols")
-    args.baselines.sort()
-
-    # get the absolute paths relative to this directory
-    args.outputpath = (Path.cwd() / args.outputpath).resolve()
-    args.credentials = (Path.cwd() / args.credentials).resolve()
-    args.opapath = Path(args.opapath).resolve()
-    args.regopath = Path(args.regopath).resolve()
-    args.documentpath = Path(args.documentpath).resolve()
-
-    # add any additional variables to args
-    gws_params = gws_products()
-    additional_args = vars(args)
-    additional_args['fullnamesdict'] = gws_params["prod_to_fullname"]
-
-    if args.skipexport and not args.runcached:
-        exc = 'Used --skipexport without --runcached' \
-        'please rerun scubagoggles with --runcached as well'
-        raise Exception(exc)
-
-    if not args.runcached:
-        # create a timestamped output folder
-        now = datetime.now()
-        folder_time = now.strftime("%Y_%m_%d_%H_%M_%S")
-        timestamped_folder = f'{args.outputfoldername}_{folder_time}'
-        args.outputpath = (args.outputpath / timestamped_folder).resolve()
+        args = self._args
+        args.outputpath = str(rel_abs_path(__file__,args.outputpath))
         Path(args.outputpath).mkdir(parents=True, exist_ok=True)
         args.outputpath = os.path.abspath(args.outputpath)
 
-        # authenticate
-        creds = gws_auth(args.credentials, args.subjectemail)
-        services = {}
-        services['reports'] = build('admin', 'reports_v1', credentials=creds)
-        services['directory'] = build('admin', 'directory_v1', credentials=creds)
-        services['groups'] = build('groupssettings', 'v1', credentials=creds)
-
-        run_gws_providers(args, services)
-        rego_eval(args)
-        run_reporter(args)
-    else:
-        run_cached(args)
+        if not args.skipexport:
+            creds = gws_auth(args.credentials)
+            services = {}
+            services['reports'] = build('admin', 'reports_v1', credentials=creds)
+            services['directory'] = build('admin', 'directory_v1', credentials=creds)
+            services['groups'] = build('groupssettings', 'v1', credentials=creds)
+            self._run_gws_providers(services)
+
+        if not os.path.exists(f'{args.outputpath}/{args.outputproviderfilename}.json'):
+            # When running run_cached, the provider output might not exist as a stand-alone
+            # file depending what version of ScubaGoggles created the output. If the provider
+            # ouptut doesn't exist as a standa-lone file, create it from the scuba results
+            # file so the other functions can execute as normal.
+            with open(f'{args.outputpath}/{args.outjsonfilename}.json', 'r',
+                    encoding='UTF-8') as scuba_results:
+                provider_output = json.load(scuba_results)['Raw']
+            with open(f'{args.outputpath}/{args.outputproviderfilename}.json', 'w',
+                    encoding='UTF-8') as provider_file:
+                json.dump(provider_output, provider_file)
+        self._rego_eval()
+        self._run_reporter()
+
+    def start_automation(self):
+        """
+        Main orchestration function
+        """
+
+        args = self._args
+        if "commoncontrols" in args.baselines and "rules" not in args.baselines:
+            args.baselines.append("rules")
+        if "rules" in args.baselines and "commoncontrols" not in args.baselines:
+            args.baselines.append("commoncontrols")
+        args.baselines.sort()
+
+        # get the absolute paths relative to this directory
+        args.outputpath = (Path.cwd() / args.outputpath).resolve()
+        args.credentials = (Path.cwd() / args.credentials).resolve()
+        args.opapath = Path(args.opapath).resolve()
+        args.regopath = Path(args.regopath).resolve()
+        args.documentpath = Path(args.documentpath).resolve()
+
+        # add any additional variables to args
+        gws_params = self.gws_products()
+        additional_args = vars(args)
+        additional_args['fullnamesdict'] = gws_params["prod_to_fullname"]
+
+        if args.skipexport and not args.runcached:
+            exc = 'Used --skipexport without --runcached' \
+            'please rerun scubagoggles with --runcached as well'
+            raise Exception(exc)
+
+        if not args.runcached:
+            # create a timestamped output folder
+            now = datetime.now()
+            folder_time = now.strftime("%Y_%m_%d_%H_%M_%S")
+            timestamped_folder = f'{args.outputfoldername}_{folder_time}'
+            args.outputpath = (args.outputpath / timestamped_folder).resolve()
+            Path(args.outputpath).mkdir(parents=True, exist_ok=True)
+            args.outputpath = os.path.abspath(args.outputpath)
+
+            # authenticate
+            creds = gws_auth(args.credentials, args.subjectemail)
+            services = {}
+            services['reports'] = build('admin', 'reports_v1', credentials=creds)
+            services['directory'] = build('admin', 'directory_v1', credentials=creds)
+            services['groups'] = build('groupssettings', 'v1', credentials=creds)
+
+            self._run_gws_providers(services)
+            self._rego_eval()
+            self._run_reporter()
+        else:
+            self._run_cached()