diff --git a/pkg/cmd/cmd.go b/pkg/cmd/cmd.go index ae113f8..1b1d08c 100644 --- a/pkg/cmd/cmd.go +++ b/pkg/cmd/cmd.go @@ -5,7 +5,6 @@ import ( "fmt" "log" "os" - "path/filepath" "github.com/SecurityRunners/CloudCommotion/pkg/config" "github.com/SecurityRunners/CloudCommotion/pkg/templates" @@ -14,271 +13,304 @@ import ( "github.com/spf13/cobra" ) -// ASCII Banner -var appName = "Cloud Commotion" -var appVersion = "v0.0.2" -var banner = figure.NewFigure("Cloud Commotion", "larry3d", true).String() -var asciiBanner = fmt.Sprintf("%s\nby Security Runners %s\n", banner, appVersion) - -// Global flags -var terraform_dir string -var terraform_loc string -var resource_name string -var sensitive_content string -var config_file string -var region string -var debug bool +type RuntimeFlags struct { + // Path of the scenario to run. + terraformDir string + // (unused) The location of the Terraform binary. + terraformLoc string + // (unused) The name of the resource. + resourceName string + // (unused) Flag to be discovered by the incident responder. + sensitiveContent string + // Path to the configuration file to use. + configFile string + // Region to manage cloud resources in. + region string + // Whether or not to print debugging information for CloudCommotion. + debug bool + // Git URL to clone Terraform templates from. + repoURL string + + // This is technically not a flag, but we want to reduce the number + // of globals we have. + config *config.Config +} + +// Checks if the set RuntimeFlags.terraformDir directory exists, returns true if it does +// and false if it does not. +func (r *RuntimeFlags) terraformDirExists() bool { + if _, err := os.Stat(r.terraformDir); os.IsNotExist(err) { + return false + } else { + return true + } +} + +// Get the terraform region as specified from the CLI, otherwise default to +// the region specified in the config file. +func (r *RuntimeFlags) terraformRegion() string { + if r.region != "" { + return r.region + } else { + return GetConfig().Region + } +} + +var runtimeFlags = RuntimeFlags{} + +// Retrieve a configuration instance from pkg/config/config.go +// +// Everything that needs a reference to the config should prefer this +// function instead of config.GetConfig as this function caches the +// config instance to avoid re-reading and parsing the files after +// each call. +func GetConfig() config.Config { + if (runtimeFlags.config != nil) { + return *runtimeFlags.config + } + + // Retrieve configuration from ~/.commotion/config and create if does not exist + runtimeFlags.config = config.GetConfig(runtimeFlags.configFile) + return *runtimeFlags.config +} + +// Prints a fancy banner to standard out. +func printAsciiBanner() { + var appName = "Cloud Commotion" + var appVersion = "v0.0.2" + var fontName = "larry3d" + var banner = figure.NewFigure(appName, fontName, true).String() + var asciiBanner = fmt.Sprintf("%s\nby Security Runners %s\n", banner, appVersion) + + fmt.Println(asciiBanner) +} + +// Download Terraform templates if the directory does not exist +func ensureTerraformDirExists() { + if runtimeFlags.terraformDirExists() { + return + } + + commotionRoot := config.GetCommotionDirectory() + + err := templates.DownloadTerraformTemplates(commotionRoot, runtimeFlags.repoURL, runtimeFlags.debug) + if err != nil { + log.Fatal(err) + } +} + +// The implementation for ./CloudCommotion +func rootCmdRun(cmd *cobra.Command, args []string) { + printAsciiBanner() + + cmd.Help() +} var rootCmd = &cobra.Command{ Use: "cloudcommotion", Short: "A CLI tool for causing commotion within your cloud environment", Long: "Cloud Commotion purposefully creates resources that should set off alarm bells within your environment to help you prepare for an incident.", Example: `cloudcommotion -h`, - Run: func(cmd *cobra.Command, args []string) { - fmt.Println(asciiBanner) - cmd.Help() - }, + Run: rootCmdRun, +} + +// The implementation for ./CloudCommotion plan +func planCmdRun(cmd *cobra.Command, args []string) { + printAsciiBanner() + + log.Println("Starting commotion planning, prepare for the inevitable!") + + ensureTerraformDirExists() + + cfg := GetConfig() + + for _, mod := range cfg.Module { + tfdir := mod.TfDir() + + // Merge config.variables, module.variables, and configured region + tfvars := config.MergeVariables(cfg.Variables, mod.Variables) + tfvars["region"] = runtimeFlags.terraformRegion() + + if runtimeFlags.debug { + log.Printf("Terraform variables: %v\n", tfvars) + log.Printf("Terraform directory: %s\n", tfdir) + log.Printf("Terraform mod: %s\n", mod.TerraformDir) + } + + // Plan the infrastructure to be created + log.Println("Planning commotion infrastructure for: " + mod.Name) + plan := terraform.PlanTerraform(tfvars, tfdir, runtimeFlags.debug) + + // Log out the results + if plan { + log.Println("Commotion infrastructure has been planned successfully: " + mod.Name) + } else { + log.Println("No changes detected for: " + mod.Name) + } + } + + log.Println("Completed! Now lets see how good your monitoring systems are...") } var planCmd = &cobra.Command{ Use: "plan", Short: "Plan infrastructure to be created through Cloud Commotion.", - Long: "Run a terraform plan on infrastructure to be created through cloud commotion", - Run: func(cmd *cobra.Command, args []string) { - // Welcome banner - fmt.Println(asciiBanner) - log.Println("Starting commotion planning, prepare for the inveitable!") - - // Check if terraform module directory exists - // If not, download the templates - terraform_dir := filepath.Join(os.Getenv("HOME"), ".commotion", "terraform") - if _, err := os.Stat(terraform_dir); os.IsNotExist(err) { - // Define repoURL if not set - var repoURL string - // Download the terraform templates - err := templates.DownloadTerraformTemplates(repoURL, debug) - if err != nil { - log.Fatal(err) - } + Long: "Run a Terraform plan on infrastructure to be created through cloud commotion", + Run: planCmdRun, +} + +// The implementation for ./CloudCommotion apply +func applyCmdRun(cmd *cobra.Command, args []string) { + printAsciiBanner() + + log.Println("Starting commotion engagement, buckle your seatbelt!") + + ensureTerraformDirExists() + + cfg := GetConfig() + + for _, mod := range cfg.Module { + tfdir := mod.TfDir() + + // Merge config.variables, module.variables, and configured region + tfvars := config.MergeVariables(cfg.Variables, mod.Variables) + tfvars["region"] = runtimeFlags.terraformRegion() + + if runtimeFlags.debug { + log.Printf("Terraform variables: %v\n", tfvars) + log.Printf("Terraform directory: %s\n", tfdir) + log.Printf("Terraform mod: %s\n", mod.TerraformDir) } - var tfdir string - for _, mod := range config.GetConfig(config_file).Module { - // Get the tf module directory - if mod.TerraformLoc == "local" { - tfdir = mod.TerraformDir - } else { - // Default to remote if not set - tfdir = filepath.Join(os.Getenv("HOME"), ".commotion", mod.TerraformDir) - } - - // Merge config.variables with module.variables - tfvars := config.MergeVariables(config.GetConfig(config_file).Variables, mod.Variables) - - // If region flag is set, use that region - if region != "" { - tfvars["region"] = region - } else { - tfvars["region"] = config.GetConfig(config_file).Region - } - - // If debug flag in args is set, print the terraform variables - if debug { - log.Println("Terraform variables: " + fmt.Sprintf("%v", tfvars)) - log.Println("Terraform directory: " + tfdir) - } - - // Plan the infrastructure to be created - log.Println("Planning commotion infrastructure for: " + mod.Name) - plan := terraform.PlanTerraform(tfvars, tfdir, debug) - - // Log out the results - if plan { - log.Println("Commotion infrastructure has been planned successfully: " + mod.Name) - } else { - log.Println("No changes detected for: " + mod.Name) - } + // Plan the infrastructure to be created + log.Println("Planning and applying commotion infrastructure for: " + mod.Name) + plan := terraform.PlanTerraform(tfvars, tfdir, runtimeFlags.debug) + + // Apply only if plan detects changes to be made + if plan { + terraform.ApplyTerraform(tfvars, tfdir, runtimeFlags.debug) } - log.Println("Completed! Now lets see how good your monitoring systems are...") - }, + // Retrieve the commotion asset + output := terraform.OutputTerraform(tfdir) + + // Extract the exposed_asset output variable + exposed_asset := output["exposed_asset"].Value + raw := json.RawMessage(exposed_asset) + asset, err := json.Marshal(&raw) + if err != nil { + panic(err) + } + + // Success + log.Println("Commotion infrastructure has been applied/updated successfully: " + string(asset)) + } + + log.Println("Completed! Now lets see how good your monitoring systems are...") } var applyCmd = &cobra.Command{ Use: "apply", Short: "Executes individual modules", Long: "Execute commotion modules located within the terraform directory", - Run: func(cmd *cobra.Command, args []string) { - // Welcome banner for the application - fmt.Println(asciiBanner) - log.Println("Starting commotion engagement, buckle your seatbelt!") - - // Check if terraform module directory exists - // If not, download the templates - terraform_dir := filepath.Join(os.Getenv("HOME"), ".commotion", "terraform") - if _, err := os.Stat(terraform_dir); os.IsNotExist(err) { - // Define repoURL if not set - var repoURL string - // Download the terraform templates - err := templates.DownloadTerraformTemplates(repoURL, debug) - if err != nil { - log.Fatal(err) - } - } + Run: applyCmdRun, +} - var tfdir string - for _, mod := range config.GetConfig(config_file).Module { - // Get the tf module directory - if mod.TerraformLoc == "local" { - tfdir = mod.TerraformDir - } else { - // Default to remote if not set - tfdir = filepath.Join(os.Getenv("HOME"), ".commotion", mod.TerraformDir) - } - - // Merge config.variables with module.variables - tfvars := config.MergeVariables(config.GetConfig(config_file).Variables, mod.Variables) - - // If region flag is set, use that region - if region != "" { - tfvars["region"] = region - } else { - tfvars["region"] = config.GetConfig(config_file).Region - } - - // If debug flag in args is set, print the terraform variables - if debug { - log.Println("Terraform variables: " + fmt.Sprintf("%v", tfvars)) - log.Println("Terraform directory: " + tfdir) - log.Println("Terraform mod:" + mod.TerraformDir) - } - - // Plan the infrastructure to be created - log.Println("Planning and applying commotion infrastructure for: " + mod.Name) - plan := terraform.PlanTerraform(tfvars, tfdir, debug) - - // Apply only if plan detects changes to be made - if plan { - terraform.ApplyTerraform(tfvars, tfdir, debug) - } - - // Retrieve the commotion asset - output := terraform.OutputTerraform(tfdir) - - // Extract the exposed_asset output variable - exposed_asset := output["exposed_asset"].Value - raw := json.RawMessage(exposed_asset) - asset, err := json.Marshal(&raw) - if err != nil { - panic(err) - } - - // Success - log.Println("Commotion infrastructure has been applied/updated successfully: " + string(asset)) - } +// The implementation for ./CloudCommotion update +func updateCmdRun(cmd *cobra.Command, args []string) { + printAsciiBanner() + + // Update the Terraform templates + commotionRoot := config.GetCommotionDirectory() - log.Println("Completed! Now lets see how good your monitoring systems are...") - }, + err := templates.UpdateTerraformTemplates(commotionRoot, runtimeFlags.repoURL, runtimeFlags.debug) + if err != nil { + log.Fatal(err) + } } var updateCmd = &cobra.Command{ Use: "update", - Long: "Update the terraform templates", - Run: func(cmd *cobra.Command, args []string) { - // Print banner - fmt.Println(asciiBanner) - - // Update the terraform templates - var repoURL string - err := templates.UpdateTerraformTemplates(repoURL, debug) - if err != nil { - log.Fatal(err) + Short: "Update the Terraform templates", + Long: "Update the Terraform templates", + Run: updateCmdRun, +} + +// The implementation for ./CloudCommotion destroy +func destroyCmdRun(cmd *cobra.Command, args []string) { + printAsciiBanner() + + cfg := GetConfig() + + // Loop through modules and destroy Terraform module + for _, mod := range cfg.Module { + tfdir := mod.TfDir() + + // Merge config.variables, module.variables, and configured region + tfvars := config.MergeVariables(cfg.Variables, mod.Variables) + tfvars["region"] = runtimeFlags.terraformRegion() + + if runtimeFlags.debug { + log.Printf("Terraform variables: %v\n", tfvars) + log.Printf("Terraform directory: %s\n", tfdir) + log.Printf("Terraform mod: %s\n", mod.TerraformDir) } - }, + + // Destroy the infrastructure + log.Println("Destroying commotion infrastructure for: " + mod.Name) + terraform.DestroyTerraform(tfvars, tfdir, runtimeFlags.debug) + } } var destroyCmd = &cobra.Command{ Use: "destroy", Short: "Destroy infrastructure created through Cloud Commotion.", Long: "Run a terraform destroy on infrastructure created through cloud commotion", - Run: func(cmd *cobra.Command, args []string) { - // Print banner - fmt.Println(asciiBanner) - - // Loop through modules and destroy terraform module - var tfdir string - for _, mod := range config.GetConfig(config_file).Module { - // Get the tf module directory - if mod.TerraformLoc == "local" { - tfdir = mod.TerraformDir - } else { - // Default to remote if not set - tfdir = filepath.Join(os.Getenv("HOME"), ".commotion", mod.TerraformDir) - } - // Merge variables - tfvars := config.MergeVariables(config.GetConfig(config_file).Variables, mod.Variables) - - // If region flag is set, use that region - if region != "" { - tfvars["region"] = region - } else { - tfvars["region"] = config.GetConfig(config_file).Region - } - - // If debug flag in args is set, print the terraform variables - if debug { - log.Println("Terraform variables: " + fmt.Sprintf("%v", tfvars)) - log.Println("Terraform directory: " + tfdir) - } - - // Destroy the infrastructure - log.Println("Destroying commotion infrastructure for: " + mod.Name) - terraform.DestroyTerraform(tfvars, tfdir, debug) - } - }, -} - -// Functino to retrieve the configuration from pkg/config/config.go -func GetConfig() config.Config { - // Retrieve configuration from ~/.commotion/config and create if does not exist - configStruct := config.GetConfig(config_file) - return *configStruct + Run: destroyCmdRun, } +// This defines the CLI argument flags that are passed to our global runtime cache. func init() { - // Region flag - rootCmd.PersistentFlags().StringVarP(®ion, "region", "r", "", "AWS region to deploy resources") - if region == "" { + rootCmd.PersistentFlags().StringVarP(&runtimeFlags.region, "region", "r", "", "AWS region to deploy resources") + if runtimeFlags.region == "" { region, ok := os.LookupEnv("AWS_REGION") - if !ok { - rootCmd.MarkFlagRequired("region") + runtimeFlags.region = region + if ok { + rootCmd.Flags().Set("region", runtimeFlags.region) } else { - rootCmd.Flags().Set("region", region) + rootCmd.MarkFlagRequired("region") } } - // Based on the value of provider(aws, azure, gcp) if the region is set to random, then set the region to a random region - if region == "random" { - // Use config.GetRandomRegion() to get a random region and process error - randregion, err := config.GetRandomRegion(config.GetConfig(config_file).Provider) + if runtimeFlags.region == "random" { + // Generate a random region based on the cloud provider specified in the config file + randregion, err := config.GetRandomRegion(GetConfig().Provider) if err != nil { log.Fatal(err) } - region = randregion + runtimeFlags.region = randregion + } + + rootCmd.PersistentFlags().BoolVarP(&runtimeFlags.debug, "debug", "d", false, "enable debug mode") + rootCmd.PersistentFlags().StringVarP(&runtimeFlags.configFile, "config", "c", "", "the config file to use") + rootCmd.PersistentFlags().StringVarP(&runtimeFlags.sensitiveContent, "flag", "f", "", "the flag to be discovered by the incident responder") + rootCmd.PersistentFlags().StringVarP(&runtimeFlags.resourceName, "resource_name", "a", "", "the name of the resource") + rootCmd.PersistentFlags().StringVarP(&runtimeFlags.terraformLoc, "terraform_loc", "l", "", "the location of the terraform binary") + rootCmd.Flags().StringVarP(&runtimeFlags.terraformDir, "terraform_dir", "t", "", "the scenario in which to run") + + // Always ensure a terraform directory is set + if runtimeFlags.terraformDir == "" { + runtimeFlags.terraformDir = config.GetRelativeToCommotionDirectory("terraform") } - // Global variables - rootCmd.PersistentFlags().BoolVarP(&debug, "debug", "d", false, "enable debug mode") - rootCmd.PersistentFlags().StringVarP(&config_file, "config", "c", "", "the config file to use") - rootCmd.PersistentFlags().StringVarP(&sensitive_content, "flag", "f", "", "the flag to be discovered by the incident responder") - rootCmd.PersistentFlags().StringVarP(&resource_name, "resource_name", "a", "", "the name of the resource") - rootCmd.PersistentFlags().StringVarP(&terraform_loc, "terraform_loc", "l", "", "the location of the terraform binary") - rootCmd.Flags().StringVarP(&terraform_dir, "terraform_dir", "t", "", "the scenario in which to run") - - // Variables for create cmd - // applyCmd.Flags().StringVarP(&resource_name, "resource_name", "a", "", "the name of the resource") - // applyCmd.Flags().StringVarP(&sensitive_content, "sensitive_content", "c", "", "the flag to be discovered by the incident responder") - // applyCmd.Flags().StringVarP(&terraform_dir, "terraform_dir", "t", "", "the scenario in which to create") + var defaultGitUrl = "git@github.com:SecurityRunners/CloudCommotion.git" + + rootCmd.PersistentFlags().StringVarP(&runtimeFlags.repoURL, "repo_url", "u", defaultGitUrl, "git repository to download Terraform templates from") + + // Always ensure a repository URL is set + if runtimeFlags.repoURL == "" { + runtimeFlags.repoURL = defaultGitUrl + } } // Execute executes the root command. diff --git a/pkg/config/config.go b/pkg/config/config.go index 2dca486..a87918e 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -27,6 +27,27 @@ type Module struct { Variables map[string]interface{} `yaml:"variables"` } +// Get the absolute path to the root commotion directory. +func GetCommotionDirectory() string { + return filepath.Join(os.Getenv("HOME"), ".commotion") +} + +// Returns the absolute path to ``name`` relative to the default commotion +// install directory. +func GetRelativeToCommotionDirectory(name string) string { + return filepath.Join(os.Getenv("HOME"), ".commotion", name) +} + +// Get the tf module directory +func (mod Module) TfDir() string { + if mod.TerraformLoc == "local" { + return mod.TerraformDir + } else { + // Default to remote if not set + return GetRelativeToCommotionDirectory(mod.TerraformDir) + } +} + func GetConfig(configfile string) *Config { var configFilePath string diff --git a/pkg/templates/template.go b/pkg/templates/template.go index aec3b22..0005029 100644 --- a/pkg/templates/template.go +++ b/pkg/templates/template.go @@ -6,20 +6,11 @@ import ( "os" "os/exec" "path/filepath" + "strings" ) -var repoURL string - -// DownloadTerraformTemplates downloads Terraform templates from a remote GitHub repository. -func DownloadTerraformTemplates(repoURL string, debug bool) error { - // Define repoURL if not set - if repoURL == "" { - repoURL = "git@github.com:SecurityRunners/CloudCommotion.git" - } - - // Define the target directory where Terraform templates will be stored - targetDir := filepath.Join(os.Getenv("HOME"), ".commotion") - +// Download Terraform templates from a remote GitHub repository in to commotion root. +func DownloadTerraformTemplates(targetDir string, repoURL string, debug bool) error { // Check if the target directory already exists; if not, create it if _, err := os.Stat(targetDir); os.IsNotExist(err) { err := os.MkdirAll(targetDir, 0755) @@ -49,14 +40,14 @@ func DownloadTerraformTemplates(repoURL string, debug bool) error { return nil } -// UpdateTerraformTemplates updates Terraform templates in the target directory. -func UpdateTerraformTemplates(repoURL string, debug bool) error { - // Define the target directory where Terraform templates are stored - targetDir := filepath.Join(os.Getenv("HOME"), ".commotion") - +// Run git pull within commotion root to download new Terraform templates. +// +// This function will implicitly run DownloadTerraformTemplates() if the +// commotion root directory does not exist. +func UpdateTerraformTemplates(targetDir string, repoURL string, debug bool) error { // Check if the target directory exists if _, err := os.Stat(targetDir); os.IsNotExist(err) { - DownloadTerraformTemplates("", debug) + DownloadTerraformTemplates(targetDir, repoURL, debug) return nil } @@ -71,7 +62,7 @@ func UpdateTerraformTemplates(repoURL string, debug bool) error { // Debug logging if debug { - log.Println("Running command: " + fmt.Sprintf("%v", cmd.Args)) + log.Printf("Running command %s in %s.\n", strings.Join(cmd.Args, " "), targetDir) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr } @@ -96,58 +87,59 @@ func CleanupTerraformTemplatesDirectory(targetDir string, debug bool) error { return fmt.Errorf("failed to read directory: %v", err) } - // Initialize a flag to track if config.yml was moved - configMoved := false - // Remove all files and directories except "terraform" for _, entry := range entries { - if entry.Name() != "terraform" { - // Preserve the config file - if entry.Name() == "config" || entry.Name() == "config.yml" { - configSourcePath := filepath.Join(targetDir, "config", "config.yml") - configDestPath := filepath.Join(targetDir, "config.yml") - - if entry.Name() == "config" { - if err := os.Rename(configSourcePath, configDestPath); err != nil { - log.Printf("failed to move config.yml: %v", err) - } else { - if debug { - log.Println("config.yml has been moved to the target directory.") - } - } - - // remove the config directory - err := os.RemoveAll(filepath.Join(targetDir, "config")) - if err != nil { - log.Printf("failed to remove directory %s: %v", filepath.Join(targetDir, "config"), err) - } - } - - continue + if entry.Name() == "terraform" { + continue + } + + // Preserve the .git directory + if entry.Name() == ".git" { + continue + } + + // Preserve the config file + if entry.Name() == "config.yml" { + continue + } + + // Pull the config file from the config directory, then remove the config directory + if entry.Name() == "config" { + configSourcePath := filepath.Join(targetDir, "config", "config.yml") + configDestPath := filepath.Join(targetDir, "config.yml") + + if err := os.Rename(configSourcePath, configDestPath); err != nil { + log.Printf("failed to move config.yml: %v", err) + } else if debug { + log.Printf("config.yml has been moved to %s.", configDestPath) } - // Preserve the .git directory - if entry.Name() == ".git" { - continue + // remove the config directory + err := os.RemoveAll(filepath.Join(targetDir, "config")) + if err != nil { + log.Printf("failed to remove directory %s: %v", filepath.Join(targetDir, "config"), err) } - entryPath := filepath.Join(targetDir, entry.Name()) - if entry.IsDir() { - err := os.RemoveAll(entryPath) - if debug { - log.Println("Removing directory: " + entryPath) - } - if err != nil { - log.Printf("failed to remove directory %s: %v", entryPath, err) - } - } else { - err := os.Remove(entryPath) - if debug { - log.Println("Removing file: " + entryPath) - } - if err != nil { - log.Printf("failed to remove file %s: %v", entryPath, err) - } + continue + } + + // Remove everything else + entryPath := filepath.Join(targetDir, entry.Name()) + if entry.IsDir() { + err := os.RemoveAll(entryPath) + if debug { + log.Println("Removing directory: " + entryPath) + } + if err != nil { + log.Printf("failed to remove directory %s: %v", entryPath, err) + } + } else { + err := os.Remove(entryPath) + if debug { + log.Println("Removing file: " + entryPath) + } + if err != nil { + log.Printf("failed to remove file %s: %v", entryPath, err) } } } @@ -159,13 +151,7 @@ func CleanupTerraformTemplatesDirectory(targetDir string, debug bool) error { if _, err := os.Stat(configSourcePath); err == nil { if err := os.Rename(configSourcePath, configDestPath); err != nil { log.Printf("failed to move config.yml: %v", err) - } else { - configMoved = true - } - } - - if configMoved { - if debug { + } else if debug { log.Println("config.yml has been moved to the target directory.") } }