mirror of
https://github.com/BishopFox/cloudfox.git
synced 2026-11-09 17:07:17 -10:00
326 lines
10 KiB
Go
326 lines
10 KiB
Go
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
|
|
}
|