def list_scenario_instance(self, scenario_name_or_path): scenario_name = normalize_scenario_name(scenario_name_or_path) scenario_instance_dir_path = find_scenario_instance_dir( self.base_dir, scenario_name) if scenario_instance_dir_path is None: print( f'[cloudgoat] Error: No scenario instance for "{scenario_name}" found.' f" Try: cloudgoat.py list deployed") return terraform = Terraform( working_dir=os.path.join(scenario_instance_dir_path, "terraform")) show_retcode, show_stdout, show_stderr = terraform.show( capture_output=False, no_color=IsNotFlagged) if show_retcode != 0: display_terraform_step_error("terraform show", show_retcode, show_stdout, show_stderr) return else: print( f"\n[cloudgoat] terraform show completed with no error code.") return
def display_cloudgoat_help(self, command): if not command or len(command) == 1: return print(help_text.CLOUDGOAT) # Makes "help foo" equivalent to "foo help". command.remove("help") if command[0] == "config": if len(command) > 1 and command[1] == "argcomplete": return print(help_text.CONFIG_ARGCOMPLETE) else: return print(help_text.CONFIG) elif command[0] == "create": return print(help_text.CREATE) elif command[0] == "destroy": return print(help_text.DESTROY) elif command[0] == "list": return print(help_text.LIST) elif command[0] == "help": if all([word == "help" for word in command]): joined_help_texts = " ".join( ["help text for" for word in command]) return print(f"Displays {joined_help_texts} CloudGoat.") else: scenario_name = normalize_scenario_name(command[0]) scenario_dir_path = find_scenario_dir(self.scenarios_dir, scenario_name) if scenario_dir_path: scenario_help_text = load_data_from_yaml_file( os.path.join(scenario_dir_path, "manifest.yml"), "help").strip() return print( f"[cloudgoat scenario: {scenario_name}]\n{scenario_help_text}" ) return print( f'Unrecognized command or scenario name. Try "cloudgoat.py help" or' f' "cloudgoat.py list all"')
def destroy_scenario(self, scenario_name_or_path, profile, confirmed=False): # Information gathering. scenario_name = normalize_scenario_name(scenario_name_or_path) scenario_instance_dir_path = find_scenario_instance_dir( self.base_dir, scenario_name) if scenario_instance_dir_path is None: print( f'[cloudgoat] Error: No scenario instance for "{scenario_name}" found.' f" Try: cloudgoat.py list deployed") return instance_name = os.path.basename(scenario_instance_dir_path) # Confirmation. if not confirmed: delete_permission = input( f'Destroy "{instance_name}"? [y/n]: ').strip() if not delete_permission or not delete_permission[0].lower( ) == "y": print(f"\nCancelled destruction of {instance_name}.\n") return # Terraform execution. terraform_directory = os.path.join(scenario_instance_dir_path, "terraform") if os.path.exists( os.path.join(terraform_directory, "terraform.tfstate")): terraform = Terraform(working_dir=terraform_directory) cgid = extract_cgid_from_dir_name( os.path.basename(scenario_instance_dir_path)) destroy_retcode, destroy_stdout, destroy_stderr = terraform.destroy( capture_output=False, var={ "cgid": cgid, "cg_whitelist": list(), "profile": profile, "region": self.aws_region, }, no_color=IsNotFlagged, ) if destroy_retcode != 0: display_terraform_step_error("terraform destroy", destroy_retcode, destroy_stdout, destroy_stderr) return else: print( "\n[cloudgoat] terraform destroy completed with no error code." ) else: print( f"\nNo terraform.tfstate file was found in the scenario instance's" f' terraform directory, so "terraform destroy" will not be run.' ) # Scenario instance directory trashing. trash_dir = create_dir_if_nonexistent(self.base_dir, "trash") trashed_instance_path = os.path.join( trash_dir, os.path.basename(scenario_instance_dir_path)) shutil.move(scenario_instance_dir_path, trashed_instance_path) print( f"\nSuccessfully destroyed {instance_name}." f"\nScenario instance files have been moved to {trashed_instance_path}" ) return
def create_scenario(self, scenario_name_or_path, profile): scenario_name = normalize_scenario_name(scenario_name_or_path) scenario_dir = os.path.join(self.scenarios_dir, scenario_name) if not scenario_dir or not scenario_name or not os.path.exists( scenario_dir): if not scenario_name: return print( f"No recognized scenario name was entered. Did you mean one of" f" these?\n " + f"\n ".join(self.scenario_names)) else: return print( f"No scenario named {scenario_name} exists in the scenarios" f" directory. Did you mean one of these?" f"\n " + f"\n ".join(self.scenario_names)) if not os.path.exists(self.whitelist_path): cg_whitelist = self.configure_or_check_whitelist(auto=True) else: cg_whitelist = self.configure_or_check_whitelist() if not cg_whitelist: print( f"A valid whitelist.txt file must exist in the {self.base_dir}" f' directory before "create" may be used.') return # Create a scenario-instance folder in the project root directory. # This command should fail with an explanatory error message if a # scenario-instance of the same root name (i.e. without the CGID) already # exists. extant_dir = find_scenario_instance_dir(self.base_dir, scenario_name) if extant_dir is not None: destroy_and_recreate = input( f"You already have an instance of {scenario_name} deployed." f" Do you want to destroy and recreate it (y) or cancel (n)? [y/n]: " ) if destroy_and_recreate.strip().lower() == "y": self.destroy_scenario(scenario_name, profile, confirmed=True) else: instance_name = os.path.basename(extant_dir) print( f"\nCancelled destruction and recreation of {instance_name}.\n" ) return cgid = generate_cgid() scenario_instance_dir_path = os.path.join(self.base_dir, f"{scenario_name}_{cgid}") # Copy all the terraform files from the "/scenarios/scenario-name" folder # to the scenario-instance folder. source_dir_contents = os.path.join(scenario_dir, ".") shutil.copytree(source_dir_contents, scenario_instance_dir_path) if os.path.exists(os.path.join(scenario_instance_dir_path, "start.sh")): print(f"\nNow running {scenario_name}'s start.sh...") start_script_process = subprocess.Popen( ["sh", "start.sh"], cwd=scenario_instance_dir_path) start_script_process.wait() else: pass terraform = Terraform( working_dir=os.path.join(scenario_instance_dir_path, "terraform")) init_retcode, init_stdout, init_stderr = terraform.init( capture_output=False, no_color=IsNotFlagged) if init_retcode != 0: display_terraform_step_error("terraform init", init_retcode, init_stdout, init_stderr) return else: print( f"\n[cloudgoat] terraform init completed with no error code.") plan_retcode, plan_stdout, plan_stderr = terraform.plan( capture_output=False, var={ "cgid": cgid, "cg_whitelist": cg_whitelist, "profile": profile, "region": self.aws_region, }, no_color=IsNotFlagged, ) # For some reason, `python-terraform`'s `terraform init` returns "2" even # when it appears to succeed. For that reason, it will temporarily permit # retcode 2. if plan_retcode not in (0, 2): display_terraform_step_error("terraform plan", plan_retcode, plan_stdout, plan_stderr) return else: print( f"\n[cloudgoat] terraform plan completed with no error code.") apply_retcode, apply_stdout, apply_stderr = terraform.apply( capture_output=False, var={ "cgid": cgid, "cg_whitelist": cg_whitelist, "profile": profile, "region": self.aws_region, }, skip_plan=True, no_color=IsNotFlagged, ) if apply_retcode != 0: display_terraform_step_error("terraform apply", apply_retcode, apply_stdout, apply_stderr) return else: print( f"\n[cloudgoat] terraform apply completed with no error code.") # python-terraform uses the '-json' flag by default. # The documentation for `output` suggests using output_cmd to receive the # library's standard threeple return value. # Can't use capture_output here because we need to write stdout to a file. output_retcode, output_stdout, output_stderr = terraform.output_cmd() if output_retcode != 0: display_terraform_step_error("terraform output", output_retcode, output_stdout, output_stderr) return else: print( f"\n[cloudgoat] terraform output completed with no error code." ) # Within this output will be values that begin with "cloudgoat_output". # Each line of console output which contains this tag will be written into # a text file named "start.txt" in the scenario-instance folder. start_file_path = os.path.join(scenario_instance_dir_path, "start.txt") with open(start_file_path, "w") as start_file: for line in output_stdout.split("\n"): if line.count("cloudgoat_output") != 0: start_file.write(line + "\n") print( f"\n[cloudgoat] Output file written to:\n\n {start_file_path}\n" )
def test_normalize_scenario_name(self): # Edge cases self.assertEqual(normalize_scenario_name(""), "") self.assertEqual(normalize_scenario_name("/"), "") self.assertEqual(normalize_scenario_name("/////"), "") # Simple cases, fake scenario names self.assertEqual(normalize_scenario_name("test_a/"), "test_a") self.assertEqual(normalize_scenario_name("/test_b"), "test_b") self.assertEqual(normalize_scenario_name("test_a/test_b"), "test_b") self.assertEqual(normalize_scenario_name("/test_a/test_b"), "test_b") # "scenarios" directory self.assertEqual(normalize_scenario_name("scenarios"), "scenarios") self.assertEqual(normalize_scenario_name("scenarios/"), "scenarios") self.assertEqual(normalize_scenario_name("/scenarios"), "scenarios") self.assertEqual(normalize_scenario_name("test_a/scenarios"), "scenarios") self.assertEqual(normalize_scenario_name("scenarios/test_b"), "test_b") self.assertEqual(normalize_scenario_name("test_a/scenarios/test_b"), "test_b") # Real scenario names self.assertEqual(normalize_scenario_name("rce_web_app/"), "rce_web_app") self.assertEqual(normalize_scenario_name("/rce_web_app"), "rce_web_app") self.assertEqual(normalize_scenario_name("scenarios/rce_web_app"), "rce_web_app") self.assertEqual(normalize_scenario_name("/scenarios/rce_web_app"), "rce_web_app") # Long paths self.assertEqual( normalize_scenario_name("/long/path/scenarios/rce_web_app"), "rce_web_app") self.assertEqual( normalize_scenario_name("scenarios/rce_web_app/even/longer/path"), "rce_web_app", ) self.assertEqual( normalize_scenario_name( "/long/path/scenarios/rce_web_app/even/longer/path"), "rce_web_app", ) self.assertEqual( normalize_scenario_name( "/long/path/scenarios/not-a-real-scenario"), "not-a-real-scenario", ) self.assertEqual( normalize_scenario_name( "scenarios/not-a-real-scenario/even/longer/path"), "not-a-real-scenario", ) self.assertEqual( normalize_scenario_name( "/long/path/scenarios/not-a-real-scenario/even/longer/path"), "not-a-real-scenario", ) # Scenario instance paths self.assertEqual( normalize_scenario_name("codebuild_secrets_cgid0123456789"), "codebuild_secrets", ) self.assertEqual( normalize_scenario_name( "scenarios/codebuild_secrets_cgid0123456789"), "codebuild_secrets", ) self.assertEqual( normalize_scenario_name( "codebuild_secrets_cgid0123456789/scenarios"), "codebuild_secrets", ) self.assertEqual( normalize_scenario_name( "/long/path/scenarios/codebuild_secrets_cgid0123456789"), "codebuild_secrets", ) self.assertEqual( normalize_scenario_name( "scenarios/codebuild_secrets_cgid0123456789/even/longer/path"), "codebuild_secrets", ) self.assertEqual( normalize_scenario_name( "/long/path/scenarios/codebuild_secrets_cgid0123456789/even/longer/path" ), "codebuild_secrets", )