package aws import ( "fmt" "os" "path/filepath" "strconv" "strings" "sync" "github.com/BishopFox/cloudfox/aws/sdk" "github.com/BishopFox/cloudfox/internal" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/cloudformation/types" "github.com/aws/aws-sdk-go-v2/service/sts" "github.com/bishopfox/awsservicemap" "github.com/sirupsen/logrus" ) type CloudformationModule struct { // General configuration data CloudFormationClient sdk.CloudFormationClientInterface Caller sts.GetCallerIdentityOutput AWSRegions []string Goroutines int AWSProfile string WrapTable bool AWSOutputType string AWSTableCols string // Main module data CFStacks []CFStack CommandCounter internal.CommandCounter // Used to store output data for pretty printing output internal.OutputData2 modLog *logrus.Entry } type CFStack struct { AWSService string Region string Name string Role string Outputs []types.Output Parameters []types.Parameter Template string } func (m *CloudformationModule) PrintCloudformationStacks(outputDirectory string, verbosity int) { // These struct values are used by the output module m.output.Verbosity = verbosity m.output.Directory = outputDirectory m.output.CallingModule = "cloudformation" m.modLog = internal.TxtLog.WithFields(logrus.Fields{ "module": m.output.CallingModule, }) if m.AWSProfile == "" { m.AWSProfile = internal.BuildAWSPath(m.Caller) } fmt.Printf("[%s][%s] Enumerating cloudformation stacks for account %s.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), aws.ToString(m.Caller.Account)) wg := new(sync.WaitGroup) semaphore := make(chan struct{}, m.Goroutines) // Create a channel to signal the spinner aka task status goroutine to finish spinnerDone := make(chan bool) //fire up the the task status spinner/updated go internal.SpinUntil(m.output.CallingModule, &m.CommandCounter, spinnerDone, "regions") //create a channel to receive the objects dataReceiver := make(chan CFStack) // Create a channel to signal to stop receiverDone := make(chan bool) go m.Receiver(dataReceiver, receiverDone) for _, region := range m.AWSRegions { wg.Add(1) m.CommandCounter.Pending++ go m.executeChecks(region, wg, semaphore, dataReceiver) } wg.Wait() //time.Sleep(time.Second * 2) // Send a message to the spinner goroutine to close the channel and stop spinnerDone <- true <-spinnerDone receiverDone <- true <-receiverDone // add - if struct is not empty do this. otherwise, dont write anything. m.output.Headers = []string{ "Account", "Service", "Region", "Name", "Role", "Parameters", "Outputs", } // Table rows for i := range m.CFStacks { var hasParameters string var hasOutputs string if m.CFStacks[i].Parameters != nil { hasParameters = "Y" } else { hasParameters = "N" } if m.CFStacks[i].Outputs != nil { hasOutputs = "Y" } else { hasOutputs = "N" } m.output.Body = append( m.output.Body, []string{ aws.ToString(m.Caller.Account), m.CFStacks[i].AWSService, m.CFStacks[i].Region, m.CFStacks[i].Name, m.CFStacks[i].Role, hasParameters, hasOutputs, }, ) } if len(m.output.Body) > 0 { o := internal.OutputClient{ Verbosity: verbosity, CallingModule: m.output.CallingModule, Table: internal.TableClient{ Wrap: m.WrapTable, }, } // If the user specified table columns, use those. // If the user specified -o wide, use the wide default cols for this module. // Otherwise, use the hardcoded default cols for this module. var tableCols []string // If the user specified table columns, use those. if m.AWSTableCols != "" { // remove any spaces between any commas and the first letter after the commas m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",") m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",") tableCols = strings.Split(m.AWSTableCols, ",") // If the user specified wide as the output format, use these columns. } else if m.AWSOutputType == "wide" { tableCols = []string{ "Account", "Service", "Region", "Name", "Role", "Parameters", "Outputs", } // Otherwise, use the default columns. } else { tableCols = []string{ "Service", "Region", "Name", "Role", } } o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{ Header: m.output.Headers, Body: m.output.Body, TableCols: tableCols, Name: m.output.CallingModule, }) o.PrefixIdentifier = m.AWSProfile o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account))) o.WriteFullOutput(o.Table.TableFiles, nil) m.writeLoot(o.Table.DirectoryName, verbosity) fmt.Printf("[%s][%s] %s cloudformation stacks found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(m.output.Body))) } else { fmt.Printf("[%s][%s] No cloudformation stacks found, skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile)) } fmt.Printf("[%s][%s] For context and next steps: https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#%s\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), m.output.CallingModule) } func (m *CloudformationModule) executeChecks(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan CFStack) { defer wg.Done() servicemap := &awsservicemap.AwsServiceMap{ JsonFileSource: "DOWNLOAD_FROM_AWS", } serviceRegions, err := servicemap.GetRegionsForService("cloudformation") if err != nil { m.modLog.Error(err.Error()) } for _, serviceRegion := range serviceRegions { if r == serviceRegion { m.CommandCounter.Total++ wg.Add(1) m.createCFStackRowsPerRegion(r, wg, semaphore, dataReceiver) } } } func (m *CloudformationModule) Receiver(receiver chan CFStack, receiverDone chan bool) { defer close(receiverDone) for { select { case data := <-receiver: m.CFStacks = append(m.CFStacks, data) case <-receiverDone: receiverDone <- true return } } } func (m *CloudformationModule) writeLoot(outputDirectory string, verbosity int) { path := filepath.Join(outputDirectory, "loot") err := os.MkdirAll(path, os.ModePerm) if err != nil { m.modLog.Error(err.Error()) m.CommandCounter.Error++ } pullFile := filepath.Join(path, "cloudformation-data.txt") var out string out = out + fmt.Sprintln("#############################################") out = out + fmt.Sprintln("# Look for secrets. Use something like trufflehog") out = out + fmt.Sprintln("#############################################") out = out + fmt.Sprintln("") for _, stack := range m.CFStacks { out = out + fmt.Sprintln("=============================================") out = out + fmt.Sprintf("Stack Name: %s\n\n", stack.Name) out = out + fmt.Sprintln("Stack Outputs:") out = out + fmt.Sprintln() for _, output := range stack.Outputs { outputDescription := aws.ToString(output.Description) outputExport := aws.ToString(output.ExportName) outputKey := aws.ToString(output.OutputKey) outputValue := aws.ToString(output.OutputValue) out = out + fmt.Sprintf("Stack Output Description: %s\n", outputDescription) out = out + fmt.Sprintf("Stack Output Name: %s\n", outputExport) out = out + fmt.Sprintf("Stack Output Key: %s\n", outputKey) out = out + fmt.Sprintf("Stack Output Value: %s\n\n", outputValue) } out = out + "Stack Parameters:\n\n" for _, param := range stack.Parameters { paramKey := aws.ToString(param.ParameterKey) paramValue := aws.ToString(param.ParameterValue) out = out + fmt.Sprintf("Stack Parameter Key: %s\n", paramKey) out = out + fmt.Sprintf("Stack Parameter Value: %s\n\n", paramValue) } //out = out + fmt.Sprintf("Stack Parameters:\n %s\n", stack.Parameters) out = out + fmt.Sprintf("Stack Template:\n %s\n", stack.Template) out = out + fmt.Sprintln("=============================================") } err = os.WriteFile(pullFile, []byte(out), 0644) if err != nil { m.modLog.Error(err.Error()) m.CommandCounter.Error++ } if verbosity > 2 { fmt.Println() fmt.Printf("[%s][%s] %s \n", cyan(m.output.CallingModule), cyan(m.AWSProfile), green("Look for secrets. Use something like trufflehog")) fmt.Print(out) fmt.Printf("[%s][%s] %s \n", cyan(m.output.CallingModule), cyan(m.AWSProfile), green("Look for secrets. Use something like trufflehog")) fmt.Printf("[%s][%s] %s \n\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), green("End of loot file.")) } fmt.Printf("[%s][%s] Loot written to [%s]\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), pullFile) } func (m *CloudformationModule) createCFStackRowsPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan CFStack) { defer func() { m.CommandCounter.Executing-- m.CommandCounter.Complete++ wg.Done() }() semaphore <- struct{}{} defer func() { <-semaphore }() var stackTemplateBody string = "" DescribeStacks, err := sdk.CachedCloudFormationDescribeStacks(m.CloudFormationClient, aws.ToString(m.Caller.Account), r) if err != nil { m.modLog.Error(err.Error()) m.CommandCounter.Error++ } for _, stack := range DescribeStacks { stackName := aws.ToString(stack.StackName) stackRole := aws.ToString(stack.RoleARN) stackOutputs := stack.Outputs stackParameters := stack.Parameters stackTemplateBody, err = sdk.CachedCloudFormationGetTemplate(m.CloudFormationClient, aws.ToString(m.Caller.Account), r, stackName) if err != nil { m.modLog.Error(err.Error()) m.CommandCounter.Error++ } dataReceiver <- CFStack{ AWSService: "cloudformation", Name: stackName, Region: r, Role: stackRole, Outputs: stackOutputs, Parameters: stackParameters, Template: stackTemplateBody, } } }