package internal import ( "bufio" "context" "encoding/gob" "encoding/json" "fmt" "os" "path/filepath" "regexp" "strings" "time" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/aws/retry" "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/credentials/stscreds" "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/sts" "github.com/aws/smithy-go/ptr" "github.com/bishopfox/awsservicemap" "github.com/kyokomi/emoji" "github.com/patrickmn/go-cache" "github.com/sirupsen/logrus" "github.com/spf13/afero" ) var ( TxtLoggerName = "root" TxtLog = TxtLogger() UtilsFs = afero.NewOsFs() credsMap = map[string]aws.Credentials{} ConfigMap = map[string]aws.Config{} ) type CloudFoxRunData struct { Profile string AccountID string OutputLocation string } func init() { gob.Register(aws.Config{}) gob.Register(sts.GetCallerIdentityOutput{}) gob.Register(CloudFoxRunData{}) } func InitializeCloudFoxRunData(AWSProfile string, version string, AwsMfaToken string, AWSOutputDirectory string) (CloudFoxRunData, error) { var runData CloudFoxRunData cacheDirectory := filepath.Join(AWSOutputDirectory, "cached-data", "aws") filename := filepath.Join(cacheDirectory, fmt.Sprintf("CloudFoxRunData-%s.json", AWSProfile)) if _, err := os.Stat(filename); err == nil { // unmarshall the data from the file into type CloudFoxRunData // Open the file (this is not actually needed if you use os.ReadFile, so you can skip this) file, err := os.Open(filename) if err != nil { return CloudFoxRunData{}, err } defer file.Close() // Read the file content jsonData, err := os.ReadFile(filename) if err != nil { return CloudFoxRunData{}, err } // Unmarshal jsonData into runData (make sure to pass a pointer to runData) err = json.Unmarshal(jsonData, &runData) if err != nil { return CloudFoxRunData{}, err } return runData, nil } CallerIdentity, err := AWSWhoami(AWSProfile, version, AwsMfaToken) if err != nil { return CloudFoxRunData{}, err } outputLocation := filepath.Join(AWSOutputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", AWSProfile, ptr.ToString(CallerIdentity.Account))) runData = CloudFoxRunData{ Profile: AWSProfile, AccountID: aws.ToString(CallerIdentity.Account), OutputLocation: outputLocation, } // Marshall the data to a file err = os.MkdirAll(cacheDirectory, 0755) if err != nil { return CloudFoxRunData{}, err } file, err := os.Create(filename) if err != nil { return CloudFoxRunData{}, err } defer file.Close() jsonData, err := json.Marshal(runData) if err != nil { return CloudFoxRunData{}, err } _, err = file.Write(jsonData) if err != nil { return CloudFoxRunData{}, err } return runData, nil } func AWSConfigFileLoader(AWSProfile string, version string, AwsMfaToken string) aws.Config { // Loads the AWS config file and returns a config object var cfg aws.Config var err error // cacheKey := fmt.Sprintf("AWSConfigFileLoader-%s", AWSProfile) // cached, found := Cache.Get(cacheKey) // if found { // cfg = cached.(aws.Config) // return cfg // } // Check if the profile is already in the config map. If not, load it and retrieve the credentials. If it is, return the cached config object // The AssumeRoleOptions below are used to pass the MFA token to the AssumeRole call (when applicable) if _, ok := ConfigMap[AWSProfile]; !ok { // Ensures the profile in the aws config file meets all requirements (valid keys and a region defined). I noticed some calls fail without a default region. if AwsMfaToken != "" { cfg, err = config.LoadDefaultConfig(context.TODO(), config.WithSharedConfigProfile(AWSProfile), config.WithDefaultRegion("us-east-1"), config.WithRetryer( func() aws.Retryer { return retry.AddWithMaxAttempts(retry.NewStandard(), 3) }), config.WithAssumeRoleCredentialOptions(func(options *stscreds.AssumeRoleOptions) { options.TokenProvider = func() (string, error) { return AwsMfaToken, nil } }), ) } else { cfg, err = config.LoadDefaultConfig(context.TODO(), config.WithSharedConfigProfile(AWSProfile), config.WithDefaultRegion("us-east-1"), config.WithRetryer( func() aws.Retryer { return retry.AddWithMaxAttempts(retry.NewStandard(), 3) }), config.WithAssumeRoleCredentialOptions(func(options *stscreds.AssumeRoleOptions) { options.TokenProvider = stscreds.StdinTokenProvider }), ) } if err != nil { //fmt.Println(err) if AWSProfile != "" { TxtLog.Println(err) fmt.Printf("[%s][%s] The specified profile [%s] does not exist or there was an error loading the credentials.\n", cyan(emoji.Sprintf(":fox:cloudfox v%s :fox:", version)), cyan(AWSProfile), AWSProfile) TxtLog.Fatalf("Could not retrieve the specified profile name %s", err) } else { fmt.Printf("[%s][%s] Error retrieving credentials from environment variables, or the instance metadata service.\n", cyan(emoji.Sprintf(":fox:cloudfox v%s :fox:", version)), cyan(AWSProfile)) TxtLog.Fatalf("Error retrieving credentials from environment variables, or the instance metadata service.\n", cyan(emoji.Sprintf(":fox:cloudfox v%s :fox:", version)), cyan(AWSProfile)) } //os.Exit(1) } _, err := cfg.Credentials.Retrieve(context.TODO()) if err != nil { fmt.Printf("[%s][%s] Error retrieving credentials from environment variables, or the instance metadata service.\n", cyan(emoji.Sprintf(":fox:cloudfox v%s :fox:", version)), cyan(AWSProfile)) } else { // update the config map with the new config for future lookups ConfigMap[AWSProfile] = cfg //return the config object for this first iteration //Cache.Set(cacheKey, cfg, cache.DefaultExpiration) return cfg } } else { //fmt.Println("Using cached config") cfg = ConfigMap[AWSProfile] return cfg } //Cache.Set(cacheKey, cfg, cache.DefaultExpiration) return cfg } func AWSWhoami(awsProfile string, version string, AwsMfaToken string) (*sts.GetCallerIdentityOutput, error) { cacheKey := fmt.Sprintf("sts-getCallerIdentity-%s", awsProfile) if cached, found := Cache.Get(cacheKey); found { // Correct type assertion: assert the type, not a variable. if cachedValue, ok := cached.(*sts.GetCallerIdentityOutput); ok { return cachedValue, nil } // Handle the case where type assertion fails, if necessary. } // Connects to STS and checks caller identity. Same as running "aws sts get-caller-identity" //fmt.Printf("[%s] Retrieving caller's identity\n", cyan(emoji.Sprintf(":fox:cloudfox v%s :fox:", version))) STSService := sts.NewFromConfig(AWSConfigFileLoader(awsProfile, version, AwsMfaToken)) CallerIdentity, err := STSService.GetCallerIdentity(context.TODO(), &sts.GetCallerIdentityInput{}) if err != nil { fmt.Printf("[%s][%s] Could not get caller's identity\n\nError: %s\n\n", cyan(emoji.Sprintf(":fox:cloudfox v%s :fox:", version)), cyan(awsProfile), err) TxtLog.Printf("Could not get caller's identity: %s", err) return CallerIdentity, err } // Convert CallerIdentity to something i can store using the cache Cache.Set(cacheKey, CallerIdentity, cache.DefaultExpiration) return CallerIdentity, err } func GetEnabledRegions(awsProfile string, version string, AwsMfaToken string) []string { cacheKey := fmt.Sprintf("GetEnabledRegions-%s", awsProfile) cached, found := Cache.Get(cacheKey) if found { return cached.([]string) } var enabledRegions []string ec2Client := ec2.NewFromConfig(ConfigMap[awsProfile]) regions, err := ec2Client.DescribeRegions( context.TODO(), &ec2.DescribeRegionsInput{ AllRegions: aws.Bool(false), }, ) if err != nil { servicemap := &awsservicemap.AwsServiceMap{ JsonFileSource: "DOWNLOAD_FROM_AWS", } AWSRegions, err := servicemap.GetAllRegions() if err != nil { TxtLog.Println(err) } return AWSRegions } for _, region := range regions.Regions { enabledRegions = append(enabledRegions, *region.RegionName) } Cache.Set(cacheKey, enabledRegions, cache.DefaultExpiration) return enabledRegions } // txtLogger - Returns the txt logger func TxtLogger() *logrus.Logger { var txtFile *os.File var err error txtLogger := logrus.New() txtFile, err = os.OpenFile(fmt.Sprintf("%s/cloudfox-error.log", ptr.ToString(GetLogDirPath())), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) if err != nil { txtFile, err = os.OpenFile(fmt.Sprintf("./cloudfox-error.log"), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) } if err != nil { panic(fmt.Sprintf("Failed to open log file %v", err)) } txtLogger.Out = txtFile txtLogger.SetLevel(logrus.InfoLevel) //txtLogger.SetReportCaller(true) return txtLogger } func CheckErr(e error, msg string) { if e != nil { TxtLog.Printf("[-] Error %s", msg) } } func GetAllAWSProfiles(AWSConfirm bool) []string { var AWSProfiles []string credentialsFile, err := UtilsFs.Open(config.DefaultSharedCredentialsFilename()) CheckErr(err, "could not open default AWS credentials file") if err == nil { defer credentialsFile.Close() scanner := bufio.NewScanner(credentialsFile) scanner.Split(bufio.ScanLines) for scanner.Scan() { text := strings.TrimSpace(scanner.Text()) if strings.HasPrefix(text, "[") && strings.HasSuffix(text, "]") { text = strings.TrimPrefix(text, "[") text = strings.TrimSuffix(text, "]") if !Contains(text, AWSProfiles) { AWSProfiles = append(AWSProfiles, text) } } } } configFile, err := UtilsFs.Open(config.DefaultSharedConfigFilename()) CheckErr(err, "could not open default AWS credentials file") if err == nil { defer configFile.Close() scanner2 := bufio.NewScanner(configFile) scanner2.Split(bufio.ScanLines) for scanner2.Scan() { text := strings.TrimSpace(scanner2.Text()) if strings.HasPrefix(text, "[") && strings.HasSuffix(text, "]") { text = strings.TrimPrefix(text, "[profile ") text = strings.TrimPrefix(text, "[") text = strings.TrimSuffix(text, "]") if !Contains(text, AWSProfiles) { AWSProfiles = append(AWSProfiles, text) } } } } if !AWSConfirm { result := ConfirmSelectedProfiles(AWSProfiles) if !result { os.Exit(1) } } return AWSProfiles } func ConfirmSelectedProfiles(AWSProfiles []string) bool { reader := bufio.NewReader(os.Stdin) fmt.Printf("[ %s] Identified profiles:\n\n", cyan(emoji.Sprintf(":fox:cloudfox :fox:"))) for _, profile := range AWSProfiles { fmt.Printf("\t* %s\n", profile) } fmt.Printf("\n[ %s] Are you sure you'd like to run this command against the [%d] listed profile(s)? (Y\\n): ", cyan(emoji.Sprintf(":fox:cloudfox :fox:")), len(AWSProfiles)) text, _ := reader.ReadString('\n') switch text { case "\n", "Y\n", "y\n": return true } return false } func GetSelectedAWSProfiles(AWSProfilesListPath string) []string { AWSProfilesListFile, err := UtilsFs.Open(AWSProfilesListPath) CheckErr(err, fmt.Sprintf("could not open given file %s", AWSProfilesListPath)) if err != nil { fmt.Printf("\nError loading profiles. Could not open file at location[%s]\n", AWSProfilesListPath) os.Exit(1) } defer AWSProfilesListFile.Close() var AWSProfiles []string scanner := bufio.NewScanner(AWSProfilesListFile) scanner.Split(bufio.ScanLines) for scanner.Scan() { profile := strings.TrimSpace(scanner.Text()) if len(profile) != 0 { AWSProfiles = append(AWSProfiles, profile) } } return AWSProfiles } func removeBadPathChars(receivedPath *string) string { var path string var bannedPathChars *regexp.Regexp = regexp.MustCompile(`[<>:"'|?*]`) path = bannedPathChars.ReplaceAllString(aws.ToString(receivedPath), "_") return path } func BuildAWSPath(Caller sts.GetCallerIdentityOutput) string { var callerAccount = removeBadPathChars(Caller.Account) var callerUserID = removeBadPathChars(Caller.UserId) return fmt.Sprintf("%s-%s", callerAccount, callerUserID) } // this is all for the spinner and command counter const clearln = "\r\x1b[2K" type CommandCounter struct { Total int Pending int Complete int Error int Executing int } func SpinUntil(callingModuleName string, counter *CommandCounter, done chan bool, spinType string) { defer close(done) for { select { case <-time.After(1 * time.Second): fmt.Printf(clearln+"[%s] Status: %d/%d %s complete (%d errors -- For details check %s)", cyan(callingModuleName), counter.Complete, counter.Total, spinType, counter.Error, fmt.Sprintf("%s/cloudfox-error.log", ptr.ToString(GetLogDirPath()))) case <-done: fmt.Printf(clearln+"[%s] Status: %d/%d %s complete (%d errors -- For details check %s)\n", cyan(callingModuleName), counter.Complete, counter.Complete, spinType, counter.Error, fmt.Sprintf("%s/cloudfox-error.log", ptr.ToString(GetLogDirPath()))) done <- true return } } } func ReorganizeAWSProfiles(allProfiles []string, mgmtProfile string) []string { // take the mgmt profile, move it from its current position to the front of the list var newProfiles []string newProfiles = append(newProfiles, mgmtProfile) for _, profile := range allProfiles { if profile != mgmtProfile { newProfiles = append(newProfiles, profile) } } return newProfiles }