Files
cloudfox/aws/cloudformation.go

332 lines
9.9 KiB
Go

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
ServiceMap *awsservicemap.AwsServiceMap // Shared service map to avoid repeated HTTP requests
// 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()
// Use shared ServiceMap instance if provided, otherwise create a new one
servicemap := m.ServiceMap
if servicemap == nil {
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,
}
}
}