package aws import ( "fmt" "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/organizations/types" "github.com/aws/aws-sdk-go-v2/service/sts" "github.com/kyokomi/emoji" "github.com/sirupsen/logrus" ) type OrgModule struct { OrganizationsClient sdk.OrganizationsClientInterface Caller sts.GetCallerIdentityOutput AWSRegions []string AWSOutputType string AWSTableCols string Goroutines int AWSProfile string SkipAdminCheck bool WrapTable bool DescribeOrgOutput *types.Organization // Main module data Accounts []Account Orgs map[string]Org CommandCounter internal.CommandCounter // Used to store output data for pretty printing output internal.OutputData2 modLog *logrus.Entry } type Org struct { OrgId string MgmtAccount string ChildAccounts []Account } type Account struct { ProfileName string isManagementAccount bool Name string Id string Email string Arn string Status string OrgId string } func (m *OrgModule) PrintOrgAccounts(outputDirectory string, verbosity int) { // These struct values are used by the output module var err error m.output.Verbosity = verbosity m.output.Directory = outputDirectory m.output.CallingModule = "org" m.modLog = internal.TxtLog.WithFields(logrus.Fields{ "module": m.output.CallingModule, }) if m.AWSProfile == "" { m.AWSProfile = internal.BuildAWSPath(m.Caller) } fmt.Printf("[%s][%s] Checking if account %s is the management account in an organization.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), aws.ToString(m.Caller.Account)) m.DescribeOrgOutput, err = sdk.CachedOrganizationsDescribeOrganization(m.OrganizationsClient, aws.ToString(m.Caller.Account)) if err != nil { m.modLog.Errorf("Failed to describe organization: %s", err) return } if err != nil { m.modLog.Errorf("Failed to describe organization: %s", err) fmt.Printf("[%s][%s] Account %s is either not associated with an organization, or you do not have the organizations:DescribeOrganization permission.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), aws.ToString(m.Caller.Account)) } else { if m.IsManagementAccount(m.DescribeOrgOutput, aws.ToString(m.Caller.Account)) { m.addOrgAccounts() } else { m.addOrgAccount() //fmt.Printf("[%s][%s] Account %s is not the management account in an organization %s.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), aws.ToString(m.Caller.Account)) } m.output.Headers = []string{ "Account", "Name", "ID", "isManagementAccount?", "Status", "Email", } // 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 != "" { // If the user specified wide as the output format, use these columns. // 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, ",") } else if m.AWSOutputType == "wide" { tableCols = []string{ "Account", "Name", "ID", "isManagementAccount?", "Status", "Email", } // Otherwise, use the default columns. } else { tableCols = []string{ "Name", "ID", "isManagementAccount?", "Status", "Email", } } // Table rows for i := range m.Accounts { m.output.Body = append( m.output.Body, []string{ aws.ToString(m.Caller.Account), m.Accounts[i].Name, m.Accounts[i].Id, strconv.FormatBool(m.Accounts[i].isManagementAccount), m.Accounts[i].Status, m.Accounts[i].Email, }, ) } if len(m.output.Body) > 0 { m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account))) o := internal.OutputClient{ Verbosity: verbosity, CallingModule: m.output.CallingModule, Table: internal.TableClient{ Wrap: m.WrapTable, }, } 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] %d accounts found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), len(m.output.Body)) 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 *OrgModule) ProcessMultipleAccounts(AWSProfiles []string, version string) { var seenOrgs []string 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, "tasks") //create a channel to receive the objects dataReceiver := make(chan Account) // Create a channel to signal to stop receiverDone := make(chan bool) // Create a channel to signal to stop go m.Receiver(dataReceiver, receiverDone) for _, profile := range AWSProfiles { wg.Add(1) m.CommandCounter.Pending++ go m.FindMgmtAccounts(profile, version, wg, semaphore, dataReceiver) wg.Wait() // Send a message to the spinner goroutine to close the channel and stop spinnerDone <- true <-spinnerDone receiverDone <- true <-receiverDone orgId := aws.ToString(m.DescribeOrgOutput.Id) // if this orgId is not in seenOrgs, add it to seenOrgs if !internal.Contains(orgId, seenOrgs) { seenOrgs = append(seenOrgs, orgId) // add this org to the Orgs map Org := Org{ OrgId: orgId, } m.Orgs[orgId] = Org } else { continue } isMgmtAccount := m.IsManagementAccount(m.DescribeOrgOutput, aws.ToString(m.Caller.Account)) if isMgmtAccount { mgmtAccount := aws.ToString(m.Caller.Account) fmt.Printf("[%s] Found an Organization Management Account: %s\n", cyan(emoji.Sprintf(":fox:cloudfox v%s :fox:", version)), mgmtAccount) //mgmtAccounts[mgmtAccount] = append(mgmtAccounts[mgmtAccount], profile) } } } func (m *OrgModule) Receiver(receiver chan Account, receiverDone chan bool) { defer close(receiverDone) for { select { case data := <-receiver: m.Accounts = append(m.Accounts, data) case <-receiverDone: receiverDone <- true return } } } func (m *OrgModule) FindMgmtAccounts(profile string, version string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Account) { var err error DescribeOrganization, err := sdk.CachedOrganizationsDescribeOrganization(m.OrganizationsClient, aws.ToString(m.Caller.Account)) if err != nil { m.modLog.Errorf("Failed to describe organization: %s", err) m.CommandCounter.Error++ return } dataReceiver <- Account{ isManagementAccount: m.IsManagementAccount(DescribeOrganization, aws.ToString(m.Caller.Account)), Name: aws.ToString(m.Caller.Account), Id: aws.ToString(m.Caller.Account), Status: "ACTIVE", Email: aws.ToString(m.Caller.Account), OrgId: aws.ToString(DescribeOrganization.Id), Arn: aws.ToString(m.Caller.Account), } } func (m *OrgModule) IsManagementAccount(Organization *types.Organization, account string) bool { // Check if the account is the management account // https://docs.aws.amazon.com/organizations/latest/APIReference/API_DescribeOrganization.html if aws.ToString(Organization.MasterAccountId) == account { return true } return false } func (m *OrgModule) addOrgAccounts() { accounts, err := sdk.CachedOrganizationsListAccounts(m.OrganizationsClient, aws.ToString(m.Caller.Account)) if err != nil { m.modLog.Error(err.Error()) m.CommandCounter.Error++ return } for _, account := range accounts { m.Accounts = append(m.Accounts, Account{ isManagementAccount: m.IsManagementAccount(m.DescribeOrgOutput, aws.ToString(account.Id)), Name: aws.ToString(account.Name), Id: aws.ToString(account.Id), Email: aws.ToString(account.Email), Arn: aws.ToString(account.Arn), Status: string(account.Status), }) } } func (m *OrgModule) addOrgAccount() { DescribeOrganization, err := sdk.CachedOrganizationsDescribeOrganization(m.OrganizationsClient, aws.ToString(m.Caller.Account)) if err != nil { sharedLogger.Errorf("Failed to describe organization: %s", err) m.CommandCounter.Error++ return } m.Accounts = append(m.Accounts, Account{ isManagementAccount: true, Name: "Mgmt Account", Id: aws.ToString(DescribeOrganization.MasterAccountId), Email: aws.ToString(DescribeOrganization.MasterAccountEmail), Arn: aws.ToString(DescribeOrganization.MasterAccountArn), Status: "ACTIVE", }) m.Accounts = append(m.Accounts, Account{ isManagementAccount: false, Name: "This account", Id: aws.ToString(m.Caller.Account), Email: "Unknown", Arn: aws.ToString(m.Caller.Arn), Status: "ACTIVE", }) } func (m *OrgModule) IsCallerAccountPartOfAnOrg() bool { DescribeOrgOutput, err := sdk.CachedOrganizationsDescribeOrganization(m.OrganizationsClient, aws.ToString(m.Caller.Account)) if err != nil { sharedLogger.Errorf("Failed to describe organization: %s", err) return false } else { m.DescribeOrgOutput = DescribeOrgOutput } return true }